标签

JWT

JSON Web Token (JWT) 是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。由于信息是经过数字签名的,所以可以验证其完整性。另外,可以对 JWT 进行加密以保护数据的机密性。 JWT 主要用于身份验证和信息交换,特别适合用于分布式站点的单点登录(SSO)场景。

JWT
服务端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');}```
服务端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 仍有效的前提。