203 lines
5.4 KiB
TypeScript
203 lines
5.4 KiB
TypeScript
/**
|
||
* JWT工具类
|
||
* 用于生成和验证前端专用的JWT Token
|
||
*
|
||
* 主要功能:
|
||
* - 生成包含用户信息的JWT
|
||
* - 验证JWT的有效性
|
||
* - 解析JWT获取用户信息
|
||
*/
|
||
|
||
import jwt from 'jsonwebtoken';
|
||
const { sign, verify, decode } = jwt;
|
||
|
||
// JWT密钥 - 从环境变量读取,如果未设置则抛出错误
|
||
const JWT_SECRET: string = (() => {
|
||
const secret = process.env.JWT_SECRET;
|
||
if (!secret) {
|
||
throw new Error('JWT_SECRET environment variable is not set. Please add it to your .env file.');
|
||
}
|
||
return secret;
|
||
})();
|
||
|
||
// JWT配置
|
||
const JWT_CONFIG = {
|
||
algorithm: 'HS256' as const,
|
||
issuer: 'docreview-system',
|
||
audience: 'docreview-frontend'
|
||
};
|
||
|
||
// JWT载荷接口
|
||
export interface JWTPayload {
|
||
// 标准字段
|
||
sub: string; // 用户唯一标识(来自IDaaS)
|
||
iss: string; // 发行者
|
||
aud: string; // 受众
|
||
iat: number; // 签发时间
|
||
exp: number; // 过期时间
|
||
|
||
// 自定义用户信息字段
|
||
user_id: string; // 数据库中的用户ID
|
||
username: string; // 用户名
|
||
nick_name: string; // 用户昵称
|
||
email?: string; // 邮箱
|
||
phone_number?: string; // 手机号
|
||
ou_id: string; // 组织单位ID
|
||
ou_name: string; // 组织单位名称
|
||
is_leader: boolean; // 是否为负责人
|
||
user_role: string; // 用户角色
|
||
}
|
||
|
||
// 用户信息接口(用于生成JWT)
|
||
export interface UserInfoForJWT {
|
||
sub: string;
|
||
user_id: string;
|
||
username: string;
|
||
nick_name: string;
|
||
email?: string;
|
||
phone_number?: string;
|
||
ou_id: string;
|
||
ou_name: string;
|
||
is_leader: boolean;
|
||
user_role: string;
|
||
}
|
||
|
||
/**
|
||
* JWT工具类
|
||
*/
|
||
export class JWTUtils {
|
||
/**
|
||
* 生成JWT
|
||
* @param userInfo 用户信息
|
||
* @param expiresIn 过期时间(秒),默认为OAuth token过期时间的90%
|
||
* @returns JWT字符串
|
||
*/
|
||
static generateJWT(userInfo: UserInfoForJWT, expiresIn: number): string {
|
||
const now = Math.floor(Date.now() / 1000);
|
||
|
||
// 将过期时间设置为OAuth token过期时间的90%,确保JWT在OAuth token之前过期
|
||
const jwtExpiresIn = Math.floor(expiresIn);
|
||
|
||
const payload: JWTPayload = {
|
||
// 标准字段
|
||
sub: userInfo.sub,
|
||
iss: JWT_CONFIG.issuer,
|
||
aud: JWT_CONFIG.audience,
|
||
iat: now,
|
||
exp: now + jwtExpiresIn,
|
||
|
||
// 用户信息字段
|
||
user_id: userInfo.user_id,
|
||
username: userInfo.username,
|
||
nick_name: userInfo.nick_name,
|
||
email: userInfo.email,
|
||
phone_number: userInfo.phone_number,
|
||
ou_id: userInfo.ou_id,
|
||
ou_name: userInfo.ou_name,
|
||
is_leader: userInfo.is_leader,
|
||
user_role: userInfo.user_role
|
||
};
|
||
|
||
return sign(payload, JWT_SECRET, {
|
||
algorithm: JWT_CONFIG.algorithm
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 验证JWT
|
||
* @param token JWT字符串
|
||
* @returns 验证结果和载荷
|
||
*/
|
||
static verifyJWT(token: string): { valid: boolean; payload?: JWTPayload; error?: string } {
|
||
try {
|
||
const decoded = verify(token, JWT_SECRET, {
|
||
algorithms: [JWT_CONFIG.algorithm],
|
||
issuer: JWT_CONFIG.issuer,
|
||
audience: JWT_CONFIG.audience
|
||
});
|
||
|
||
// 验证返回的payload是否包含必需字段
|
||
if (typeof decoded === 'object' && decoded !== null && 'sub' in decoded) {
|
||
const payload = decoded as JWTPayload;
|
||
return { valid: true, payload };
|
||
}
|
||
|
||
return { valid: false, error: 'JWT载荷格式不正确' };
|
||
} catch (error) {
|
||
if (error instanceof Error) {
|
||
return { valid: false, error: error.message };
|
||
}
|
||
return { valid: false, error: 'JWT验证失败' };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析JWT(不验证签名)
|
||
* @param token JWT字符串
|
||
* @returns 解析结果
|
||
*/
|
||
static decodeJWT(token: string): { payload?: JWTPayload; error?: string } {
|
||
try {
|
||
const payload = decode(token) as JWTPayload;
|
||
return { payload };
|
||
} catch (error) {
|
||
return { error: 'JWT解析失败' };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查JWT是否即将过期
|
||
* @param token JWT字符串
|
||
* @param bufferMinutes 缓冲时间(分钟),默认5分钟
|
||
* @returns 是否即将过期
|
||
*/
|
||
static isJWTExpiringSoon(token: string, bufferMinutes: number = 5): boolean {
|
||
const decoded = this.decodeJWT(token);
|
||
if (!decoded.payload) {
|
||
return true; // 解析失败视为过期
|
||
}
|
||
|
||
const now = Math.floor(Date.now() / 1000);
|
||
const bufferSeconds = bufferMinutes * 60;
|
||
|
||
return decoded.payload.exp <= (now + bufferSeconds);
|
||
}
|
||
|
||
/**
|
||
* 获取JWT过期时间
|
||
* @param token JWT字符串
|
||
* @returns 过期时间戳
|
||
*/
|
||
static getJWTExpiration(token: string): number | null {
|
||
const decoded = this.decodeJWT(token);
|
||
return decoded.payload?.exp || null;
|
||
}
|
||
|
||
/**
|
||
* 从JWT中提取用户信息
|
||
* @param token JWT字符串
|
||
* @returns 用户信息
|
||
*/
|
||
static extractUserInfo(token: string): UserInfoForJWT | null {
|
||
const verification = this.verifyJWT(token);
|
||
if (!verification.valid || !verification.payload) {
|
||
return null;
|
||
}
|
||
|
||
const payload = verification.payload;
|
||
return {
|
||
sub: payload.sub,
|
||
user_id: payload.user_id,
|
||
username: payload.username,
|
||
nick_name: payload.nick_name,
|
||
email: payload.email,
|
||
phone_number: payload.phone_number,
|
||
ou_id: payload.ou_id,
|
||
ou_name: payload.ou_name,
|
||
is_leader: payload.is_leader,
|
||
user_role: payload.user_role
|
||
};
|
||
}
|
||
}
|
||
|
||
export default JWTUtils;
|