79 lines
2.9 KiB
TypeScript
79 lines
2.9 KiB
TypeScript
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { SERVER_CONFIG, type ServerConfig } from '../config/server-config';
|
|
|
|
/** 最小身份载荷;authVersion 用于与账户当前版本比较,不在 token 中固化角色。 */
|
|
export interface AccessTokenClaims {
|
|
sub: string;
|
|
authVersion: number;
|
|
iat: number;
|
|
exp: number;
|
|
}
|
|
|
|
@Injectable()
|
|
/** 统一签发和验证访问凭证,固定算法、签发方、受众和有效期。 */
|
|
export class JwtTokenService {
|
|
constructor(
|
|
private readonly jwt: JwtService,
|
|
@Inject(SERVER_CONFIG) private readonly config: ServerConfig,
|
|
) {}
|
|
|
|
/** 仅供可信登录业务调用;调用方须先验证微信身份并取得真实账户。 */
|
|
async signAccessToken(
|
|
userId: string,
|
|
authVersion: number,
|
|
): Promise<string> {
|
|
if (
|
|
!userId ||
|
|
userId.length > 128 ||
|
|
!Number.isSafeInteger(authVersion) ||
|
|
authVersion < 0
|
|
) {
|
|
throw new Error('Invalid access token identity');
|
|
}
|
|
return this.jwt.signAsync(
|
|
{ sub: userId, authVersion },
|
|
{
|
|
secret: this.config.jwtSecret,
|
|
algorithm: 'HS256',
|
|
issuer: this.config.jwtIssuer,
|
|
audience: this.config.jwtAudience,
|
|
expiresIn: this.config.jwtExpiresInSeconds,
|
|
},
|
|
);
|
|
}
|
|
|
|
/** 验签及校验载荷;当前账户状态与权限仍由全局守卫继续核对。 */
|
|
async verifyAccessToken(token: string): Promise<AccessTokenClaims> {
|
|
try {
|
|
const claims = await this.jwt.verifyAsync<AccessTokenClaims>(
|
|
token,
|
|
{
|
|
secret: this.config.jwtSecret,
|
|
// 不根据 token 头自由选择算法,并限制凭证用途和签发方。
|
|
algorithms: ['HS256'],
|
|
issuer: this.config.jwtIssuer,
|
|
audience: this.config.jwtAudience,
|
|
},
|
|
);
|
|
// 签名正确仍需检查字段类型及必填时间,拒绝无期限或畸形身份载荷。
|
|
if (
|
|
typeof claims.sub !== 'string' ||
|
|
!claims.sub ||
|
|
claims.sub.length > 128 ||
|
|
!Number.isSafeInteger(claims.authVersion) ||
|
|
claims.authVersion < 0 ||
|
|
!Number.isSafeInteger(claims.iat) ||
|
|
!Number.isSafeInteger(claims.exp) ||
|
|
claims.exp <= claims.iat
|
|
) {
|
|
throw new Error('Invalid access token claims');
|
|
}
|
|
return claims;
|
|
} catch {
|
|
// 过期、伪造或载荷错误统一返回 401,不向客户端暴露验签细节。
|
|
throw new UnauthorizedException();
|
|
}
|
|
}
|
|
}
|