做后端开发这些年,我最早给API加认证的时候,用的还是最笨的办法:每个接口里自己查一遍登录状态,查到了放行,查不到返回401。后来接手的项目多了,发现只要涉及用户体系,API认证就是一道绕不开的门槛。尤其是现在前后端分离、移动端和Web端共用一套接口的场景,接口一旦裸奔,任何人都可以拿着HTTP工具直接调你的下单、查询、修改接口,轻则数据被爬,重则整个业务被刷穿。
HTTP协议本身是无状态的,服务器默认不知道“这次请求是不是上次那个人”。为了解决这个“记住我”的问题,业界给出了很多方案:Cookie-Session、OAuth2、API Key、JWT。我为什么最终在各种方案里反复回到JWT?核心原因是它无状态、跨端友好、实现成本低。认证信息全部放在Token里,服务器不需要为每个登录用户维护会话记录,这对水平扩容特别友好,几台后端实例随便加,不需要同步Session。
很多刚接触这块的读者会问,为什么不直接用Cookie-Session?原因很简单:Session模式需要服务端保存状态,要么存内存,要么存Redis,多实例部署时还要处理Session共享。而且Cookie在跨域场景下非常别扭,移动端也没有Cookie这种概念,得自己维护会话标识。所以当你的接口同时面向Web、小程序、App的时候,Session方案会越做越重。
API Key呢?它适合机器与机器之间的调用,比如内部服务间通信、第三方平台对接。但API Key通常长期有效,没有过期和用户维度的概念,一旦泄露就是永久性风险,做用户认证并不合适。OAuth2则是一个完整的授权框架,适合“第三方应用需要访问用户资源”的场景,比如微信登录、GitHub登录,它和JWT并不是互斥关系,OAuth2签发的Access Token完全可以是JWT。
JWT最适合的场景,就是第一方应用的用户认证:自己负责注册登录,自己的后端API需要识别每个请求是谁,并且要做接口级别的权限控制。它的思路是把用户身份信息和过期时间一起做签名,发给客户端,之后客户端每次请求带上这个Token,服务端验签通过就放行。这个流程就是标题里说的“用户认证与授权”,我在后面会把它拆成登录、校验、授权、续签四步,每一步都有可以直接落地的代码。
1. JWT到底是个什么结构:三段字符串背后的设计逻辑
1.1 Header、Payload、Signature分别管什么
很多初学者第一次看到JWT,会被那一长串以点号分隔的字符串吓到。其实拆开来看,JWT就是三个用Base64Url编码的片段拼在一起,中间用英文句点分隔,类似这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c三个片段分别对应Header、Payload、Signature。
Header里存的是这个Token的元信息,最常见的就是签名算法(alg)和类型(typ)。默认长这样:
{ "alg": "HS256", "typ": "JWT" }Payload是真正承载业务信息的地方。你可以把它理解成一张“身份卡”上的字段,比如用户ID、用户名、角色、过期时间等。这些字段在JWT标准里叫Claim,其中有一批预定义的标准字段,比如exp(过期时间)、iat(签发时间)、iss(签发者)、aud(受众)、sub(主体)。除了标准字段,你完全可以根据业务需要自定义字段,比如role、tenantId、userId。
Signature是三个部分里最关键的,它负责保证前面两段内容没有被篡改。签名的计算方式是把Header和Payload的Base64Url字符串用句点拼起来,再用Header里声明的算法和密钥做一次签名运算。以HS256为例,伪代码就是:
SigningInput = Base64Url(Header) + "." + Base64Url(Payload) Signature = HMACSHA256(SigningInput, Secret)服务端收到Token后,会用同样的密钥对前两段重新算一遍签名,再和客户端带过来的签名对比。只要密钥不泄露,攻击者改了Header或Payload里的任何字符,签名都会对不上,直接验签失败。
1.2 HS256和RS256:对称与非对称签名算法的取舍
JWT的签名算法按密钥类型分为两大类:对称签名和非对称签名。对称签名的代表是HS256,加密和解密用的是同一个密钥,实现简单、性能好,但要求签发方和验证方必须共享同一个密钥。如果你的API只有一套后端,所有Token都由它签发,也由它验证,那HS256完全够用,也是大多数项目的首选。
非对称签名的代表是RS256,它有一对密钥:私钥负责签发,公钥负责验证。私钥只有认证服务持有,公钥可以放心交给任何需要验证Token的服务。这种设计在多服务架构里特别有用:用户在一个认证中心登录,拿到Token之后去访问订单服务、支付服务、用户服务,这些服务只需要拿到公钥就能完成验签,不需要跟认证中心做网络通信,也不存在密钥同步泄漏的问题。
选型建议我给得很直接:单体应用、或所有服务都在同一个可信任内网里,用HS256,省事;微服务架构、有独立的认证中心,或者第三方合作方也需要验证Token,用RS256。另外要注意,算法选择必须和安全配置绑定,我在后面安全章节会重点讲“算法混淆攻击”,如果服务端只验签不限制算法,攻击者可以用一个自己知道的公钥把算法改成HS256,然后用公钥当密钥来签名,整个认证就形同虚设。
1.3 Payload字段设计:哪些能放,哪些千万别放
Payload是JWT最容易被误解的地方。很多人把它当成一个“加密存储”,以为别人看不到里面的内容,于是把密码、手机号、身份证号、甚至银行卡号直接塞进去,这是非常危险的习惯。JWT的Payload只是Base64Url编码,不是加密,任何人把这个Token复制到jwt在线解析的网页上,一键就能看到明文内容。
那Payload里到底该放什么?我的经验是只放“认证和授权必要的最小集合”。最核心的几个字段:用户ID、角色或权限标识、必要时的租户ID、以及标准的时间类字段iat和exp。不要放任何敏感个人信息,不要放大对象、大列表,因为Token每次请求都要随Header传输,Payload越大,带宽消耗越明显,HTTP Header的大小也有限制,很多网关默认限制8KB以内。
还有一点需要强调:Payload里的信息都是客户端可以自己看到的,所以永远不要信任Payload里的数据本身,只信任“验签通过后的数据”。服务端应该在验签通过后,把用户ID从Token里取出来,然后去数据库或缓存里查这个人的最新状态,而不是直接相信Token里的role字段永远是准的。比如一个用户被管理员降权了,如果还留在有效期内的旧Token里的role是admin,那权限判断就会出错,这就是为什么关键权限校验不能只看Token里的静态字段。
2. 实操第一步:用Node.js实现用户登录和Token签发
2.1 环境准备与依赖选型
技术栈我选了Node.js + Express,因为这是最主流的API开发组合之一,代码量少,理解成本低。不过整套思路换到Java、Go、Python、.NET都一样。Java里可以用Hutool的JWT工具或jjwt,.NET Core里可以用System.IdentityModel.Tokens.Jwt,API虽然不同,原理完全一致。
先建一个项目并安装依赖:
mkdir jwt-demo && cd jwt-demo npm init -y npm install express jsonwebtoken dotenv这里闯进日常生活的是两个关键依赖:
- jsonwebtoken:负责JWT的签发和验证,最常用,没有之一。
- dotenv:用来加载环境变量文件,把密钥放到环境里,不要写在代码中。
2.2 登录接口的完整实现
一个典型的登录接口流程是:接收用户名密码,校验用户是否存在、密码是否正确,正确则生成一个Token返回给前端。下面这段代码就是最小可用的登录接口:
const express = require('express'); const jwt = require('jsonwebtoken'); require('dotenv').config(); const app = express(); app.use(express.json()); // 这里仅为演示,真实项目请替换为数据库查询 const users = [ { id: 1, username: 'admin', password: '123456', role: 'admin' }, { id: 2, username: 'user', password: '123456', role: 'user' } ]; app.post('/api/login', (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ message: '用户名和密码不能为空' }); } const user = users.find( (u) => u.username === username && u.password === password ); if (!user) { return res.status(401).json({ message: '用户名或密码错误' }); } const token = jwt.sign( { sub: user.id, username: user.username, role: user.role }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '2h' } ); res.json({ token, tokenType: 'Bearer' }); }); app.listen(3000, () => { console.log('API server running at http://localhost:3000'); });仔细观察jwt.sign的调用,第一个参数是Payload对象,第二个是密钥,第三个是配置项。expiresIn支持各种写法,比如'15m'、'2h'、'7d',也可以直接传秒数。jsonwebtoken会自动帮你把exp字段写到Payload里,不需要手动维护过期时间。
2.3 过期时间怎么定:expiresIn、iat和在线解析里的时间戳
JWT的过期时间有两个时间点需要区分:iat是签发时间,exp是过期时间。二者都是Unix时间戳,单位是秒。jsonwebtoken在签发时如果设置了expiresIn,会自动计算exp的值。比如签发时iat是1710000000,expiresIn设为'2h',那exp就是1710007200。
在jwt在线解析工具里,你会看到类似“Expires At: 2024-03-10 12:00:00”这样的人类可读时间,其实就是exp字段解析出来的结果。这个工具非常适合开发调试,建议只在本地环境使用自己的测试Token,生产环境的Token要小心,一旦贴上去,Payload里的内容对工具提供方就是透明的。
过期时间具体设多长,没有标准答案,完全取决于业务风险。管理后台可以设短一点,比如15分钟到30分钟;普通C端App可以设2小时到24小时;如果要做“7天免登录”,不要直接把access token设成7天,更稳妥的方案是用Refresh Token,我后面有一整节专门讲续签。这里的原则是:过期时间越短越安全,用户体验越差;越长体验越好,泄露风险越大。行业里比较常见的做法是Access Token 15分钟到2小时,Refresh Token 7天到30天。
3. 实操第二步:写一个JWT认证中间件保护你的API
3.1 中间件的校验流程
登录接口可以签发Token,但如果每个业务接口都在代码里手动解析和验签,很快就会写出一大堆重复代码,而且很容易漏掉某个接口。正确做法是写一个认证中间件,在请求到达业务路由之前统一做校验。
中间件的校验流程一般是四步:
- 从请求头里取出Authorization字段。
- 判断格式是否是Bearer Token。
- 用jsonwebtoken的verify方法验签,并检查过期时间。
- 验签通过后,把用户信息挂到req对象上,继续往下走业务逻辑。
代码如下:
function authenticate(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ message: '缺少Authorization请求头' }); } const parts = authHeader.split(' '); if (parts.length !== 2 || parts[0] !== 'Bearer') { return res.status(401).json({ message: 'Authorization格式不正确' }); } const token = parts[1]; try { const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }); req.user = payload; next(); } catch (err) { if (err.name === 'TokenExpiredError') { return res.status(401).json({ message: 'Token已过期' }); } return res.status(401).json({ message: 'Token无效' }); } }注意这里第14行我专门传了algorithms参数,限定只接受HS256,这是防止算法混淆攻击的关键一步,后面安全章节会展开讲。
3.2 解析Authorization头的细节与坑
Authorization头最常见的标准格式是Bearer <token>,Bearer是认证类型,Token跟在后面,中间用空格分隔。但这里坑不少。第一个坑是大写问题:有些客户端会把Bearer写成bearer或BearerToken,如果你的判断写得太死,就会被误杀。稳妥的做法是对第一个片段做大小写不敏感处理,比如parts[0].toLowerCase() !== 'bearer'。
第二个坑是Token里本身有两段点号,直接用split(' ')是安全的,因为空格不会出现在Base64Url编码里。但如果你脑抽用了split('.')去取Token,那就会拆出Header、Payload、Signature三个片段,和自己想要的东西完全不一样。
第三个坑是Header可能带引号,尤其是一些老旧的客户端或代理层。解决方式是用authHeader.replace(/^Bearer\s+/i, '')直接去掉前缀,而不是手动split,这样容错性更好。
3.3 把用户身份注入请求,后续接口直接使用
中间件验签通过后,我习惯把解析出来的payload直接挂到req.user上,这样后续路由处理器里随时可以用,不需要再解析一次。比如:
app.get('/api/profile', authenticate, (req, res) => { res.json({ userId: req.user.sub, username: req.user.username, role: req.user.role }); });这里有个重要提醒:req.user里的内容来自客户端的Token,虽然经过验签,但客户端完全可以自己构造一组合法的Payload字段交给服务端签名。所以不要假设req.user里的所有字段都是可信的,尤其是权限类字段,需要以数据库或权限表中的最新数据为准。严格的做法是只信任sub,也就是用户ID,其他信息到数据库里重新查一遍。
4. 授权怎么做:从认证到“只允许admin访问”
4.1 RBAC最小可用模型:角色字段就够了
认证解决的是“你是谁”的问题,授权解决的是“你能干什么”的问题。很多项目刚起步时权限模型很简单,就是用户-角色两级。JWT+RBAC(基于角色的访问控制)是最经典也最容易落地的搭配。
在Payload里放一个role字段,登录时从用户表里查出来写进去,后面的接口校验这个字段就能实现粗粒度权限控制。比如管理员的删除接口,只有role为admin的人可以调用。
如果业务复杂一点,一个用户可能有多个角色,对应多个部门或项目,Payload里就可以放一个数组:roles: ['admin', 'editor']。再复杂一点的,可以在服务端维护一张角色-权限映射表,Token里只放角色,具体权限点查缓存或字典,这样用户角色变化后不需要等Token过期就能生效。
4.2 接口级权限校验的实现
在JWT认证中间件的基础上,再做一层授权中间件。下面代码实现了一个requireRole工厂函数,返回一个中间件,检查当前用户角色是否在允许列表里:
function requireRole(...allowedRoles) { return (req, res, next) => { if (!req.user) { return res.status(401).json({ message: '未认证' }); } const roles = Array.isArray(req.user.role) ? req.user.role : [req.user.role]; const hasRole = roles.some((r) => allowedRoles.includes(r)); if (!hasRole) { return res.status(403).json({ message: '没有权限执行此操作' }); } next(); }; } app.delete('/api/users/:id', authenticate, requireRole('admin'), (req, res) => { res.json({ message: '用户删除成功' }); });401和403的区别要讲清楚:401是“你没有被认证”,或者Token无效、过期;403是“你认证了,但权限不够”。我见过不少项目把这两个状态码混用,客户端根据状态码做跳转逻辑时就会出问题。
4.3 路由级、按钮级和数据级权限怎么配合
接口权限只是授权的一层。真正完整的授权体系通常有三层:
- 路由级/接口级:决定某个用户能不能访问某个URL。
- 按钮级:决定页面上的按钮是否展示,比如“删除”“审核”。
- 数据级:决定同一份数据,不同用户能看到哪些,比如部门经理只能看到本部门数据。
按钮级权限通常不由后端JWT直接管,而是登录后后端返回当前用户的权限码列表,前端根据权限码控制按钮显示。数据级权限则必须在后端业务逻辑里强制校验,因为前端隐藏按钮并不能阻止别人直接拼HTTP请求调接口。
我见过最典型的漏洞就是:前端把删除按钮藏起来了,用户就没法删除,但攻击者用Burp Suite直接发一个DELETE请求,接口也没有做数据归属校验,结果把别人的数据删了。所以接口权限只是第一道门,数据级权限必须靠后端对资源归属做判断,这个比JWT本身更重要。
5. Token过期不是终点:续签与刷新令牌实操
5.1 为什么Access Token不能设置得太长
Access Token如果设置成7天、30天,泄露后的风险窗口太长。而且JWT一旦签发,在过期之前是无法主动作废的,除非你再引入黑名单机制,那就失去了无状态的优势。所以行业惯例是把Access Token的过期时间设短,比如15分钟到2小时,然后配合一个Refresh Token来实现“过期后无感续签”。
Refresh Token是一个生命周期更长的凭证,通常7天到30天。它的作用是专门用来换取新的Access Token,不参与业务接口的权限判断。Refresh Token必须存储在服务端可管理的地方,比如数据库或Redis,关键是可以主动吊销。
5.2 双Token方案的实现流程
登录时一次性签发两个Token:Access Token和Refresh Token。Access Token按照正常方式给前端用,Refresh Token可以选择存入Redis,记录它对应的用户ID、过期时间、是否已撤销等信息。
简化版登录签发:
const accessToken = jwt.sign( { sub: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '30m' } ); const refreshToken = jwt.sign( { sub: user.id, type: 'refresh' }, process.env.REFRESH_SECRET, { expiresIn: '7d' } );刷新Token时提供一个新接口:
app.post('/api/refresh', (req, res) => { const { refreshToken } = req.body; try { const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET, { algorithms: ['HS256'] }); // 建议在这里检查Redis中的refreshToken是否已撤销 if (payload.type !== 'refresh') { return res.status(401).json({ message: 'Token类型错误' }); } const newAccessToken = jwt.sign( { sub: payload.sub, role: payload.role }, process.env.JWT_SECRET, { expiresIn: '30m' } ); res.json({ accessToken: newAccessToken }); } catch (err) { res.status(401).json({ message: 'Refresh Token无效或已过期' }); } });这里Refresh Token的secret单独用一个,不要和Access Token共用,这样即使Access Token的密钥泄露,Refresh Token也不会被伪造。
5.3 无感刷新和重放检测的几个细节
前端拿到新的Access Token后,要能替换掉之前的存储。我见过很多项目在Token续签后没回写,导致用户明明请求成功,但下一次请求还是用的旧Token,结果又401。正确做法是前端封装一个统一的HTTP客户端,比如axios拦截器,遇到401时自动调用刷新接口,拿到新Token后重放原来的请求。
重放检测是Refresh Token方案里比较进阶的话题。原理是每次刷新时,把Refresh Token轮换成一个新的Token,旧的立即作废。如果有人偷到了Refresh Token,正常用户一旦刷新,攻击者手里的旧Token就会失效。更严格的做法是在Redis里存Refresh Token的哈希,每次刷新都要校验并更新。
实际项目中Refresh Token也需要考虑吊销场景:用户修改密码后,应该把该用户所有的Refresh Token全部标记失效;用户退出登录时,也要删除服务端记录的Refresh Token。
6. 安全加固:JWT身份认证绕过漏洞与修复建议
6.1 为什么jwt在线解析能解开你的Token
先明确一个概念:JWT是签名,不是加密。签名的作用是防篡改,不是防泄露。任何拿到Token的人,都能在jwt在线解析这类工具上把Header和Payload直接解出来。这不是漏洞,而是JWT的设计机制,但很多人把它当加密用,把敏感信息直接放进去,这才是真正的漏洞来源。
所以在设计Payload时务必遵守一条铁律:除用户ID、角色这类非敏感身份标识外,其他信息一律不放。如果确实需要在Token里带一些中间态数据,也要保证这些数据不构成隐私泄露。
6.2 常见攻击路径:算法混淆、密钥泄露、暴力破解
算法混淆攻击(Algorithm Confusion Attack)是针对JWT最典型的攻击方式。攻击者把Header里的alg字段从RS256改成HS256,然后用目标服务公开的公钥作为HS256的对称密钥来签名。如果服务端验签时没有限制算法,就会用公钥去验证这个HS256签名,结果成功通过。要修复这个问题,核心就是一条:在verify时显式指定algorithms白名单,只允许服务端预期的算法。
密钥泄露是另一个常见问题。很多项目把JWT_SECRET直接硬编码在代码里,或者用了一个很短的“123456”之类的弱密钥,攻击者可以下载在线字典暴力跑出密钥。我只说一个建议:密钥长度至少32字节以上,最好是64字节的随机字符串,用openssl rand -base64 48生成,存入环境变量或密钥管理服务,并定期轮换。
暴力破解更多发生在对方拿到了一个真实Token但没拿到密钥的情况下。如果密钥强度不够,攻击者可以用hashcat等工具离线爆破。要应对这个问题,除了提高密钥复杂度,还可以引入签名算法的加盐策略,并在服务端监控异常频繁的401请求。
6.3 绕过漏洞的修复清单
根据我在网上看到的“JWT令牌身份认证绕过漏洞”相关案例,总结一份修复清单:
- 在jwt.verify中强制指定algorithms参数,禁止省略。
- 校验iss和aud字段,确认Token是给自己的服务签发的。
- 不要信任Header里的kid、jku等参数,除非你有完整的信任链校验逻辑。
- 对Payload里的角色、租户信息等关键字段,到数据库或权限中心二次校验。
- 控制Token有效期,缩短到业务可接受的最小范围。
- 对需要吊销能力的Token,在Redis中维护黑名单或白名单。
- 使用HTTPS传输,避免Token在网络上明文被抓包。
- 在Refresh Token刷新接口上增加防重放逻辑,每次刷新同步轮换Token。
6.4 密钥管理与多环境配置
关于密钥管理,我的个人习惯是:本地开发用.env文件,测试环境用CI/CD注入的环境变量,生产环境用独立的密钥管理服务,比如云厂商的KMS、Vault这类工具。千万不要让开发、测试、生产共用同一个密钥,这意味着任何一个环境被攻破,所有环境的Token都能被伪造。
同时要看好日志。很多团队在开发调试时喜欢把Token和密钥打到日志里,这些日志一旦泄露,等于把认证系统的钥匙交给别人。建议日志中只记录Token的前几位和后几位,比如token: eyJxxx...abc,方便定位问题,又不暴露完整信息。
7. 常见报错排查与个人实战心得
7.1 常见报错速查表
我在实际项目里最常看到的几个JWT相关报错,整理成一张速查表,方便你遇到问题的时候直接对照:
| 报错信息 | 含义 | 常见原因 | 处理方式 |
|---|---|---|---|
| jwt malformed | Token格式不完整 | 不是三段结构,可能被截断或手写错误 | 检查Authorization头取值是否完整 |
| invalid signature | 签名校验失败 | 密钥不匹配、Token被篡改 | 检查JWT_SECRET是否一致 |
| jwt expired | Token已过期 | 超过了exp字段时间 | 触发刷新流程或重新登录 |
| jwt not active | Token还未生效 | nbf字段设置为未来时间 | 检查服务器时间是否同步 |
| no authorization token was found | 没找到Token | Header缺失或格式错误 | 检查前端是否携带Authorization头 |
| invalid algorithm | 算法不被允许 | 服务端算法白名单与Token不一致 | 统一算法配置 |
其中“invalid signature”最常见的原因是多个后端服务用了不同的密钥或环境变量没同步。尤其在容器化部署后,同一个服务多个副本配置不一致,会出现“有时能通过,有时401”的诡异问题,排查时先看环境变量是否一致。
7.2 调试技巧与日志设计
调试JWT最方便的还是在jwt在线解析工具上,但注意别在生产环境随便贴真实Token。我自己常用的方式是写一个本地脚本,用同一个密钥签发测试Token,然后在服务端日志中输出验签失败的err.name和过期时间,能快速定位问题。
日志设计上,我建议对认证相关的请求统一输出一串指标:用户ID、IP、请求路径、Token是否有效、过期时间剩余秒数。这些日志在排查线上问题时能救命。比如用户投诉“老是掉线”,你一看日志发现所有401都集中在某个时间段,很可能就是Access Token过期时间太短,或者前端刷新没做好,而不是密钥有问题。
7.3 我在真实项目里的几点体会
最后分享几个我踩过坑之后总结的体会。
第一,JWT不是银弹。如果你的业务有服务端主动注销用户的需求,比如封号、踢人下线,纯JWT无状态方案就不好用,必须引入Redis黑名单或者改用其他方案。我的做法是:普通接口用JWT无状态认证,敏感操作(改密码、注销、支付)额外校验一次更严格的Session或二次验证码。
第二,JWT的续签代码一定要在项目初期就设计好,不要等到上线后再补。我接手过一个系统,Access Token设了7天,没有Refresh Token,也没有自动续签,结果用户改了密码之后,盗用者还能用旧Token继续访问长达7天。这个漏洞就是我前面说的“Token过期时间是安全边界”的最好反例。
第三,把认证和授权配置拆开。认证中间件只管验签,授权中间件只管查角色和权限,两个中间件的职责不要混在一起。这样当权限模型升级时,你只需要改授权部分,不需要动认证逻辑。我在项目里还习惯把这两个中间件做成可复用的npm包或公共模块,几个服务共用同一套逻辑,避免不同团队各写一套导致安全标准不统一。
第四,一定要给Token加一个客户端标识。在Payload里放一个字段记录签发时的设备信息,比如设备指纹或设备ID。这样如果检测到同一个Refresh Token在两个设备上使用,就可以判断可能发生了泄露,及时让该Token失效。网上很多关于“JWT续签”的讨论都聚焦在过期时间上,但真正影响安全的是Token被复制后能否被识别和拦截。
JWT这个东西,上手容易,做好很难。我见过太多项目把“认证通过”和“授权正确”混为一谈,也见过把Token当数据库缓存把ID信息一股脑塞进去的做法。把基础原理搞清楚,再按业务需要选择合适的安全加固措施,这套方案就能在你的项目里跑得很稳。