Files
services/utils/jwt.js
T
2026-06-03 14:15:55 +08:00

113 lines
3.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* JWTJSON 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
}