mirror of
https://github.com/ZeroCatDev/ClassworksKV.git
synced 2025-12-10 08:03:09 +00:00
Compare commits
4 Commits
9f4f2a537f
...
be1d8d1328
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be1d8d1328 | ||
|
|
0576a02d6e | ||
|
|
d83d748da0 | ||
|
|
6ab78af370 |
@ -24,7 +24,7 @@ const CONFIG = {
|
|||||||
// 应用ID
|
// 应用ID
|
||||||
appId: process.env.APP_ID || '1',
|
appId: process.env.APP_ID || '1',
|
||||||
// 授权页面地址(Classworks前端)
|
// 授权页面地址(Classworks前端)
|
||||||
authPageUrl: process.env.AUTH_PAGE_URL || 'http://localhost:5173/authorize',
|
authPageUrl: process.env.FRONTEND_URL,
|
||||||
// 本地回调服务器端口
|
// 本地回调服务器端口
|
||||||
callbackPort: process.env.CALLBACK_PORT || '8080',
|
callbackPort: process.env.CALLBACK_PORT || '8080',
|
||||||
// 回调路径
|
// 回调路径
|
||||||
|
|||||||
@ -21,7 +21,7 @@ const CONFIG = {
|
|||||||
// 应用ID
|
// 应用ID
|
||||||
appId: process.env.APP_ID || '1',
|
appId: process.env.APP_ID || '1',
|
||||||
// 授权页面地址(Classworks前端)
|
// 授权页面地址(Classworks前端)
|
||||||
authPageUrl: process.env.AUTH_PAGE_URL || 'http://localhost:5173/authorize',
|
authPageUrl: process.env.FRONTEND_URL,
|
||||||
// 轮询间隔(秒)
|
// 轮询间隔(秒)
|
||||||
pollInterval: 3,
|
pollInterval: 3,
|
||||||
// 最大轮询次数
|
// 最大轮询次数
|
||||||
|
|||||||
@ -24,6 +24,20 @@ export const oauthProviders = {
|
|||||||
color: "#6366f1",
|
color: "#6366f1",
|
||||||
description: "使用 ZeroCat 账号登录",
|
description: "使用 ZeroCat 账号登录",
|
||||||
},
|
},
|
||||||
|
hly: {
|
||||||
|
// 厚浪云(Logto) - OIDC Provider
|
||||||
|
clientId: process.env.HLY_CLIENT_ID,
|
||||||
|
clientSecret: process.env.HLY_CLIENT_SECRET, // 可选:若使用PKCE且应用为Public,可不配置
|
||||||
|
authorizationURL: "https://oauth.houlang.cloud/oidc/auth",
|
||||||
|
tokenURL: "https://oauth.houlang.cloud/oidc/token",
|
||||||
|
userInfoURL: "https://oauth.houlang.cloud/oidc/me",
|
||||||
|
scope: "openid profile email offline_access",
|
||||||
|
name: "厚浪云",
|
||||||
|
icon: "logto",
|
||||||
|
color: "#0ea5e9",
|
||||||
|
description: "使用厚浪云账号登录",
|
||||||
|
pkce: true, // 启用PKCE支持
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取OAuth回调URL
|
// 获取OAuth回调URL
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ClassworksKV",
|
"name": "ClassworksKV",
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www",
|
"start": "node ./bin/www",
|
||||||
|
|||||||
@ -11,6 +11,24 @@ const prisma = new PrismaClient();
|
|||||||
// 存储OAuth state,防止CSRF攻击(生产环境应使用Redis等)
|
// 存储OAuth state,防止CSRF攻击(生产环境应使用Redis等)
|
||||||
const oauthStates = new Map();
|
const oauthStates = new Map();
|
||||||
|
|
||||||
|
// 生成PKCE code_verifier 和 code_challenge
|
||||||
|
function generatePkcePair() {
|
||||||
|
const codeVerifier = crypto
|
||||||
|
.randomBytes(32)
|
||||||
|
.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/, "");
|
||||||
|
const challenge = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(codeVerifier)
|
||||||
|
.digest("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/, "");
|
||||||
|
return { codeVerifier, codeChallenge: challenge };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成安全的访问令牌
|
* 生成安全的访问令牌
|
||||||
*/
|
*/
|
||||||
@ -27,7 +45,8 @@ router.get("/oauth/providers", (req, res) => {
|
|||||||
|
|
||||||
for (const [key, config] of Object.entries(oauthProviders)) {
|
for (const [key, config] of Object.entries(oauthProviders)) {
|
||||||
// 只返回已配置的提供者
|
// 只返回已配置的提供者
|
||||||
if (config.clientId && config.clientSecret) {
|
const pkceAllowed = !!config.pkce;
|
||||||
|
if (config.clientId && (config.clientSecret || pkceAllowed)) {
|
||||||
providers.push({
|
providers.push({
|
||||||
id: key,
|
id: key,
|
||||||
name: config.name,
|
name: config.name,
|
||||||
@ -64,7 +83,8 @@ router.get("/oauth/:provider", (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!providerConfig.clientId || !providerConfig.clientSecret) {
|
const pkceAllowed = !!providerConfig.pkce;
|
||||||
|
if (!providerConfig.clientId || (!providerConfig.clientSecret && !pkceAllowed)) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: `OAuth提供者 ${provider} 未配置`,
|
message: `OAuth提供者 ${provider} 未配置`,
|
||||||
@ -74,11 +94,20 @@ router.get("/oauth/:provider", (req, res) => {
|
|||||||
// 生成state参数
|
// 生成state参数
|
||||||
const state = generateState();
|
const state = generateState();
|
||||||
|
|
||||||
|
// PKCE: 若启用,为此次会话生成code_verifier/challenge
|
||||||
|
let codeChallenge, codeVerifier;
|
||||||
|
if (pkceAllowed) {
|
||||||
|
const pair = generatePkcePair();
|
||||||
|
codeVerifier = pair.codeVerifier;
|
||||||
|
codeChallenge = pair.codeChallenge;
|
||||||
|
}
|
||||||
|
|
||||||
// 保存state和redirect_uri(5分钟过期)
|
// 保存state和redirect_uri(5分钟过期)
|
||||||
oauthStates.set(state, {
|
oauthStates.set(state, {
|
||||||
provider,
|
provider,
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
codeVerifier,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清理过期的state(超过5分钟)
|
// 清理过期的state(超过5分钟)
|
||||||
@ -103,6 +132,11 @@ router.get("/oauth/:provider", (req, res) => {
|
|||||||
params.append("prompt", "consent");
|
params.append("prompt", "consent");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pkceAllowed && codeChallenge) {
|
||||||
|
params.append("code_challenge", codeChallenge);
|
||||||
|
params.append("code_challenge_method", "S256");
|
||||||
|
}
|
||||||
|
|
||||||
const authUrl = `${providerConfig.authorizationURL}?${params.toString()}`;
|
const authUrl = `${providerConfig.authorizationURL}?${params.toString()}`;
|
||||||
|
|
||||||
// 重定向到OAuth提供者
|
// 重定向到OAuth提供者
|
||||||
@ -153,10 +187,12 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
|||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: providerConfig.clientId,
|
client_id: providerConfig.clientId,
|
||||||
client_secret: providerConfig.clientSecret,
|
...(providerConfig.clientSecret ? { client_secret: providerConfig.clientSecret } : {}),
|
||||||
code: code,
|
code: code,
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
redirect_uri: getCallbackURL(provider),
|
redirect_uri: getCallbackURL(provider),
|
||||||
|
// PKCE: 携带code_verifier
|
||||||
|
...(stateData?.codeVerifier ? { code_verifier: stateData.codeVerifier } : {}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -176,7 +212,7 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
|||||||
|
|
||||||
const userData = await userResponse.json();
|
const userData = await userResponse.json();
|
||||||
|
|
||||||
// 3. 标准化用户数据(不同提供者返回的字段不同)
|
// 3. 标准化用户数据(不同提供者返回的字段不同)
|
||||||
let normalizedUser = {};
|
let normalizedUser = {};
|
||||||
|
|
||||||
if (provider === "github") {
|
if (provider === "github") {
|
||||||
@ -193,6 +229,22 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
|||||||
name: userData.nickname || userData.username,
|
name: userData.nickname || userData.username,
|
||||||
avatarUrl: userData.avatar,
|
avatarUrl: userData.avatar,
|
||||||
};
|
};
|
||||||
|
} else if (provider === "hly") {
|
||||||
|
// 厚浪云(Logto)标准OIDC用户信息
|
||||||
|
normalizedUser = {
|
||||||
|
providerId: userData.sub,
|
||||||
|
email: userData.email_verified ? userData.email : null,
|
||||||
|
name: userData.name || userData.preferred_username || userData.nickname,
|
||||||
|
avatarUrl: userData.picture,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 名称为空时,用邮箱@前部分回填(若邮箱可用)
|
||||||
|
if ((!normalizedUser.name || normalizedUser.name.trim() === "") && normalizedUser.email) {
|
||||||
|
const at = normalizedUser.email.indexOf("@");
|
||||||
|
if (at > 0) {
|
||||||
|
normalizedUser.name = normalizedUser.email.substring(0, at);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 查找或创建账户
|
// 4. 查找或创建账户
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user