|
@@ -0,0 +1,90 @@
|
|
|
|
|
+import jwt, { SignOptions } from 'jsonwebtoken';
|
|
|
|
|
+import { UserEntity } from '@/server/modules/users/user.entity';
|
|
|
|
|
+import debug from 'debug';
|
|
|
|
|
+
|
|
|
|
|
+const logger = {
|
|
|
|
|
+ info: debug('backend:jwt:info'),
|
|
|
|
|
+ error: debug('backend:jwt:error')
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
|
|
|
+const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
|
|
|
|
|
+
|
|
|
|
|
+export interface JWTPayload {
|
|
|
|
|
+ id: number;
|
|
|
|
|
+ username: string;
|
|
|
|
|
+ roles?: string[];
|
|
|
|
|
+ openid?: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export class JWTUtil {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 生成 JWT token
|
|
|
|
|
+ * @param user 用户实体
|
|
|
|
|
+ * @param additionalPayload 额外的 payload 数据
|
|
|
|
|
+ * @returns JWT token
|
|
|
|
|
+ */
|
|
|
|
|
+ static generateToken(user: UserEntity, additionalPayload: Partial<JWTPayload> = {}): string {
|
|
|
|
|
+ if (!user.id || !user.username) {
|
|
|
|
|
+ throw new Error('用户ID和用户名不能为空');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const payload: JWTPayload = {
|
|
|
|
|
+ id: user.id,
|
|
|
|
|
+ username: user.username,
|
|
|
|
|
+ roles: user.roles?.map(role => role.name) || [],
|
|
|
|
|
+ openid: user.openid || undefined,
|
|
|
|
|
+ ...additionalPayload
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN as SignOptions['expiresIn']});
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('生成JWT token失败:', error);
|
|
|
|
|
+ throw new Error('生成token失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 验证 JWT token
|
|
|
|
|
+ * @param token JWT token
|
|
|
|
|
+ * @returns 验证后的 payload
|
|
|
|
|
+ */
|
|
|
|
|
+ static verifyToken(token: string): JWTPayload {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return jwt.verify(token, JWT_SECRET) as JWTPayload;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('验证JWT token失败:', error);
|
|
|
|
|
+ throw new Error('无效的token');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解码 JWT token(不验证签名)
|
|
|
|
|
+ * @param token JWT token
|
|
|
|
|
+ * @returns 解码后的 payload
|
|
|
|
|
+ */
|
|
|
|
|
+ static decodeToken(token: string): JWTPayload | null {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return jwt.decode(token) as JWTPayload;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('解码JWT token失败:', error);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取 token 的剩余有效期(秒)
|
|
|
|
|
+ * @param token JWT token
|
|
|
|
|
+ * @returns 剩余有效期(秒),如果 token 无效则返回 0
|
|
|
|
|
+ */
|
|
|
|
|
+ static getTokenRemainingTime(token: string): number {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload & { exp: number };
|
|
|
|
|
+ const currentTime = Math.floor(Date.now() / 1000);
|
|
|
|
|
+ return Math.max(0, decoded.exp - currentTime);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|