/** * JWT Service * * JWT令牌的生成、验证和刷新 * * 设计原则: * - 使用 jsonwebtoken 库 * - Token payload 包含用户ID、角色、租户ID * - 支持 Access Token 和 Refresh Token */ import jwt, { SignOptions, JwtPayload } from 'jsonwebtoken'; import { config } from '../../config/env.js'; /** * JWT Payload 接口 */ export interface JWTPayload { /** 用户ID */ userId: string; /** 用户手机号 */ phone: string; /** 用户角色 */ role: string; /** 租户ID */ tenantId: string; /** 租户Code(用于URL路由) */ tenantCode?: string; /** 是否为默认密码 */ isDefaultPassword?: boolean; } /** * Token 响应接口 */ export interface TokenResponse { accessToken: string; refreshToken: string; expiresIn: number; // 秒 tokenType: 'Bearer'; } /** * 解码后的 Token 接口 */ export interface DecodedToken extends JWTPayload { iat: number; // 签发时间 exp: number; // 过期时间 } // Token 配置 const ACCESS_TOKEN_EXPIRES_IN = '2h'; // Access Token 2小时过期 const REFRESH_TOKEN_EXPIRES_IN = '7d'; // Refresh Token 7天过期 const ACCESS_TOKEN_EXPIRES_SECONDS = 2 * 60 * 60; // 7200秒 /** * JWT Service 类 */ export class JWTService { private readonly secret: string; constructor() { this.secret = config.jwtSecret; if (this.secret === 'your-secret-key-change-in-production' && config.nodeEnv === 'production') { throw new Error('JWT_SECRET must be configured in production environment'); } } /** * 生成 Access Token */ generateAccessToken(payload: JWTPayload): string { const options: SignOptions = { expiresIn: ACCESS_TOKEN_EXPIRES_IN, issuer: 'aiclinical', subject: payload.userId, }; return jwt.sign(payload, this.secret, options); } /** * 生成 Refresh Token */ generateRefreshToken(payload: JWTPayload): string { // Refresh Token 只包含必要信息 const refreshPayload = { userId: payload.userId, type: 'refresh', }; const options: SignOptions = { expiresIn: REFRESH_TOKEN_EXPIRES_IN, issuer: 'aiclinical', subject: payload.userId, }; return jwt.sign(refreshPayload, this.secret, options); } /** * 生成完整的 Token 响应 */ generateTokens(payload: JWTPayload): TokenResponse { return { accessToken: this.generateAccessToken(payload), refreshToken: this.generateRefreshToken(payload), expiresIn: ACCESS_TOKEN_EXPIRES_SECONDS, tokenType: 'Bearer', }; } /** * 验证 Token */ verifyToken(token: string): DecodedToken { try { const decoded = jwt.verify(token, this.secret, { issuer: 'aiclinical', }) as DecodedToken; return decoded; } catch (error) { if (error instanceof jwt.TokenExpiredError) { throw new Error('Token已过期'); } if (error instanceof jwt.JsonWebTokenError) { throw new Error('Token无效'); } throw error; } } /** * 刷新 Token * * 使用 Refresh Token 获取新的 Access Token */ async refreshToken(refreshToken: string, getUserById: (id: string) => Promise): Promise { // 验证 Refresh Token const decoded = this.verifyToken(refreshToken); if ((decoded as any).type !== 'refresh') { throw new Error('无效的Refresh Token'); } // 获取用户最新信息 const user = await getUserById(decoded.userId); if (!user) { throw new Error('用户不存在'); } // 生成新的 Tokens return this.generateTokens(user); } /** * 从 Authorization Header 中提取 Token */ extractTokenFromHeader(authHeader: string | undefined): string | null { if (!authHeader) { return null; } const [type, token] = authHeader.split(' '); if (type !== 'Bearer' || !token) { return null; } return token; } /** * 解码 Token(不验证签名) * * 用于调试或获取过期token的payload */ decodeToken(token: string): DecodedToken | null { const decoded = jwt.decode(token); return decoded as DecodedToken | null; } } // 导出单例 export const jwtService = new JWTService();