本文基于
在@ tttt 的UI版本基础上做了一些自认为是优化的的优化
感谢两位大佬以及大佬文中提到的所有大佬的前期工作。让我得以方便的使用
我在@ tttt 的UI版本的基础上做了以下工作:
1.增加用户名用户白名单 ,参考@ Maple 本文的第二篇引用中的白名单内容
2.增加用户自定义密码的变量,用户名为变量key,用户名的密码为 变量value,使一起拼车的用户方便记住自己的密码(白名单用户第一次登录判定为密码正确,并存到变量表中后续验证取变量表中的密码进行验证)无需管理员手动加变量。
3.原UI版使用 https://yourdomain/auth/login_share?token=fk-xxx 的方式会暴露share_token,有一定风险。我对其进行了转OAuth的方式登陆,提高安全性,参考@ Maple 本文的第二篇引用中的转OAuth的方式登陆相关内容
4.对错误界面做了一定优化。
5.登录界面做了一些优化
6.增加display_lock_flag变量用于控制是否显示小锁标志,不填为不显示,填写true则显示小锁,不显示锁时加载稍微会更慢一点,会并行把其他页的响应拿到合并并过滤
workers代码:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
function parseJwt(token) {
const base64Url = token.split('.')[1]; // 获取载荷部分
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); // 将 Base64Url 转为 Base64
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload); // 返回载荷解析后的 JSON 对象
}
function isTokenExpired(token) {
const payload = parseJwt(token);
const currentTime = Math.floor(Date.now() / 1000); // 获取当前时间戳(秒)
return payload.exp < currentTime; // 检查 token 是否过期
}
async function handleRequest(request) {
const requestURL = new URL(request.url);
// ------------ 反代到 new.oaifree.com
const path = requestURL.pathname;
//拦截请求
if (path !== '/auth/login_auth0' && path !== '/auth/login') {
requestURL.host = 'new.oaifree.com';
if (path !== '/backend-api/conversations') {
return await fetch(new Request(requestURL, request));
}
const displayLock = await oai_global_variables.get('display_lock_flag');
if(displayLock === 'true'){
return await fetch(new Request(requestURL, request));
}
return await handleLockFlag(requestURL, request);
}
// ------------ 进行拿share_token去登录
const token = await oai_global_variables.get('at'); // 这里填入你的 JWT
if (isTokenExpired(token)) {
// 如果 Token 过期,执行获取新 Token 的逻辑
const url = 'https://token.oaifree.com/api/auth/refresh';
// @ts-ignore
const refreshToken = await oai_global_variables.get('rt'); // 实际情况下你可能会从某处获取这个值
// 发送 POST 请求
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body: `refresh_token=${refreshToken}`
});
// 检查响应状态
if (response.ok) {
const data = await response.json();
const token = data.access_token;
// @ts-ignore
await oai_global_variables.put('at', token);
} else {
return getErrorResponse(response.status)
}
}
// 如果 Token 未过期,继续执行原来的逻辑
if (request.method === "POST") {
const formData = await request.formData();
// @ts-ignore
const access_token = await oai_global_variables.get('at')
const unique_name = formData.get('unique_name');
const site_limit = '';
const expires_in = '0';
const gpt35_limit = '-1';
const gpt4_limit = '-1';
const show_conversations = 'false';
const show_userinfo = 'false';// 是否显示用户信息
const reset_limit = 'false';
// @ts-ignore
// 验证用户名是否存在
const users = await oai_global_variables.get("users");
if (!users.split(",").includes(unique_name)) {
return getErrorResponse(401);
}
const site_password = formData.get('site_password') || '';
const userPassword = await oai_global_variables.get(unique_name);
const userPasswordIsNull = userPassword === undefined || userPassword === null || userPassword === ''
if (userPasswordIsNull) {
// 首次登录,认定密码正确,并将密码存储
await oai_global_variables.put(unique_name, site_password);
}
// 变量中有密码且不对则拒绝访问
if (!userPasswordIsNull && site_password !== userPassword) {
return getErrorResponse(401);
}
const url = 'https://chat.oaifree.com/token/register';
const body = new URLSearchParams({
unique_name,
access_token, // 使用来自表单v或KV变量的 access_token
site_limit,
expires_in,
gpt35_limit,
gpt4_limit,
show_conversations,
show_userinfo,
reset_limit
}).toString();
const apiResponse = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: body
});
const respJson = await apiResponse.json();
const tokenKey = 'token_key' in respJson ? respJson.token_key : '未找到 Share_token';
// @ts-ignore
const DOMAIN = await oai_global_variables.get('DOMAIN') || requestURL.host;
// 重定向到登录 URL
return Response.redirect(await getOAuthLink(tokenKey, DOMAIN), 301);
}
return getLoginPage();
}
// 获取错误页面的函数
function getErrorResponse(statusCode) {
const errorMessageMap = {
401: '访问被拒绝,请检查您的凭据。',
404: '请求的资源未找到。',
500: '服务器内部错误,请稍后再试。'
};
const message = errorMessageMap[statusCode] || '发生未知错误。';
const html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>错误</title>
<link rel="icon" type="image/x-icon" href="https://cdn2.oaifree.com/_next/static/media/favicon-32x32.630a2b99.png">
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
padding: 20px;
text-align: center;
}
.error-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
max-width: 600px;
margin: 50px auto;
}
button {
background-color: #0f9977;
color: #ffffff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
width: 295px !important;
height: 52px;
}
h1 {
color: #333;
}
p {
color: #666;
}
</style>
</head>
<body>
<div class="error-container">
<h1>ERROR</h1>
<p>${message}</p>
<button onclick="history.back()">Back</button>
</div>
</body>
</html>
`;
return new Response(html, {
headers: {
'Content-Type': 'text/html; charset=utf-8'
},
status: statusCode
});
}
//获取登录页面的函数
function getLoginPage() {
const formHtml = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登陆</title>
<link rel="icon" type="image/x-icon" href="https://cdn2.oaifree.com/_next/static/media/favicon-32x32.630a2b99.png">
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 90vh;
overflow: hidden;
font-family: Arial, sans-serif;
background-color: #f4f4f4;
}
h1 {
text-align: center;
}
input {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
outline: none;
}
.input-wrapper {
position: relative;
margin-bottom: 20px;
}
.input-wrapper label {
position: absolute;
left: 10px;
top: 14px;
transition: all 0.3s ease;
color: #ccc;
}
.input-wrapper input {
width: 274px;
height: 52px;
padding: 0 10px;
border-radius: 5px;
border: 1px solid #ccc;
display: block;
font-size: 16px;
}
.input-wrapper input:not(:placeholder-shown) + label,
.input-wrapper input:focus + label {
top: -12px; /* Adjusted to not cover the line */
left: 20px; /* Moved right a bit */
font-size: 12px;
color: #0f9977;
}
.password-toggle {
position: absolute;
right: 10px;
top: 15px;
cursor: pointer;
}
button {
background-color: #0f9977;
color: #ffffff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
width: 295px !important;
height: 52px;
}
@media (max-width: 768px) {
body, form, .response-container {
padding: 20px;
}
}
</style>
</head>
<body>
<div>
<h1>Welcome Back</h1>
<form method="POST">
<div class="input-wrapper">
<input type="text" id="unique_name" name="unique_name" placeholder=" " required value="">
<label for="unique_name">Username</label>
</div>
<div class="input-wrapper">
<input type="password" id="site_password" name="site_password" placeholder=" ">
<label for="site_password">Password</label>
<span class="password-toggle" onclick="togglePassword()">
<svg width="24" height="24" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5C7 5 2.73 8.11 1 12.5C2.73 16.89 7 20 12 20C17 20 21.27 16.89 23 12.5C21.27 8.11 17 5 12 5ZM12 17C9.24 17 7 14.76 7 12C7 9.24 9.24 7 12 7C14.76 7 17 9.24 17 12C17 14.76 14.76 17 12 17ZM12 9C10.34 9 9 10.34 9 12C9 13.66 10.34 15 12 15C13.66 15 15 13.66 15 12C15 10.34 13.66 9 12 9Z" fill="#CCC"/>
</svg>
</span>
</div>
<button type="submit">Continue</button>
</form>
</div>
<script>
function togglePassword() {
var passwordInput = document.getElementById('site_password');
var passwordIcon = document.querySelector('.password-toggle svg path');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
passwordIcon.setAttribute('fill', '#0f9977'); // 更改图标颜色以反映状态更改
} else {
passwordInput.type = 'password';
passwordIcon.setAttribute('fill', '#CCC'); // 更改图标颜色以恢复原状
}
}
</script>
</body>
</html>
`;
return new Response(formHtml, {
headers: {
"Content-Type": "text/html; charset=utf-8",
},
});
}
// 获取 OAuth 登录链接的函数
async function getOAuthLink(tokenKey, DOMAIN) {
const url = `https://new.oaifree.com/api/auth/oauth_token`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Origin': `https://${DOMAIN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
share_token: tokenKey
})
})
const data = await response.json();
return data.login_url;
}
// 处理小锁去除逻辑
async function handleLockFlag(requestURL, request) {
const response = await fetch(new Request(requestURL, request));
let data = await response.json();
//获取总数
const total = data.total;
//构建所有页的请求
const requests = [];
if(total<=28){
// 总数小于一页,过滤锁定的数据直接返回
data.items = data.items.filter(item => item.title !== "🔒");
return new Response(JSON.stringify(data), {
status: response.status,
headers: response.headers
});
}
//计算多少页
const page = Math.ceil(total / 28);
for (let i = 1; i < page; i++) {
const tempRequestURL = new URL(requestURL);
tempRequestURL.searchParams.set('offset', String(i * 28));
tempRequestURL.searchParams.set('limit', String(28));
tempRequestURL.searchParams.set('order', 'updated');
requests.push(fetch(new Request(tempRequestURL, request)));
}
//并行执行所有请求
const responses = await Promise.all(requests);
//处理所有响应
for (const res of responses) {
const tempData = await res.json();
data.items = data.items.concat(tempData.items);
}
// 过滤锁定的数据
data.items = data.items.filter(item => item.title !== "🔒");
data.total = data.items.length;
data.offset = 0;
data.limit = data.items.length;
return new Response(JSON.stringify(data), {
status: response.status,
headers: response.headers
});
}