/** * JWT(JSON Web Token)工具函数 * @module services/utils/jwt */ const crypto = require('crypto') const SECRET = process.env.JWT_SECRET || (() => { if (process.env.NODE_ENV === 'production') { throw new Error('JWT_SECRET environment variable is required in production') } return 'dev-only-jwt-secret-change-in-production-' + crypto.randomBytes(16).toString('hex') })() const ACCESS_TTL = parseInt(process.env.JWT_ACCESS_TTL || 7 * 24 * 3600) const REFRESH_TTL = parseInt(process.env.JWT_REFRESH_TTL || 30 * 24 * 3600) const ISSUER = 'miniprogram' const ALG = 'HS256' /** * 编码为 URL 安全的 base64 * @param {string|Buffer} input - 输入 * @returns {string} 编码后的字符串 */ function base64url(input) { const buf = Buffer.isBuffer(input) ? input : Buffer.from(input) return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } /** * 解码 URL 安全的 base64 * @param {string} input - 编码后的字符串 * @returns {Buffer} 解码后的 Buffer */ function base64urlDecode(input) { const pad = 4 - (input.length % 4) const normalized = input.replace(/-/g, '+').replace(/_/g, '/') + (pad < 4 ? '='.repeat(pad) : '') return Buffer.from(normalized, 'base64') } /** * 签名 JWT * @param {Object} payload - JWT 载荷 * @param {number} [ttlSeconds=ACCESS_TTL] - TTL(秒) * @returns {string} JWT token */ function sign(payload, ttlSeconds = ACCESS_TTL) { const now = Math.floor(Date.now() / 1000) const header = { alg: ALG, typ: 'JWT' } const body = { ...payload, iat: now, exp: now + ttlSeconds, iss: ISSUER } const head = base64url(JSON.stringify(header)) const data = base64url(JSON.stringify(body)) const sig = crypto.createHmac('sha256', SECRET).update(`${head}.${data}`).digest() return `${head}.${data}.${base64url(sig)}` } /** * 验证 JWT * @param {string} token - JWT token * @returns {Object|null} 验证后的载荷或 null */ function verify(token) { if (!token || typeof token !== 'string') return null const parts = token.split('.') if (parts.length !== 3) return null const [head, data, sig] = parts const expected = crypto.createHmac('sha256', SECRET).update(`${head}.${data}`).digest() const provided = base64urlDecode(sig) if (expected.length !== provided.length || !crypto.timingSafeEqual(expected, provided)) { return null } try { const payload = JSON.parse(base64urlDecode(data).toString('utf8')) if (payload.exp && Math.floor(Date.now() / 1000) >= payload.exp) return null if (payload.iss && payload.iss !== ISSUER) return null return payload } catch { return null } } /** * 签名访问令牌 * @param {Object} user - 用户对象 * @returns {string} 访问令牌 */ function signAccess(user) { return sign({ sub: user.id, role: user.role, type: 'access' }, ACCESS_TTL) } /** * 签名刷新令牌 * @param {Object} user - 用户对象 * @returns {string} 刷新令牌 */ function signRefresh(user) { return sign({ sub: user.id, type: 'refresh' }, REFRESH_TTL) } module.exports = { sign, verify, signAccess, signRefresh, ACCESS_TTL, REFRESH_TTL }