From 77273bcfeb068a34e256e27052fcd80513877806 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Sat, 21 Feb 2026 05:06:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0JWKS=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E7=94=A8=E6=88=B7token=E5=88=9B=E5=BB=BA=E6=96=B0toke?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.config.mjs | 17 +++++++++ src/app.ts | 4 ++- .../index.ts => auth/models/jwks-manager.ts} | 0 src/auth/models/user-secret.ts | 36 ++++++++++--------- src/auth/models/user.ts | 23 ++++++++++-- src/auth/oauth/auth.ts | 5 +++ src/auth/oauth/index.ts | 3 +- src/routes-simple/routes/jwks.ts | 2 +- src/routes/user/index.ts | 4 ++- src/routes/user/jwks.ts | 33 +++++++++++++++++ src/test/jwks.ts | 2 +- 11 files changed, 105 insertions(+), 24 deletions(-) rename src/{modules/jwks/index.ts => auth/models/jwks-manager.ts} (100%) create mode 100644 src/routes/user/jwks.ts diff --git a/bun.config.mjs b/bun.config.mjs index dd412f9..49c3a4f 100644 --- a/bun.config.mjs +++ b/bun.config.mjs @@ -17,6 +17,23 @@ await Bun.build({ }, external, env: 'KEVISUAL_*', + // 启用模块转换和优化 + minify: false, + splitting: false, + sourcemap: 'external', + // 处理 CommonJS 到 ESM 的转换 + plugins: [{ + name: 'transform-requires', + setup(build) { + // 转换内置模块为 node: 前缀 + build.onResolve({ filter: /^(path|fs|module|url|util|crypto|stream|buffer|events|http|https|net|os|querystring|zlib|cluster|child_process|worker_threads|perf_hooks|inspector|dgram|dns|tls|readline|repl|process|assert|vm|timers|constants|string_decoder|punycode|v8)$/ }, args => { + return { + path: `node:${args.path}`, + external: true + } + }); + } + }] }); // const cmd = `dts -i src/index.ts -o app.d.ts`; diff --git a/src/app.ts b/src/app.ts index 4825af1..639fe85 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,6 +7,7 @@ import { BailianProvider } from '@kevisual/ai'; import * as schema from './db/schema.ts'; import { config } from './modules/config.ts' import { db } from './modules/db.ts' + export const router = useContextKey('router', () => new SimpleRouter()); export const runtime = useContextKey('runtime', () => { return { @@ -41,4 +42,5 @@ export const ai = useContextKey('ai', () => { }); }); -export { schema }; \ No newline at end of file +export { schema }; + diff --git a/src/modules/jwks/index.ts b/src/auth/models/jwks-manager.ts similarity index 100% rename from src/modules/jwks/index.ts rename to src/auth/models/jwks-manager.ts diff --git a/src/auth/models/user-secret.ts b/src/auth/models/user-secret.ts index a31d040..f5809ea 100644 --- a/src/auth/models/user-secret.ts +++ b/src/auth/models/user-secret.ts @@ -1,7 +1,7 @@ import { useContextKey } from '@kevisual/context'; import { Redis } from 'ioredis'; import { User } from './user.ts'; -import { oauth } from '../oauth/auth.ts'; +import { oauth, jwksManager } from '../oauth/auth.ts'; import { OauthUser } from '../oauth/oauth.ts'; import { db } from '../../modules/db.ts'; import { cfUserSecrets, cfUser } from '../../db/drizzle/schema.ts'; @@ -53,6 +53,10 @@ export class UserSecret { * @returns */ static async verifyToken(token: string) { + if (token?.includes?.('.')) { + // 先尝试作为jwt token验证,如果验证成功则直接返回用户信息 + return await jwksManager.verify(token); + } if (!oauth.isSecretKey(token)) { return await oauth.verifyToken(token); } @@ -62,11 +66,11 @@ export class UserSecret { } console.log('verifyToken: try to verify as secret key'); const userSecrets = await db.select().from(userSecretsTable).where(eq(userSecretsTable.token, token)).limit(1); - + if (userSecrets.length === 0) { return null; // 如果没有找到对应的用户密钥,则返回null } - + const userSecret = new UserSecret(userSecrets[0]); if (userSecret.isExpired()) { return null; // 如果用户密钥已过期,则返回null @@ -97,13 +101,13 @@ export class UserSecret { */ static async findOne(where: { token?: string; id?: string }): Promise { let query = db.select().from(userSecretsTable); - + if (where.token) { query = query.where(eq(userSecretsTable.token, where.token)) as any; } else if (where.id) { query = query.where(eq(userSecretsTable.id, where.id)) as any; } - + const secrets = await query.limit(1); return secrets.length > 0 ? new UserSecret(secrets[0]) : null; } @@ -119,12 +123,12 @@ export class UserSecret { owner: usersTable.owner, data: usersTable.data, }).from(usersTable).where(eq(usersTable.id, this.userId)).limit(1); - + let org: any = null; if (users.length === 0) { return null; // 如果没有找到对应的用户,则返回null } - + const user = users[0]; const expiredTime = this.expiredTime ? new Date(this.expiredTime).getTime() : null; const oauthUser: Partial = { @@ -142,7 +146,7 @@ export class UserSecret { type: usersTable.type, owner: usersTable.owner, }).from(usersTable).where(eq(usersTable.id, this.orgId)).limit(1); - + if (orgUsers.length > 0) { org = orgUsers[0]; oauthUser.id = org.id; @@ -164,7 +168,7 @@ export class UserSecret { const expiredTime = new Date(this.expiredTime); return now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期 } - + /** * 检查是否过期,如果过期则更新状态为expired * @@ -225,7 +229,7 @@ export class UserSecret { await this.save(); return token; } - + static async createToken() { let token = oauth.generateSecretKey(); // 确保生成的token是唯一的 @@ -234,7 +238,7 @@ export class UserSecret { } return token; } - + /** * 根据 unionid 生成redis的key * `wxmp:unionid:token:${unionid}` @@ -244,13 +248,13 @@ export class UserSecret { static wxRedisKey(unionid: string) { return `wxmp:unionid:token:${unionid}`; } - + static getExpiredTime(expireDays?: number) { const defaultExpireDays = expireDays || 365; const expireTime = defaultExpireDays * 24 * 60 * 60 * 1000; return new Date(Date.now() + expireTime); } - + static async createSecret(tokenUser: { id: string; uid?: string, title?: string }, expireDays = 365) { const token = await UserSecret.createToken(); let userId = tokenUser.id; @@ -259,18 +263,18 @@ export class UserSecret { userId = tokenUser.uid; orgId = tokenUser.id; } - + const insertData: Partial = { userId, token, title: tokenUser.title || randomString(6), expiredTime: UserSecret.getExpiredTime(expireDays).toISOString(), }; - + if (orgId !== null && orgId !== undefined) { insertData.orgId = orgId; } - + const inserted = await db.insert(userSecretsTable).values(insertData).returning(); return new UserSecret(inserted[0]); diff --git a/src/auth/models/user.ts b/src/auth/models/user.ts index a666514..07e34b6 100644 --- a/src/auth/models/user.ts +++ b/src/auth/models/user.ts @@ -2,7 +2,7 @@ import { nanoid, customAlphabet } from 'nanoid'; import { CustomError } from '@kevisual/router'; import { useContextKey } from '@kevisual/context'; import { Redis } from 'ioredis'; -import { oauth } from '../oauth/auth.ts'; +import { oauth, jwksManager } from '../oauth/auth.ts'; import { cryptPwd } from '../oauth/salt.ts'; import { OauthUser } from '../oauth/oauth.ts'; import { db } from '../../modules/db.ts'; @@ -36,6 +36,9 @@ const userSecretsTable = cfUserSecrets; export const redis = useContextKey('redis'); +type TokenOptions = { + expire?: number; // 过期时间,单位秒 +} /** * 用户模型,使用 Drizzle ORM */ @@ -69,7 +72,7 @@ export class User { * @param uid * @returns */ - async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week', expand: any = {}) { + async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'jwks', opts: TokenOptions = {}) { const { id, username, type } = this; const oauthUser: OauthUser = { id, @@ -81,7 +84,21 @@ export class User { if (uid) { oauthUser.orgId = id; } - const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...expand }); + if (loginType === 'jwks') { + const accessToken = await jwksManager.sign({ + sub: 'user:' + this.id, + name: this.username, + }); + const expiresIn = opts?.expire ?? 2 * 3600; // 2 hours + return { + accessToken: accessToken, + refreshToken: null, + token: accessToken, + refreshTokenExpiresIn: null, + accessTokenExpiresIn: expiresIn + }; + } + const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...opts }); return { accessToken: token.accessToken, refreshToken: token.refreshToken, diff --git a/src/auth/oauth/auth.ts b/src/auth/oauth/auth.ts index 44a0328..5dda441 100644 --- a/src/auth/oauth/auth.ts +++ b/src/auth/oauth/auth.ts @@ -1,6 +1,7 @@ import { OAuth, RedisTokenStore } from './oauth.ts'; import { useContextKey } from '@kevisual/context'; import { Redis } from 'ioredis'; +import { manager } from '../models/jwks-manager.ts'; export const oauth = useContextKey('oauth', () => { const redis = useContextKey('redis'); @@ -16,3 +17,7 @@ export const oauth = useContextKey('oauth', () => { const oauth = new OAuth(store); return oauth; }); + +export const jwksManager = useContextKey('jwksManager', () => manager); + +await manager.init() diff --git a/src/auth/oauth/index.ts b/src/auth/oauth/index.ts index 3ebdf92..2f40674 100644 --- a/src/auth/oauth/index.ts +++ b/src/auth/oauth/index.ts @@ -1,2 +1,3 @@ export * from './oauth.ts'; -export * from './salt.ts'; \ No newline at end of file +export * from './salt.ts'; +export * from './auth.ts'; \ No newline at end of file diff --git a/src/routes-simple/routes/jwks.ts b/src/routes-simple/routes/jwks.ts index 75a71fa..69efeb9 100644 --- a/src/routes-simple/routes/jwks.ts +++ b/src/routes-simple/routes/jwks.ts @@ -1,5 +1,5 @@ import { router } from '@/app.ts' -import { manager } from '@/modules/jwks/index.ts' +import { manager } from '@/auth/models/jwks-manager.ts' router.all('/api/convex/jwks.json', async (req, res) => { const jwks = await manager.getJWKS() res.setHeader('Content-Type', 'application/json'); diff --git a/src/routes/user/index.ts b/src/routes/user/index.ts index 233bfe0..64c223e 100644 --- a/src/routes/user/index.ts +++ b/src/routes/user/index.ts @@ -17,4 +17,6 @@ import './secret-key/list.ts'; import './wx-login.ts' -import './cnb-login.ts'; \ No newline at end of file +import './cnb-login.ts'; + +import './jwks.ts'; \ No newline at end of file diff --git a/src/routes/user/jwks.ts b/src/routes/user/jwks.ts new file mode 100644 index 0000000..8ab6826 --- /dev/null +++ b/src/routes/user/jwks.ts @@ -0,0 +1,33 @@ +import { app } from '@/app.ts' +import { UserModel } from '@/auth/index.ts'; +import z from 'zod'; + +app.route({ + path: 'user', + key: 'token-create', + description: '根据用户token创建一个新的token,主要用于临时访问', + middleware: ['auth'], + metadata: { + args: { + loginType: z.enum(['jwks']).optional(), + } + } +}).define(async (ctx) => { + const user = await UserModel.getUserByToken(ctx.query.token); + const loginType = ctx.query?.loginType ?? 'jwks'; + if (!user) { + ctx.throw(404, 'user not found'); + } + if (loginType !== 'jwks') { + ctx.throw(400, 'unsupported login type'); + } + let expire = ctx.query.expire ?? 24 * 3600; + // 大于24小时的过期时间需要管理员权限 + if (expire > 24 * 3600) { + expire = 2 * 3600; + } + const value = await user.createToken(null, loginType, { + expire: expire, // 24小时过期 + }) + ctx.body = value +}).addTo(app) \ No newline at end of file diff --git a/src/test/jwks.ts b/src/test/jwks.ts index f9c49fe..36c3602 100644 --- a/src/test/jwks.ts +++ b/src/test/jwks.ts @@ -1,4 +1,4 @@ -import { manager } from '@/modules/jwks/index.ts' +import { manager } from '@/auth/models/jwks-manager.ts' await manager.init()