feat: 添加JWKS管理功能,支持基于用户token创建新token

This commit is contained in:
2026-02-21 05:06:25 +08:00
parent 366a21d621
commit 77273bcfeb
11 changed files with 105 additions and 24 deletions

View File

@@ -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`;

View File

@@ -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 {
@@ -42,3 +43,4 @@ export const ai = useContextKey('ai', () => {
});
export { schema };

View File

@@ -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);
}

View File

@@ -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>('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,

View File

@@ -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>('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()

View File

@@ -1,2 +1,3 @@
export * from './oauth.ts';
export * from './salt.ts';
export * from './auth.ts';

View File

@@ -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');

View File

@@ -18,3 +18,5 @@ import './secret-key/list.ts';
import './wx-login.ts'
import './cnb-login.ts';
import './jwks.ts';

33
src/routes/user/jwks.ts Normal file
View File

@@ -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)

View File

@@ -1,4 +1,4 @@
import { manager } from '@/modules/jwks/index.ts'
import { manager } from '@/auth/models/jwks-manager.ts'
await manager.init()