服务端5月29日 00:24
JWT 如何满足 GDPR 等合规性要求?JWT 与合规的核心矛盾:JWT 不可变(签发后不能修改内容),但 GDPR 要求用户可以删除数据(被遗忘权)。解决思路有三个:一是 payload 只放最小必要信息(用户 ID + 角色),不存个人数据;二是实现 token 黑名单或短期 TTL,用户请求删除时立即作废所有 token;三是审计日志记录所有 token 签发和使用,满足可追溯要求。对 HIPAA/PCI DSS 同理——JWT 中绝不放 PHI(受保护健康信息)和支付卡号,只存引用 ID。## 追问**GDPR 被遗忘权与 JWT 不可变性怎么协调?**JWT 内容无法修改,但可以通过作废来"遗忘":将用户所有 token 加入黑名单、使 refresh token 失效、删除服务端用户数据。过期 token 中的个人信息残留属于技术限制,GDPR 在合理范围内允许。**HIPAA 对 JWT 有什么特殊要求?**不能在 payload 中存储任何 PHI(诊断、药物、病历等),只放用户 ID 和 scope 声明。所有 PHI 访问必须记录审计日志(谁在什么时间访问了什么数据),JWT 中的 sub 和 scope 用于日志关联。**密钥轮换与合规审计有什么关系?**合规框架(SOC 2/ISO 27001)要求定期轮换密钥。实现方式:维护多个密钥版本,新 token 用新密钥签发,验证时按 kid 字段选择对应密钥。轮换期间旧 token 逐步过期,无需强制用户重新登录。**JWT 的审计日志应该记录什么?**至少包含:用户 ID、操作类型(签发/验证/撤销)、时间戳、来源 IP、User-Agent、token 前缀(不记录完整 token)。日志存储到不可篡改的系统(如 append-only 存储),满足 SOC 2 审计要求。## 写段代码```javascript// GDPR 合规:数据删除 + token 作废async function deleteUserData(userId) { // 作废所有 token await redis.set(`deleted:${userId}`, '1', 'EX', maxTokenTTL); // 删除用户数据 await db.delete('users', { id: userId }); // 审计日志 await auditLog('DATA_DELETED', { userId, reason: 'user_request' });}// 验证时检查用户是否已删除if (await redis.get(`deleted:${decoded.sub}`)) { throw new Error('user data deleted');}```标签
JWT
JSON Web Token (JWT) 是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。由于信息是经过数字签名的,所以可以验证其完整性。另外,可以对 JWT 进行加密以保护数据的机密性。 JWT 主要用于身份验证和信息交换,特别适合用于分布式站点的单点登录(SSO)场景。

服务端5月29日 00:23
JWT 在生产环境有哪些常见问题?怎么解决?JWT 生产环境最核心的问题是三个:token 无法主动撤销(签发后到过期前一直有效)、payload 明文可读(Base64 非加密,敏感信息暴露)、token 体积大影响性能(每次请求都要传 1-2KB)。对应解法:撤销用黑名单(Redis 存已注销 token,TTL 等于剩余过期时间)或短期 access token + refresh token 轮换;敏感数据只放引用 ID,详情查库;体积优化用短字段名和 ES256 算法(比 RS256 小约 50%)。## 追问**token 泄露了怎么办?**XSS 可窃取 localStorage 中的 token,CSRF 可利用 Cookie 中的 token。防护:优先用 HttpOnly + SameSite Cookie 存储,配合 CSRF Token;access token 设短过期时间(15 分钟),泄露窗口小;检测到异常时强制刷新 refresh token 使旧 access token 自然失效。**Refresh Token 轮换是什么?**每次用 refresh token 换新 access token 时,同时签发新 refresh token 并作废旧的那个。如果检测到旧 refresh token 被复用,说明泄露,立即撤销该用户所有 refresh token,强制重新登录。**JWT 验签每次都要算 HMAC,性能怎么优化?**对称算法验签很快(微秒级),一般不是瓶颈。非对称算法(RS256)较慢,可切换到 ES256。大规模场景可在 API 网关层做验签,避免业务服务重复计算。**多设备登录怎么管理?**每个设备签发独立 refresh token,服务端记录设备信息。踢下线时删除对应 refresh token,该设备 access token 过期后无法续期,自然登出。## 写段代码```javascript// 黑名单撤销 + refresh token 轮换async function logout(token) { const { exp } = jwt.decode(token); await redis.setex(`bl:${token}`, exp - now(), '1');}async function refresh(oldRt) { if (await redis.get(`rt:${oldRt}`)) { await redis.del(`rt:${oldRt}`); const newRt = crypto.randomBytes(40).toString('hex'); await redis.setex(`rt:${newRt}`, 7*86400, userId); return { accessToken, refreshToken: newRt }; } throw new Error('invalid refresh token');}```服务端5月29日 00:23
JWT 有哪些安全风险?如何防护?JWT 的 payload 仅做 Base64 编码而非加密,任何持有 token 的人都能解码查看内容;存储在 localStorage 中的 token 易遭 XSS 窃取;攻击者可将算法篡改为 none 绕过签名验证。核心防护策略:使用短期 Access Token + 长期 Refresh Token 机制;token 存入 HttpOnly Cookie 并设置 SameSite 防 CSRF;服务端固定签名算法拒绝 alg:none;敏感数据使用 JWE 加密传输。
## 追问
**JWT 无法主动撤销怎么办?**
设置 15-30 分钟短过期时间,配合 Redis 黑名单或 token 版本号机制,关键操作强制二次验证。
**HttpOnly Cookie 和 localStorage 存 token 各有什么利弊?**
HttpOnly 防 XSS 但需额外防 CSRF(SameSite 属性);localStorage 不防 XSS 但天然免疫 CSRF,取舍取决于项目威胁模型。
**RS256 和 HS256 该选哪个?**
RS256 非对称签名,公钥验签私钥签发,适合分布式系统;HS256 对称签名密钥需共享,密钥泄露风险更高,优先选 RS256。
**如何防止 token 重放攻击?**
在 payload 中加 jti(唯一 ID)声明,服务端记录已用 jti 拒绝重复请求,配合短过期时间降低窗口期。
## 写段代码
```javascript
// 短期 Access Token + Refresh Token 签发
const accessToken = jwt.sign(
{ sub: userId, jti: uuid() },
RSA_PRIVATE_KEY,
{ algorithm: 'RS256', expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
```服务端5月29日 00:23
JWT 和 OAuth2.0 是什么关系?有什么区别?它们不在同一层面:JWT 是令牌格式标准(RFC 7519),定义 token 的结构和编码方式;OAuth2.0 是授权框架(RFC 6749),定义授权流程和角色关系。一句话:OAuth2.0 规定了"怎么授权",JWT 规定了"令牌长什么样"。两者可以组合——OAuth2.0 的 Access Token 可以采用 JWT 格式,但不是必须的。OAuth2.0 也可以用不透明的随机字符串做 token。## 追问**OAuth2.0 不用 JWT 行不行?**完全可以。OAuth2.0 对 token 格式没有约束,可以用随机字符串(opaque token),验证时需查库。用 JWT 的好处是资源服务器可以本地验签、无需回调授权服务器,降低延迟和耦合。**OAuth2.0 的四种授权模式分别适合什么场景?**授权码模式(Authorization Code)适合有后端的 Web 应用,最安全;客户端凭证模式(Client Credentials)适合服务间调用;资源所有者密码模式已不推荐;隐式模式(Implicit)因安全风险已被 OAuth2.1 移除,改用 PKCE 增强的授权码模式。**既然 JWT 可以独立做认证,为什么还需要 OAuth2.0?**JWT 解决的是单系统的 token 格式问题。OAuth2.0 解决的是第三方委托授权问题——"让用户授权 A 应用访问 B 服务的资源,且不暴露密码"。这是 JWT 独自做不到的。**OAuth2.0 用 JWT 做 token 时,刷新令牌也用 JWT 吗?**一般不用。Refresh Token 需要可撤销,通常用 opaque token 存在服务端。JWT 格式的 refresh token 撤销困难,违背 refresh token 的设计初衷。## 写段代码```javascript// OAuth2.0 授权码流程中签发 JWT access tokenconst accessToken = jwt.sign( { sub: userId, scope: 'read write', client_id: appId }, PRIVATE_KEY, { algorithm: 'RS256', expiresIn: '1h' });const refreshToken = crypto.randomBytes(32).toString('hex');await redis.set(`rt:${refreshToken}`, userId, 'EX', 7*86400);```服务端5月29日 00:23
什么是 JWT?它由哪三部分组成?JWT(JSON Web Token,RFC 7519)是一种紧凑的自包含令牌格式,用于在各方之间安全传输信息。它由三部分组成,用点号分隔:**Header.Payload.Signature**。Header 指定令牌类型和签名算法(如 HS256);Payload 承载声明(Claims),包括注册声明(iss/exp/sub)、公共声明和私有声明;Signature 用 Header 指定的算法对前两部分签名,保证完整性。注意 Payload 只是 Base64 编码而非加密,不能放敏感数据。## 追问**JWT 的 Signature 是怎么生成的?**将 Base64Url 编码的 Header 和 Payload 用点号拼接,再用 Header 中声明的算法和密钥进行签名。公式:`HMACSHA256(base64(header) + '.' + base64(payload), secret)`。接收方用同一密钥重新计算并比对,即可判断 token 是否被篡改。**注册声明中的 exp、iat、nbf 各是什么?**exp(Expiration Time)过期时间,iat(Issued At)签发时间,nbf(Not Before)生效时间。服务端验证时会检查这些字段,过期的 token 直接拒绝。**HS256 和 RS256 有什么区别?**HS256 是对称加密,签发和验证用同一个密钥,适合单服务场景;RS256 是非对称加密,私钥签发、公钥验证,适合微服务架构——各服务只需公钥即可验签,无需暴露私钥。**JWT 和 JWE 有什么关系?**JWT 只保证完整性(防篡改),不保证机密性。JWE(JSON Web Encryption)在 JWT 基础上加密 payload,实现机密性。需要保护敏感数据时用 JWE,一般场景 JWT 足够。## 写段代码```javascript// 签发与验证const token = jwt.sign( { sub: 'u123', role: 'admin', iat: Math.floor(Date.now()/1000) }, SECRET, { expiresIn: '1h', algorithm: 'HS256' });const decoded = jwt.verify(token, SECRET);```服务端5月29日 00:23
JWT 和 Session 认证有什么区别?各适合什么场景?核心区别在于状态存储位置:JWT 是无状态的,用户信息编码在 token 中由客户端持有;Session 是有状态的,用户信息存在服务端,客户端只拿一个 Session ID。这意味着 JWT 天然支持水平扩展(任何节点都能验签),但一旦签发就无法主动撤销;Session 可以随时销毁,但多节点需要共享存储(如 Redis)。选择依据:分布式系统、移动端、跨域 API 选 JWT;需要即时吊销权限、传统 Web 应用选 Session。## 追问**JWT 无法主动撤销,用户登出怎么办?**常用三种方案:黑名单(Redis 存已撤销 token,过期自动清除)、短期 access token + refresh token 轮换、token 版本号(用户表加 version 字段,签发时写入,验证时比对)。**JWT 存 localStorage 还是 Cookie?**各有利弊。localStorage 容易被 XSS 窃取;HttpOnly Cookie 防 XSS 但需额外防 CSRF。生产推荐 HttpOnly + SameSite Cookie + CSRF Token 双重防护。**Session 在微服务下怎么共享?**用 Redis 集群做集中式 Session 存储,或用 Spring Session 等框架透明化处理。本质是把有状态存储从本地内存移到分布式缓存。**JWT 的 payload 能放敏感信息吗?**不能。Payload 只是 Base64 编码,不是加密,任何人都能解码。敏感数据只存引用 ID,详情查库获取。## 写段代码```javascript// 短期 access token + 长期 refresh tokenconst accessToken = jwt.sign( { sub: userId, role }, SECRET, { expiresIn: '15m' });const refreshToken = crypto.randomBytes(40).toString('hex');await redis.setex(`refresh:${userId}`, 7*86400, refreshToken);```服务端5月27日 23:12
如何对 JWT 进行测试JWT 测试需要从三个层面入手:单元测试验证签名和解析逻辑,集成测试验证完整认证流程,安全测试验证防护常见攻击。下面逐层展开。
## 单元测试:Token 生成与验证
单元测试关注 JWT 库本身的行为是否正确。核心测试点有三个:生成出的 token 结构是否合法、payload 是否正确写入、过期时间是否生效。
### Token 生成测试
```javascript
const jwt = require('jsonwebtoken');
const { expect } = require('chai');
describe('JWT Token Generation', () => {
const SECRET = 'test-secret';
const payload = { userId: '123', role: 'user' };
it('should generate a valid three-part token', () => {
const token = jwt.sign(payload, SECRET);
expect(token.split('.')).to.have.lengthOf(3);
});
it('should encode payload correctly', () => {
const decoded = jwt.decode(jwt.sign(payload, SECRET));
expect(decoded.userId).to.equal('123');
expect(decoded.role).to.equal('user');
});
it('should set expiration', () => {
const decoded = jwt.decode(jwt.sign(payload, SECRET, { expiresIn: '1h' }));
expect(decoded.exp).to.be.greaterThan(Math.floor(Date.now() / 1000));
});
});
```
### Token 验证测试
验证环节容易出问题,重点测三类异常:签名不匹配、token 过期、算法不符。
```javascript
describe('JWT Token Verification', () => {
const SECRET = 'test-secret';
it('should verify a valid token', () => {
const token = jwt.sign({ userId: '123' }, SECRET);
const decoded = jwt.verify(token, SECRET);
expect(decoded.userId).to.equal('123');
});
it('should reject expired token', () => {
const expired = jwt.sign({ userId: '123' }, SECRET, { expiresIn: '-1s' });
expect(() => jwt.verify(expired, SECRET)).to.throw('jwt expired');
});
it('should reject wrong secret', () => {
const token = jwt.sign({ userId: '123' }, SECRET);
expect(() => jwt.verify(token, 'wrong')).to.throw('invalid signature');
});
it('should reject wrong algorithm', () => {
const token = jwt.sign({ userId: '123' }, SECRET, { algorithm: 'HS256' });
expect(() => jwt.verify(token, SECRET, { algorithms: ['RS256'] }))
.to.throw('invalid algorithm');
});
});
```
注意 `algorithms` 白名单是必须的——后面安全测试会解释原因。
## 集成测试:认证流程
单元测试只覆盖了库函数,集成测试验证 token 在 HTTP 请求中的真实表现。关键场景:登录拿 token → 带 token 访问受保护资源 → token 无效/过期时被拒绝。
```javascript
const request = require('supertest');
const app = require('../app');
describe('Auth Flow', () => {
let token;
it('should return token on login', async () => {
const res = await request(app)
.post('/auth/login')
.send({ username: 'testuser', password: 'password123' });
expect(res.status).to.equal(200);
expect(res.body.token).to.exist;
token = res.body.token;
});
it('should access protected route with valid token', async () => {
const res = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${token}`);
expect(res.status).to.equal(200);
});
it('should reject request without token', async () => {
const res = await request(app)
.get('/api/protected');
expect(res.status).to.equal(401);
});
it('should reject expired token', async () => {
const expired = jwt.sign({ userId: '123' }, process.env.JWT_SECRET, { expiresIn: '-1s' });
const res = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${expired}`);
expect(res.status).to.equal(401);
});
});
```
如果系统使用 refresh token,还需要单独测试 refresh 端点:用合法 refresh token 换新 access token,用无效 refresh token 被拒绝。
## 安全测试:防护常见攻击
JWT 最常见的安全问题有两个:算法混淆攻击和 token 篡改。
### 算法混淆攻击
攻击者把 header 中的 `alg` 改为 `none`,绕过签名验证。防御方式是在 verify 时显式指定 `algorithms` 白名单:
```javascript
describe('Security - Algorithm Confusion', () => {
it('should reject none algorithm', () => {
const noneToken = jwt.sign({ userId: 'admin' }, '', { algorithm: 'none' });
expect(() => jwt.verify(noneToken, SECRET, { algorithms: ['HS256'] }))
.to.throw();
});
});
```
这就是为什么前面单元测试中强调 `algorithms` 白名单——不写白名单的 verify 调用就是安全漏洞。
### Token 篡改
攻击者修改 payload 后重新拼接 token,但无法生成合法签名,所以 verify 必须能检测到签名不匹配:
```javascript
describe('Security - Token Tampering', () => {
it('should reject tampered payload', () => {
const token = jwt.sign({ userId: '123' }, SECRET);
const parts = token.split('.');
parts[1] = Buffer.from(JSON.stringify({ userId: 'admin' })).toString('base64');
const tampered = parts.join('.');
expect(() => jwt.verify(tampered, SECRET)).to.throw('invalid signature');
});
});
```
## 边界情况补充
除了上述三层,实际项目中还需关注:
- **时钟偏移**:分布式系统中各节点时钟不同步,verify 时设置 `clockTolerance`(如 30 秒)避免误判过期。
- **密钥轮换**:旧密钥签发的 token 在新密钥上线后仍需能验证,需支持多密钥验证逻辑。
- **并发刷新**:多个请求同时触发 token 刷新,需要幂等处理避免竞态条件。
## 追问
- JWT 的 `alg: none` 攻击原理是什么?如何防御?
- RS256 和 HS256 在验证时有什么安全差异?(提示:RS256 公钥验证 vs HS256 共享密钥,HS256 存在密钥泄露风险)
- 如何在不停机的情况下轮换 JWT 签名密钥?服务端5月27日 23:11
JWT 的签名算法有哪些,如何选择?## 签名算法分三类
JWT 签名算法分为三类:**HMAC 对称签名**(HS256/HS384/HS512)、**RSA 非对称签名**(RS256/RS384/RS512/PS256/PS384/PS512)、**ECDSA 椭圆曲线签名**(ES256/ES384/ES512)。
## 如何选择
**默认选 RS256**。它是非对称算法,私钥签名、公钥验证,公钥可安全分发给任何需要验证 token 的服务,适合分布式架构和微服务场景。如果只是单体应用、内部系统通信,HS256 足够且性能更好。如果对签名体积或性能敏感(移动端、IoT),选 ES256——安全性与 RS256 相当,签名体积小约 50%。
| 场景 | 推荐算法 | 原因 |
|------|----------|------|
| 单体应用 / 内部通信 | HS256 | 对称密钥够用,性能最佳 |
| 分布式 / 微服务 / 公开 API | RS256 | 公钥可分发,私钥不泄露 |
| 移动端 / IoT / 高性能 | ES256 | 签名小、速度快、安全性高 |
| 金融 / 医疗等高安全 | PS256 | RSA-PSS 提供更强安全证明 |
## 代码示例
```javascript
// HS256 — 对称密钥
const token = jwt.sign(payload, 'secret-key', { algorithm: 'HS256' });
const decoded = jwt.verify(token, 'secret-key', { algorithms: ['HS256'] });
// RS256 — 非对称密钥
const privateKey = fs.readFileSync('private.key');
const publicKey = fs.readFileSync('public.key');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// ES256 — 椭圆曲线
const ecPrivate = fs.readFileSync('ec-private.key');
const ecPublic = fs.readFileSync('ec-public.key');
const token = jwt.sign(payload, ecPrivate, { algorithm: 'ES256' });
const decoded = jwt.verify(token, ecPublic, { algorithms: ['ES256'] });
```
## 密钥生成
```bash
# RSA 密钥对
openssl genrsa -out private.key 2048
openssl rsa -in private.key -pubout -out public.key
# ECDSA 密钥对
openssl ecparam -name prime256v1 -genkey -noout -out ec-private.key
openssl ec -in ec-private.key -pubout -out ec-public.key
```
## 安全要点
1. **禁用 none 算法**:永远不要接受 alg: none 的 token,攻击者可伪造任意内容。
2. **验证时显式指定算法**:防止算法混淆攻击(攻击者将 RS256 改为 HS256,用公钥当对称密钥验证)。
3. **密钥强度**:RSA 至少 2048 位,HMAC 至少 256 位,ECDSA 至少 P-256。
4. **定期轮换密钥**:每 6-12 个月轮换,支持多密钥验证实现平滑过渡。
## 追问:HS256 和 RS256 的核心区别是什么?
HS256 是对称算法,签名和验证用同一密钥,任何能验证 token 的人也能伪造它;RS256 是非对称算法,私钥签名、公钥验证,验证方无法伪造。因此在微服务架构中,RS256 是唯一合理选择——你不会想把签名密钥交给每一个验证服务。
## 追问:什么是算法混淆攻击?
攻击者拿到 RS256 的公钥后,将 token header 的 alg 改为 HS256,再用该公钥作为 HMAC 密钥签名。如果服务端验证时没有显式指定算法,就会用公钥当 HMAC 密钥去验证,结果验证通过——攻击者成功伪造 token。防御方法:验证时必须通过 algorithms 参数显式指定允许的算法。服务端5月27日 23:10
JWT 在微服务架构中如何使用## 核心思路
微服务中使用 JWT 的关键是:**由认证服务签发令牌,各服务通过公钥本地验证,避免集中式校验带来的性能瓶颈和单点故障**。
具体架构:客户端登录 → 认证服务签发 JWT → 客户端携带 JWT 请求 API Gateway → Gateway 验证后转发 → 下游服务通过 JWK 端点获取公钥自行验证,无需回调认证服务。
## 为什么用非对称加密(RS256)而不是对称加密(HS256)
HS256 要求所有验证方共享同一个密钥,微服务场景下密钥分发和泄露风险高。RS256 签名用私钥(仅认证服务持有),验证用公钥(可公开分发),任何服务只需拿到公钥就能验证,无需共享敏感信息。
## 认证服务签发 JWT
```javascript
const jwt = require('jsonwebtoken');
const { generateKeyPairSync } = require('crypto');
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// 签发令牌
app.post('/auth/login', (req, res) => {
const user = validateUser(req.body.username, req.body.password);
const token = jwt.sign(
{ userId: user.id, role: user.role, permissions: user.permissions },
privateKey,
{
algorithm: 'RS256',
keyid: 'key-20260527',
expiresIn: '15m',
issuer: 'auth-service',
audience: 'my-platform'
}
);
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
privateKey,
{ algorithm: 'RS256', expiresIn: '7d' }
);
res.json({ token, refreshToken });
});
// JWK Set 端点,供其他服务获取公钥
app.get('/.well-known/jwks.json', (req, res) => {
const jwk = jose.JWK.asKey(publicKey, { kid: 'key-20260527' });
res.json({ keys: [jwk.toJWK()] });
});
```
访问令牌有效期设短(15 分钟),配合 Refresh Token 续期,降低令牌泄露风险。
## API Gateway 验证与转发
```javascript
app.use(async (req, res, next) => {
const token = req.headers['authorization']?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'No token' });
try {
const publicKey = await getPublicKey(); // 从 JWK 端点获取,带缓存
req.user = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'auth-service',
audience: 'my-platform'
});
// 将用户信息注入请求头,传递给下游服务
req.headers['x-user-id'] = req.user.userId;
req.headers['x-user-role'] = req.user.role;
next();
} catch (e) {
res.status(401).json({ error: 'Invalid token' });
}
});
```
## 下游服务验证与 Token 传播
下游服务有两种选择:
- **Gateway 已验证,下游信任 Gateway**:只检查 `x-user-id` 等请求头,适用于内部可信网络
- **下游自行验证**:从 JWK 端点获取公钥,本地校验,适用于跨团队或零信任场景
服务间调用时,用拦截器自动传播 Token:
```javascript
const axios = require('axios');
const serviceClient = axios.create({ timeout: 5000 });
serviceClient.interceptors.request.use(config => {
const token = getCurrentToken(); // 从请求上下文获取
if (token) config.headers['Authorization'] = `Bearer ${token}`;
return config;
});
```
## 密钥轮换
线上必须支持密钥轮换。流程:生成新密钥对 → 新令牌用新 kid 签发 → 旧令牌仍可用旧公钥验证 → 旧密钥过期后从 JWK Set 移除。验证方根据 JWT Header 中的 `kid` 字段匹配对应公钥,过渡期内 JWK Set 同时包含新旧公钥。
## Token 撤销的处理
JWT 本身无状态,签出后无法直接撤销。常用方案:
- **短有效期 + Refresh Token**:最主流,泄露窗口小
- **黑名单**:Redis 存已撤销的 Token ID,每次验证时查询,牺牲部分无状态优势
- **Gateway 层拦截**:只对敏感操作生效,如修改密码后立即失效旧 Token
## 追问
**Q: JWT 和 OAuth2 是什么关系?**
JWT 是令牌格式,OAuth2 是授权框架。OAuth2 可以用 JWT 作为 Access Token 的载体,也可以用不透明令牌。两者不在同一层面。
**Q: 微服务之间用 JWT 还是 mTLS?**
服务间通信(非用户请求)更推荐 mTLS(双向 TLS),双方通过证书互验身份,不依赖令牌传播。JWT 适合用户到服务的场景,mTLS 适合服务到服务。
**Q: JWK 端点挂了怎么办?**
验证方必须缓存公钥(TTL 5-10 分钟),JWK 端点短暂不可用时用缓存继续验证,不影响线上服务。服务端5月27日 23:09
如何优化 JWT 的性能?核心优化方向有三个:**减小 Token 体积**、**加速签名验证**、**避免重复解析**。
## 减小 Token 体积
JWT 每次请求都携带,体积直接影响带宽和解析耗时。
```javascript
// 只存必要字段,用短键名
const token = jwt.sign({ uid: "123", rol: "admin" }, SECRET);
// 而非 { userId, userName, userRole, userPermissions }
```
- Payload 只放用户 ID 和角色,其他信息从缓存取
- 避免存数组、嵌套对象,每个字段都计入 Base64 编码后的体积
- 压缩(如 pako deflate)仅在 Payload > 1KB 时有意义,小 Payload 压缩后反而更大
## 加速签名验证
算法选择对性能影响最大:
| 算法 | 签名速度 | 验证速度 | Token 大小 | 适用场景 |
|------|---------|---------|-----------|----------|
| HS256 | 最快 | 最快 | ~32B | 单体应用,密钥可安全共享 |
| RS256 | 慢 | 中 | ~256B | 微服务,公钥可公开分发 |
| ES256 | 中 | 快 | ~64B | 移动端/IoT,兼顾速度与体积 |
- HS256 比 RS256 快 5-10 倍,但密钥泄露风险更高
- ES256 是分布式场景的平衡选择:签名比 RS256 小 4 倍,验证更快
- 如果只用 HS256,可手写 HMAC 验证替代 jsonwebtoken 库,减少框架开销
## 避免重复解析
每次请求都 `jwt.verify()` 是浪费,可用缓存跳过:
```javascript
// 验证结果缓存(注意安全风险)
const NodeCache = require("node-cache");
const cache = new NodeCache({ stdTTL: 300 }); // 缓存 5 分钟
function verifyCached(token) {
const key = `tk:${crypto.createHash("sha256").update(token).digest("hex")}`;
const hit = cache.get(key);
if (hit) return hit;
const decoded = jwt.verify(token, SECRET);
cache.set(key, decoded);
return decoded;
}
```
**缓存的安全代价**:Token 被撤销(如用户登出)后,缓存期内仍可访问。解法:
- 缓存 TTL 设短(< Token 有效期的 1/10)
- 登出时主动清除对应用户的缓存 key
- 高安全场景不缓存,用算法优化替代
多实例部署时用 Redis 替代本地缓存,公钥(JWKS)也做缓存避免每次远程拉取。
## 异步验证
```javascript
// 同步验证阻塞事件循环
const decoded = jwt.verify(token, SECRET); // 阻塞
// 异步验证释放事件循环
const decoded = await new Promise((resolve, reject) => {
jwt.verify(token, SECRET, (err, d) => err ? reject(err) : resolve(d));
});
```
高并发下同步 crypto 操作会阻塞 Node.js 事件循环,异步 + worker_threads 才是正解。
## 追问
**Q: 缓存验证结果后,Token 撤销怎么办?**
上面已提到,核心思路是短 TTL + 主动失效。也可以用 Token 版本号:用户登出时递增版本,缓存中版本不匹配则重新验证。
**Q: JWT 无状态的意义在哪,加了缓存不就有状态了?**
缓存是性能优化手段,不是架构依赖。缓存挂掉系统仍可用(降级为全量验证),这和 Session 必须依赖 Redis 有本质区别。
**Q: 什么时候该用 JWT,什么时候该用 Session?**
多服务/跨域/移动端选 JWT;单体内聚应用选 Session 更简单。JWT 的性能代价换来的是水平扩展能力,不是所有场景都需要。
服务端5月27日 23:08
如何在 Node.js 中实现 JWT 认证?JWT(JSON Web Token)是无状态认证方案,服务端不存储会话,客户端每次请求携带 Token 证明身份。Node.js 中使用 jsonwebtoken 库即可完成签发与验证。
核心流程:用户登录 → 服务端签发 JWT → 客户端存储并在请求头携带 → 服务端验证签名和过期时间。
## 签发 Token
```javascript
const jwt = require("jsonwebtoken");
const SECRET = process.env.JWT_SECRET;
function signToken(payload) {
return jwt.sign(payload, SECRET, {
expiresIn: "1h",
issuer: "my-app",
});
}
```
Payload 只放用户标识(id、role),不放敏感信息——JWT 只做签名不做加密,Base64 可被任何人解码。
## 验证中间件
```javascript
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return res.status(401).json({ error: "Missing token" });
try {
req.user = jwt.verify(token, SECRET, { issuer: "my-app" });
next();
} catch (e) {
const msg = e.name === "TokenExpiredError" ? "Token expired" : "Invalid token";
res.status(401).json({ error: msg });
}
}
```
中间件从 Authorization 头提取 Token,验证失败区分过期和无效两种情况。
## Refresh Token
Access Token 过期时间短(15min),Refresh Token 是服务端生成的随机字符串,存入 Redis 并设置较长过期(7d)。客户端用 Refresh Token 换取新 Access Token,避免用户频繁重新登录。登出时删除 Redis 中对应的 Refresh Token 即可使其失效。
## HS256 vs RS256
- HS256:对称加密,签发和验证用同一密钥,适合单体应用
- RS256:非对称加密,私钥签发、公钥验证,适合微服务架构——各服务只需公钥即可验证,无需共享私钥
## 常见追问
**JWT 和 Session 有什么区别?**
Session 是有状态方案,服务端存储会话数据;JWT 是无状态方案,信息编码在 Token 中。JWT 适合分布式系统和跨域场景,但无法主动吊销(除非引入黑名单)。
**Token 泄露怎么办?**
设置短过期时间 + Refresh Token 轮换机制;敏感操作二次验证;必要时将已泄露 Token 加入 Redis 黑名单,验证时先查黑名单。
**为什么不在 JWT 里存角色权限?**
Token 一旦签发无法修改,如果用户角色变更,旧 Token 仍携带过期权限。建议 Token 只存 id,权限实时查库或查缓存。服务端5月27日 23:08
JWT 的过期时间怎么设?刷新机制怎么实现?## 直接回答
**过期时间设置:Access Token 设 15-30 分钟,Refresh Token 设 7-30 天。** Access Token 短过期限制盗用窗口,Refresh Token 长过期保证用户体验,两者配合实现安全与可用性的平衡。
**刷新机制核心思路:双 Token 架构。** 登录时同时签发 Access Token(短命)和 Refresh Token(长命),前者用于业务请求,后者仅在 Access Token 过期时调用刷新接口换取新的 Access Token。Refresh Token 必须存服务端(推荐 Redis),支持主动撤销。
## 过期时间设置
```javascript
const jwt = require('jsonwebtoken');
// Access Token:15 分钟
const accessToken = jwt.sign({ userId }, SECRET, { expiresIn: '15m' });
// Refresh Token:7 天(不使用 JWT,用随机字符串存 Redis)
const refreshToken = crypto.randomBytes(40).toString('hex');
await redis.setex(`refresh:${refreshToken}`, 7 * 24 * 3600, userId);
```
**为什么不推荐 Refresh Token 也用 JWT?** JWT 一旦签发无法撤销,如果 Refresh Token 也是 JWT 则无法实现服务端主动失效,失去 Refresh Token 存在的意义。
## 刷新接口实现
```javascript
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
const userId = await redis.get(`refresh:${refreshToken}`);
if (!userId) {
return res.status(401).json({ error: 'Invalid or expired refresh token' });
}
// 生成新 Access Token
const accessToken = jwt.sign({ userId }, SECRET, { expiresIn: '15m' });
// 可选:轮换 Refresh Token(刷新时同时换发新的,旧的作废)
await redis.del(`refresh:${refreshToken}`);
const newRefresh = crypto.randomBytes(40).toString('hex');
await redis.setex(`refresh:${newRefresh}`, 7 * 24 * 3600, userId);
res.json({ accessToken, refreshToken: newRefresh, expiresIn: 900 });
});
```
## 前端自动刷新
核心思路:Axios 响应拦截器捕获 401,自动用 Refresh Token 换新 Access Token,然后重试原请求。并发场景下用队列避免多次刷新。
```javascript
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const { refreshToken } = localStorage;
const { data } = await axios.post('/auth/refresh', { refreshToken });
localStorage.accessToken = data.accessToken;
localStorage.refreshToken = data.refreshToken;
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return api(originalRequest);
}
return Promise.reject(error);
}
);
```
## 追问:Refresh Token 被盗怎么办?
1. **Token 轮换**:每次刷新时换发新 Refresh Token、作废旧的,被盗的旧 Token 下次使用时会被服务端拒绝
2. **Token 版本控制**:用户表加 `tokenVersion` 字段写入 Access Token,修改密码或强制登出时递增版本号,旧 Token 自动失效
3. **使用次数限制**:同一个 Refresh Token 只允许刷新一次,重复使用说明可能被盗,立刻撤销该用户所有 Refresh Token
## 追问:如何强制用户下线?
在用户表维护 `tokenVersion`,签发 Access Token 时写入该值。验证时对比数据库中的版本号,不一致则拒绝。强制下线只需递增版本号,所有旧 Token 立即失效,无需等过期。
```javascript
// 签发时
const accessToken = jwt.sign({ userId, tokenVersion: user.tokenVersion }, SECRET, { expiresIn: '15m' });
// 验证时
const decoded = jwt.verify(token, SECRET);
const user = await getUserById(decoded.userId);
if (user.tokenVersion !== decoded.tokenVersion) {
throw new Error('Token revoked');
}
```
## 追问:并发刷新会不会产生竞态?
会。多个请求同时 401 时可能触发多次刷新。解法:前端用 `isRefreshing` 标志 + 订阅队列,第一个请求执行刷新,后续请求排队等刷新完成后拿到新 Token 重试。
## 关键要点
- Access Token 短过期(15-30m),Refresh Token 长过期(7-30d),后者存 Redis
- Refresh Token 不用 JWT,用随机字符串,保证可撤销
- 刷新时轮换 Refresh Token,防盗用
- 强制下线用 tokenVersion 方案,最简洁
- 前端用拦截器 + 队列解决自动刷新和并发竞态服务端5月27日 23:08
JWT 错误处理应该怎么做?JWT 错误处理的核心思路是:**按错误类型分类响应,统一错误格式,前端拦截自动续签**。
`jsonwebtoken` 的 `jwt.verify()` 抛出的错误主要有三种:`TokenExpiredError`(过期)、`JsonWebTokenError`(格式/签名无效)、`NotBeforeError`(nbf 未生效)。处理方式不是逐个 try-catch,而是在一个 catch 中按 `error.name` 分发。
## 后端:统一错误分发
```javascript
function handleJwtError(error) {
const map = {
TokenExpiredError: { status: 401, code: 'TOKEN_EXPIRED', msg: '令牌已过期' },
JsonWebTokenError: { status: 401, code: 'INVALID_TOKEN', msg: '令牌无效' },
NotBeforeError: { status: 401, code: 'TOKEN_NOT_ACTIVE', msg: '令牌尚未生效' },
};
return map[error.name] || { status: 500, code: 'AUTH_ERROR', msg: '认证失败' };
}
function authMiddleware(req, res, next) {
const token = (req.headers.authorization || '').replace('Bearer ', '');
if (!token) return res.status(401).json({ code: 'MISSING_TOKEN', msg: '未提供令牌' });
try {
req.user = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
next();
} catch (e) {
const err = handleJwtError(e);
res.status(err.status).json({ code: err.code, msg: err.msg });
}
}
```
关键点:`jwt.verify` 的 `algorithms` 参数必须显式指定,防止算法混淆攻击(none 算法绕过)。
## 前端:拦截 401 并自动续签
```javascript
api.interceptors.response.use(
res => res,
async error => {
const { config, response } = error;
if (response?.status === 401 && !config._retry) {
if (response.data.code === 'TOKEN_EXPIRED') {
config._retry = true;
try {
const { data } = await axios.post('/auth/refresh', {
refreshToken: localStorage.getItem('refreshToken')
});
localStorage.setItem('accessToken', data.accessToken);
config.headers.Authorization = `Bearer ${data.accessToken}`;
return api(config);
} catch {
// refresh 也失败,必须重新登录
}
}
localStorage.clear();
window.location.href = '/login';
}
return Promise.reject(error);
}
);
```
并发请求时多个 401 会同时触发 refresh,需要用 Promise 队列去重,只发一次 refresh 请求,其余请求等结果复用新 token。
## 刷新 Token 的错误边界
Refresh token 过期或被撤销(黑名单)时,服务端直接返回 `LOGIN_REQUIRED`,前端跳登录页,不再重试。不要对 JWT 验证错误做自动重试——验证是幂等的,失败就是失败,重试没有意义。
## 安全注意事项
- 错误消息不要暴露内部实现(如密钥名、算法细节),对外只返回业务错误码
- 验证失败要记日志,包含 IP、路径、UA,用于检测暴力破解和 token 嗅探
- 生产环境用 HttpOnly Cookie 存 token,避免 XSS 读取 localStorage
## 追问:Token 过期但用户正在操作怎么办?
两种方案:一是前端在 token 快过期时静默 refresh(如距过期 5 分钟主动续签);二是后端在响应头加 `X-Token-Will-Expire`,前端收到后触发刷新。两者都依赖 refresh token 仍有效的前提。