重构 JWKS 模块,新增 JWKS 管理器,优化密钥生成与管理逻辑,更新路由处理

This commit is contained in:
2026-02-12 21:11:16 +08:00
parent 3cca0720c1
commit bb8ce3338d
7 changed files with 184 additions and 39 deletions

6
.gitignore vendored
View File

@@ -2,11 +2,8 @@ node_modules
dist dist
app.config.json5
apps.config.json apps.config.json
deploy.tar.gz
cache-file cache-file
/apps /apps
@@ -22,6 +19,7 @@ release/*
!.env.example !.env.example
pack-dist pack-dist
app.config.json5.envision
/pages /pages
storage

View File

@@ -1,6 +1,6 @@
import { app } from './app.ts'; import { app } from './app.ts';
import './route.ts'; import './route.ts';
import { handleRequest } from './routes-simple/handle-request.ts'; import { handleRequest } from './routes-simple/index.ts';
import { port } from './modules/config.ts'; import { port } from './modules/config.ts';
import { wssFun } from './modules/ws-proxy/index.ts'; import { wssFun } from './modules/ws-proxy/index.ts';
import { WebSocketListenerFun, HttpListenerFun } from '@kevisual/router/src/server/server-type.js'; import { WebSocketListenerFun, HttpListenerFun } from '@kevisual/router/src/server/server-type.js';

View File

@@ -1,3 +1,4 @@
import { signJWT, decodeJWT, type JWTPayload, verifyJWT } from '@kevisual/auth'
import { generate } from '@kevisual/auth' import { generate } from '@kevisual/auth'
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -14,7 +15,6 @@ export const getPath = async (dir: string) => {
} }
} }
export const jwksGenerate = async (opts: { dir: string }) => { export const jwksGenerate = async (opts: { dir: string }) => {
const dir = path.isAbsolute(opts.dir) ? opts.dir : path.join(process.cwd(), opts.dir); const dir = path.isAbsolute(opts.dir) ? opts.dir : path.join(process.cwd(), opts.dir);
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
@@ -26,6 +26,105 @@ export const jwksGenerate = async (opts: { dir: string }) => {
fs.writeFileSync(PRIVATE_KEY_PATH, privatePEM); fs.writeFileSync(PRIVATE_KEY_PATH, privatePEM);
fs.writeFileSync(PRIVATE_JWK_PATH, JSON.stringify(privateJWK, null, 2)); fs.writeFileSync(PRIVATE_JWK_PATH, JSON.stringify(privateJWK, null, 2));
fs.writeFileSync(JWKS_PATH, JSON.stringify(jwks, null, 2)); fs.writeFileSync(JWKS_PATH, JSON.stringify(jwks, null, 2));
console.log(`Keys have been saved to directory: ${dir}`); console.log(`Keys 已保存到目录: ${dir}`);
} }
interface JWKSPaths {
JWKS_PATH: string
PRIVATE_JWK_PATH: string
PRIVATE_KEY_PATH: string
PUBLIC_KEY_PATH: string
}
interface JWKSContent {
jwks: string
privateJWK: string
privateKey: string
publicKey: string
}
export class JWKSManager {
private paths: JWKSPaths | null = null
private content: JWKSContent | null = null
constructor(private basePath?: string) {
this.basePath = basePath || path.join(process.cwd(), 'storage/jwks')
}
async init() {
// 确保目录存在
if (!fs.existsSync(this.basePath!)) {
fs.mkdirSync(this.basePath!, { recursive: true })
}
// 获取所有路径
this.paths = await getPath(this.basePath!)
// 如果 JWKS 文件不存在,则生成
if (!fs.existsSync(this.paths.JWKS_PATH)) {
await jwksGenerate({ dir: this.basePath! })
console.log(`JWKS 创建成功,路径: ${this.paths.JWKS_PATH}`)
}
// 加载所有内容到内存
await this.loadContent()
return this
}
async checkInit() {
if (!this.content) {
await this.init()
}
}
private async loadContent() {
if (!this.paths) {
await this.init()
}
this.content = {
jwks: fs.readFileSync(this.paths.JWKS_PATH, 'utf-8'),
privateJWK: fs.readFileSync(this.paths.PRIVATE_JWK_PATH, 'utf-8'),
privateKey: fs.readFileSync(this.paths.PRIVATE_KEY_PATH, 'utf-8'),
publicKey: fs.readFileSync(this.paths.PUBLIC_KEY_PATH, 'utf-8')
}
}
async sign(payload: JWTPayload): Promise<string> {
await this.checkInit()
return signJWT(payload, this.content.privateKey)
}
async verify(token: string) {
await this.checkInit()
return verifyJWT(token, this.content.publicKey)
}
async decode(token: string) {
await this.checkInit()
return decodeJWT(token)
}
async getJWKS() {
await this.checkInit()
return JSON.parse(this.content.jwks)
}
async getPrivateJWK() {
await this.checkInit()
return JSON.parse(this.content.privateJWK)
}
async getPublicKey() {
await this.checkInit()
return this.content.publicKey
}
async getPrivateKey() {
await this.checkInit()
return this.content.privateKey
}
getPaths() {
return this.paths
}
}
export const manager = new JWKSManager()

View File

@@ -1,31 +0,0 @@
import http from 'node:http';
import { router } from './router.ts';
import './index.ts';
import { handleRequest as PageProxy } from './page-proxy.ts';
const simpleAppsPrefixs = [
"/api/wxmsg"
];
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
if (req.url?.startsWith('/api/router')) {
// router自己管理
return;
}
// if (req.url === '/MP_verify_NGWvli5lGpEkByyt.txt') {
// res.writeHead(200, { 'Content-Type': 'text/plain' });
// res.end('NGWvli5lGpEkByyt');
// return;
// }
if (req.url && simpleAppsPrefixs.some(prefix => req.url!.startsWith(prefix))) {
// 简单应用路由处理
// 设置跨域
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return router.parse(req, res);
}
// 其他请求交给页面代理处理
return PageProxy(req, res);
};

View File

@@ -0,0 +1,33 @@
import http from 'node:http';
import { router } from './router.ts';
import { handleRequest as PageProxy } from './page-proxy.ts';
import './routes/jwks.ts'
const simpleAppsPrefixs = [
"/api/wxmsg",
"/api/convex/"
];
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
if (req.url?.startsWith('/api/router')) {
// router自己管理
return;
}
// if (req.url === '/MP_verify_NGWvli5lGpEkByyt.txt') {
// res.writeHead(200, { 'Content-Type': 'text/plain' });
// res.end('NGWvli5lGpEkByyt');
// return;
// }
if (req.url && simpleAppsPrefixs.some(prefix => req.url!.startsWith(prefix))) {
// 简单应用路由处理
// 设置跨域
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return router.parse(req, res);
}
// 其他请求交给页面代理处理
return PageProxy(req, res);
};

View File

@@ -0,0 +1,18 @@
import { router } from '@/app.ts'
import { manager } from '@/modules/jwks/index.ts'
router.all('/api/convex/jwks.json', async (req, res) => {
const jwks = await manager.getJWKS()
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(jwks));
})
// rsync -avz kevisual:/root/kevisual/assistant-app/storage/jwks/ ./storage/jwks
// router.all('/api/convex/sign', async (req, res) => {
// const payload = {
// sub: 'abc'
// };
// const token = await manager.sign(payload);
// res.setHeader('Content-Type', 'application/json');
// res.end(JSON.stringify({ token }));
// });

28
src/test/jwks.ts Normal file
View File

@@ -0,0 +1,28 @@
import { manager } from '@/modules/jwks/index.ts'
await manager.init()
// const value = await manager.sign({
// sub: 'user:abc',
// email: 'x@xiongxiao.me',
// })
// console.log('value', value)
const v2 = {
iss: "https://convex.kevisual.cn",
sub: "user:8fa2be73c2229e85",
aud: "convex-app",
exp: Math.floor(Date.now() / 1000) + 3600,
name: "Test User AA",
email: "test@example.com",
}
const value2 = await manager.sign(v2)
console.log('v2', value2)
const token = '33'
// const decode = await manager.decode(token)
// console.log('decode', decode)
// const verify = await manager.verify(token);
// console.log('verify', verify)