import { jwtVerify, SignJWT } from "jose" /** * @description JSON Web Token(JWT)中当前对外承诺的业务载荷。 */ export interface JsonWebTokenPayload { userId: string } // JWT 的签发和校验都要求把共享密钥表示为字节序列。 // 这个辅助函数明确说明:传入的密钥字符串会按 UTF-8 编码后再交给签名与验签逻辑。 const internalJsonWebTokenTextToBytes = (text: string): Uint8Array => { return new TextEncoder().encode(text) } // JWT 的过期时间既可以用持续时间字符串表示,也可以用相对秒数表示。 // 这个辅助函数统一把 number 解释为“若干秒后过期”,避免把相对时长误当成绝对 Unix 时间。 const internalNormalizeJsonWebTokenExpiresIn = (expiresIn: string | number): string | number => { return typeof expiresIn === "number" ? `${expiresIn}s` : expiresIn } /** * @description 创建 JsonWebToken 实例时需要提供的配置。 * * `secret` 表示对称签名密钥,`expiresIn` 表示 token 的相对过期时间。 */ export interface JsonWebTokenOptions { secret: string expiresIn: string | number } /** * @description 提供围绕 JSON Web Token(JWT)进行签发与解析的基础能力。 * * 当前实现使用对称密钥的 HS256 算法,并把 `userId` 作为默认业务载荷写入 token。 * `expiresIn` 表示相对时长:若传入字符串,可使用 `1h`、`30m` 之类的持续时间文本;若传入数字,则表示多少秒后过期。 * * @example * ``` * const jwt = new JsonWebToken({ secret: "secret", expiresIn: "1h" }) * const token = await jwt.signToken("user-1") * // Expect: 3 * const example1 = token.split(".").length * // Expect: "user-1" * const example2 = await jwt.parseToken(token) * ``` */ export class JsonWebToken { private options: JsonWebTokenOptions constructor(options: JsonWebTokenOptions) { this.options = options } /** * @description 为给定用户标识签发 JWT 字符串。 */ async signToken(userId: string): Promise { const { secret, expiresIn } = this.options return await new SignJWT({ userId } satisfies JsonWebTokenPayload) .setProtectedHeader({ alg: "HS256", typ: "JWT" }) .setIssuedAt() .setExpirationTime(internalNormalizeJsonWebTokenExpiresIn(expiresIn)) .sign(internalJsonWebTokenTextToBytes(secret)) } /** * @description 解析原始 JWT 字符串,并返回其中的用户标识。 */ async parseToken(token: string | undefined): Promise { if (token === undefined) { return undefined } try { const { secret } = this.options const { payload } = await jwtVerify( token, internalJsonWebTokenTextToBytes(secret), { algorithms: ["HS256"], }, ) if (typeof payload.userId !== "string") { return undefined } return payload.userId } catch { return undefined } } }