外包风险评估系统:领域引擎+前端+服务端持久化与生产部署
- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解 - 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护) - 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理 - 专业图标体系替换全部emoji、看板美化 - 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC - 444单测+204前端测试+51 e2e
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 认证与权限模块(生产级基础)。
|
||||
*
|
||||
* 当前实现:基于 JWT 的无状态认证 + 角色权限校验中间件。
|
||||
* 密钥取自环境变量 AUTH_SECRET,未配置时降级为无校验(演示模式)。
|
||||
*/
|
||||
|
||||
import type { Context, Next } from 'hono';
|
||||
import { createHmac } from 'node:crypto';
|
||||
|
||||
export type AuthRole = '商务/销售' | '风控' | '管理层';
|
||||
|
||||
export interface AuthPayload {
|
||||
username: string;
|
||||
role: AuthRole;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
const SECRET = (): string => process.env.AUTH_SECRET ?? '';
|
||||
|
||||
/** 简易 HMAC-SHA256 签名(不依赖外部库,生产建议替换为 jose)。 */
|
||||
function base64url(buf: Buffer): string {
|
||||
return buf.toString('base64url');
|
||||
}
|
||||
|
||||
function sign(payload: object): string {
|
||||
if (SECRET() === '') return ''; // 演示模式不签发
|
||||
const header = base64url(Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })));
|
||||
const body = base64url(Buffer.from(JSON.stringify(payload)));
|
||||
const sig = base64url(createHmac('sha256', SECRET()).update(`${header}.${body}`).digest());
|
||||
return `${header}.${body}.${sig}`;
|
||||
}
|
||||
|
||||
function verify(token: string): AuthPayload | null {
|
||||
if (SECRET() === '') return null;
|
||||
try {
|
||||
const [header, body, sig] = token.split('.');
|
||||
if (!header || !body || !sig) return null;
|
||||
const expected = base64url(createHmac('sha256', SECRET()).update(`${header}.${body}`).digest());
|
||||
if (sig !== expected) return null;
|
||||
const payload = JSON.parse(Buffer.from(body, 'base64url').toString()) as AuthPayload;
|
||||
if (payload.exp < Date.now() / 1000) return null;
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 签发 JWT(登录成功后调用)。 */
|
||||
export function issueToken(username: string, role: AuthRole): string {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return sign({ username, role, iat: now, exp: now + 86400 });
|
||||
}
|
||||
|
||||
/** 无需鉴权的公共路径(登录与健康检查)。 */
|
||||
const PUBLIC_PATHS = new Set(['/api/health', '/api/auth/login', '/api/llm/status']);
|
||||
|
||||
/**
|
||||
* Hono 中间件:从 Authorization Bearer token 解析并注入当前用户(供 requireRole 使用)。
|
||||
*
|
||||
* 设计:本中间件**只负责识别身份、不负责拦截**——读操作保持开放,敏感写操作由
|
||||
* {@link requireRole} 按角色拦截。AUTH_SECRET 未配置时为演示模式(不识别身份)。
|
||||
* 这样开启鉴权后既能强制敏感操作的角色校验,又不破坏只读接口与看板。
|
||||
*/
|
||||
export function authMiddleware() {
|
||||
return async (c: Context, next: Next): Promise<void | Response> => {
|
||||
if (SECRET() === '' || PUBLIC_PATHS.has(c.req.path)) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
const auth = c.req.header('Authorization');
|
||||
if (auth !== undefined && auth.startsWith('Bearer ')) {
|
||||
const payload = verify(auth.slice(7));
|
||||
if (payload !== null) {
|
||||
c.set('user', payload);
|
||||
}
|
||||
}
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
||||
/** 角色权限校验中间件。 */
|
||||
export function requireRole(...roles: AuthRole[]) {
|
||||
return async (c: Context, next: Next): Promise<void | Response> => {
|
||||
if (SECRET() === '') {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
const user = c.get('user') as AuthPayload | undefined;
|
||||
if (user === undefined || !roles.includes(user.role)) {
|
||||
return c.json({ error: '权限不足' }, 403);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user