113 lines
3.1 KiB
JavaScript
113 lines
3.1 KiB
JavaScript
/**
|
||
* 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
|
||
}
|