三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

JWT令牌详解:从原理到实践,构建无状态身份认证系统

JWT令牌详解:从原理到实践,构建无状态身份认证系统

1. 从登录到鉴权:为什么我们需要Token?

如果你做过Web开发,或者用过一些需要登录的App,肯定对“登录”这个动作不陌生。输入用户名密码,点一下,就进去了。但你想过没有,服务器怎么知道“你”就是“你”呢?尤其是在你点开一个新页面,或者刷新一下App的时候,为什么不需要重新输入密码?这背后,就是“会话管理”和“身份认证”在起作用。

早期的Web应用,普遍采用一种叫Session-Cookie的机制。简单来说,就是你第一次登录,服务器在内存或数据库里创建一个“档案袋”(Session),里面记录你的登录状态、用户ID等信息,然后给你一个“取件码”(Session ID),通常放在你浏览器的Cookie里。之后你每次访问,浏览器都自动带上这个“取件码”,服务器一看,哦,是熟人,档案袋里有记录,直接放行。

这个模式在单体应用时代挺好用,但随着系统越做越大,问题就来了。想象一下,你的应用部署了10台服务器做负载均衡,你第一次登录请求落在了A服务器,它给你创建了档案袋。但下一次请求,负载均衡器把你指到了B服务器,B服务器一看这个“取件码”,懵了——我这儿没存你的档案袋啊!这就叫Session不共享问题。为了解决它,你可能得搞个Redis集群来集中存储所有Session,增加了架构的复杂性。

更关键的是,这种模式是“有状态”的。服务器必须时刻维护着这个档案袋,记住每个登录的用户。对于用户量巨大的应用,这对服务器内存和存储是巨大的压力。于是,一种更轻量、更灵活的方案应运而生,这就是Token

Token,中文常译作“令牌”,它的核心思想是“去状态化”或“自包含”。服务器不再帮你保管档案袋,而是在你登录成功后,直接把你的身份信息(比如用户ID、角色)、有效期等,打包加密成一个字符串,发给你。这个字符串就是Token。你之后每次请求,只需要在HTTP请求头(通常是Authorization头)里带上这个Token。服务器收到后,用自己的密钥解密并验证这个Token的合法性和有效性,如果通过,就认为你是合法用户。

这样做的好处显而易见:

  1. 无状态:服务器不用存任何会话信息,减轻了存储压力,架构更简单。
  2. 易于扩展:因为不依赖服务器内存,所以非常适合分布式和微服务架构。任何一台服务器或服务,只要持有验证密钥,都能独立验证Token。
  3. 跨域支持:Token可以轻松放在请求头里,完美支持跨域(CORS)场景,这是Cookie在默认情况下比较麻烦的地方。
  4. 多端适配:不仅浏览器,移动端App、桌面客户端、甚至IoT设备,都可以用同一种方式携带Token,协议统一。

JWT,就是实现Token这种思想的一种非常流行和标准化的具体方案。它不是Token的唯一形式,但无疑是目前最主流、最被广泛讨论和使用的。接下来,我们就深入这个“令牌”的核心,看看JWT到底是怎么一回事。

2. JWT解剖:三段式结构的自包含令牌

JWT,全称JSON Web Token,读作 [jot]。它本质上是一个开放标准(RFC 7519),定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这个信息可以被验证和信任,因为它是数字签名的。

你可以把JWT想象成一张防伪门票。这张门票上印着(加密了)你的基本信息(比如姓名、票类)、入场时间(签发时间)和有效期。检票员(服务器)有特定的验票机(密钥)可以验证这张门票的真伪和是否在有效期内,而无需去后台查数据库确认你的购票记录。

一张JWT令牌就是一个长字符串,由三部分组成,用点(.)分隔,形如:xxxxx.yyyyy.zzzzz。它们分别是:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

2.1 Header:声明类型与算法

头部通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,如HMAC SHA256或RSA。

{ "alg": "HS256", "typ": "JWT" }

这里,alg表示签名算法是HS256(HMAC with SHA-256),typ表示类型是JWT。这个JSON对象会被Base64Url编码,形成JWT的第一部分。

注意:Base64Url编码是一种URL安全的Base64编码,它用-_分别替代了标准Base64中的+/,并且省略了末尾的=,以确保Token可以安全地放在URL或HTTP头中,而不会被特殊字符干扰。

2.2 Payload:携带的核心信息

负载部分包含了你要传递的“声明”。声明是关于实体(通常是用户)和其他数据的陈述。有三种类型的声明:

  • 注册声明:预定义的一些声明,虽然不是强制性的,但推荐使用,它们提供了一组有用的、可互操作的声明。例如:
    • iss:签发者
    • exp:过期时间
    • sub:主题
    • aud:接收方
  • 公共声明:可以添加任何信息的自定义声明,但为了避免冲突,应定义在IANA JSON Web Token Registry中或使用防冲突命名空间。
  • 私有声明:自定义声明,用于在同意使用它们的各方之间共享信息。

一个典型的Payload可能如下所示:

{ "sub": "1234567890", "name": "John Doe", "admin": true, "iat": 1516239022 }

这里,sub是用户ID,name是用户名,admin表示是否是管理员,iat是令牌签发时间。同样,这个JSON对象也会被Base64Url编码,形成JWT的第二部分。

重要心得:Payload里的信息虽然是Base64Url编码,但任何人都可以解码看到明文。所以,绝对不要在Payload里存放敏感信息,如密码、信用卡号等。JWT的设计目标是保证信息不被篡改,而不是保证信息不被看见。对于需要保密的信息,应该先加密再放入Payload,或者根本不放。

2.3 Signature:防篡改的保障

签名部分是整个JWT安全性的核心。它用于验证消息在传递过程中没有被篡改,并且,在使用私钥签名的场景下,还可以验证发送方的身份。

签名的生成方式依赖于Header中指定的算法。以HS256为例,签名是这样生成的:

HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )

简单说,就是将编码后的Header和Payload用点连接起来,然后使用一个只有服务器知道的密钥(secret)和指定的算法(如HMAC SHA256)进行签名计算。这个签名结果也会被Base64Url编码,作为JWT的第三部分。

验证过程:当服务器收到JWT时,它会用同样的密钥和算法,对收到的Header和Payload部分重新计算签名,然后与JWT自带的第三部分(签名)进行比较。如果一致,说明Token是完整的、未被篡改的;如果不一致,说明Token被修改过,立即拒绝。

将这三部分用点连接起来,就形成了一个完整的JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

你可以把这个字符串复制到任何在线的JWT调试器(如 jwt.io ),就能直观地看到解码后的Header和Payload,并可以尝试修改内容后观察签名失效的过程。

3. JWT工作全流程:从签发到验证的每一步

理解了JWT的结构,我们再来梳理一下它在实际应用中的完整生命周期。这个过程清晰地展示了无状态认证是如何运作的。

3.1 登录与令牌签发

  1. 用户提交凭证:用户在客户端(浏览器、App)输入用户名和密码,点击登录。
  2. 服务器验证凭证:客户端将凭证发送到认证服务器(例如/api/auth/login)。服务器查询数据库,核对用户名和密码(通常是加盐哈希后的密码)是否匹配。
  3. 生成JWT:验证通过后,服务器准备生成JWT。
    • 构建Header:指定算法,如{“alg”: “HS256”, “typ”: “JWT”}
    • 构建Payload:放入必要的用户信息,如用户ID(sub)、角色(role)、过期时间(exp,通常设置为当前时间+几小时或几天)。
    • 生成签名:使用服务器保管的密钥(secret),对Base64UrlEncode(header) + “.” + Base64UrlEncode(payload)进行签名计算。
    • 组合令牌:将三部分用点连接。
  4. 返回令牌:服务器将生成的JWT字符串返回给客户端。通常通过HTTP响应体返回,例如{“token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...”}

3.2 客户端存储与携带

客户端(通常是前端)收到Token后,需要妥善保存,并在后续请求中携带。

  • 存储方式
    • Web:可以存储在localStoragesessionStorage中。localStorage持久化,关闭浏览器后还在;sessionStorage会话级,关闭标签页即清除。也可以存储在Cookie中(需设置HttpOnlySecure以增强安全,但这会使JavaScript无法直接读取)。
    • 移动App:存储在安全的存储区域,如iOS的Keychain或Android的Keystore。
  • 携带方式:在发起需要认证的API请求时,将Token放在HTTP请求的Authorization头中,格式通常为:
    Authorization: Bearer <your-jwt-token>
    例如:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

3.3 服务器端验证与鉴权

  1. 拦截请求:受保护的API接口前,会有一个鉴权中间件。对于每个到达的请求,中间件首先检查Authorization头是否存在且格式正确。
  2. 提取并验证Token
    • Authorization头中提取出Token字符串。
    • 用点(.)分割字符串,得到三部分。
    • 使用相同的密钥(secret)和算法(从Header解码得知),对前两部分重新计算签名。
    • 将计算出的签名与Token自带的第三部分签名进行比较。这是最关键的一步,确保Token未被篡改。
  3. 检查标准声明:签名验证通过后,解码Payload,检查标准声明是否有效:
    • exp:检查令牌是否已过期(当前时间是否大于exp)。
    • nbf:检查令牌是否已生效(如果存在,当前时间是否大于等于nbf)。
    • iss:检查签发者是否可信(可选)。
    • aud:检查接收方是否为本服务(可选)。
  4. 鉴权与放行:所有验证通过后,中间件可以从Payload中提取出用户信息(如sub,role),并将其附加到本次请求的上下文(例如,在Node.js的req.user,在Java Spring的SecurityContext)中。然后,请求被传递给真正的业务处理逻辑。业务逻辑可以直接使用上下文中的用户信息,而无需再次查询数据库。
  5. 返回响应:业务逻辑处理完毕,返回响应给客户端。

这个流程完美体现了“无状态”的精髓:服务器不需要在内存或数据库中维护一个“已登录用户列表”,只需要在每次请求时,验证客户端带来的“门票”是否真实有效即可。所有必要的身份信息,都包含在这张“门票”里。

4. 深入实践:JWT的进阶议题与安全考量

在实际项目中,仅仅实现基础的JWT签发和验证是远远不够的。你会遇到一系列现实问题,处理不好就会带来安全漏洞或糟糕的用户体验。

4.1 Token的有效期与续签策略

JWT一旦签发,在过期之前无法被服务器主动废止,这是它相比Session的一个特点(也是缺点)。因此,合理设置有效期和设计续签机制至关重要。

  • Access Token与Refresh Token模式:这是解决JWT“无法主动失效”和平衡安全与体验的黄金标准。
    • Access Token:短期令牌,有效期较短(如15分钟、1小时)。用于访问业务API。即使泄露,危害时间也有限。
    • Refresh Token:长期令牌,有效期很长(如7天、30天),单独存储于服务器端(如数据库或Redis)。仅用于获取新的Access Token,不能直接访问业务API。

工作流程

  1. 用户登录,服务器返回一对Token:{“access_token”: “短效JWT”, “refresh_token”: “一个唯一的长字符串”},并将refresh_token及其对应用户ID存入服务器数据库。
  2. 客户端用access_token调用API。
  3. access_token过期,客户端用refresh_token调用一个特定的刷新接口(如/api/auth/refresh)。
  4. 服务器检查该refresh_token是否存在于数据库中且未过期、未被禁用。
  5. 验证通过后,服务器签发一个新的access_token返回给客户端。可以选择同时签发一个新的refresh_token并让旧的失效(滚动刷新),以增强安全性。
  6. 客户端使用新的access_token继续访问。

这种模式下,如果access_token泄露,攻击者只有很短的时间窗口。而服务器可以通过删除数据库中的refresh_token,随时让用户“下线”。

实操心得:Refresh Token的存储,建议用用户IDRefresh Token本身组合作为Key,并设置一个较长的TTL。当用户主动登出或管理员禁用用户时,直接删除对应的Refresh Token记录即可。这比黑名单整个JWT要高效得多。

4.2 安全性强化措施

  1. 使用强算法和足够长的密钥:避免使用已被认为脆弱的算法(如HS256在某些场景下密钥不够长时)。对于HS256/HS384/HS512,密钥长度必须足够。对于RS256/ES256等非对称算法,私钥必须妥善保管。
  2. HTTPS是必须的:JWT在传输过程中是明文的(Base64Url可解码),必须使用HTTPS来防止中间人攻击和Token被窃取。
  3. 避免在URL中传递JWT:虽然JWT设计为URL安全,但将其放在URL参数中可能导致Token被记录在浏览器历史、服务器日志中,造成泄露。应始终放在Authorization头中。
  4. 控制Payload大小:JWT会随着每次请求被发送,过大的Payload会增加网络开销。只存放必要的信息。
  5. 实现令牌黑名单(可选):对于某些需要立即吊销令牌的场景(如用户改密、管理员封禁),可以维护一个短期的黑名单(如Redis,存储已吊销但未过期的access_tokenjti声明或直接存储Token片段),在验证时额外检查。但这会引入一定的状态,需权衡利弊。

4.3 在常见框架中的实现要点

不同的后端框架对JWT的支持各有不同,但核心逻辑一致。

  • Node.js (Express):使用jsonwebtoken库进行签发和验证,使用express-jwt或自定义中间件进行拦截验证。
    // 签发 const jwt = require('jsonwebtoken'); const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' }); // 验证中间件 const authenticateJWT = (req, res, next) => { const authHeader = req.headers.authorization; if (authHeader) { const token = authHeader.split(' ')[1]; // Bearer <token> jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) return res.sendStatus(403); // Forbidden req.user = user; next(); }); } else { res.sendStatus(401); // Unauthorized } };
  • Java (Spring Boot):使用jjwt库,并结合Spring Security进行配置。通常需要自定义一个JwtAuthenticationFilter,放在安全过滤器链中。
  • Python (Django/Flask):对于Django,可以使用djangorestframework-simplejwt库。对于Flask,可以使用PyJWTFlask-JWT-Extended库。
  • Go (Gin):使用github.com/golang-jwt/jwt/v5库,编写一个Gin中间件来处理JWT验证。

关键点:无论用什么框架,密钥(secret)的管理都至关重要。绝对不要将密钥硬编码在代码中,而应该通过环境变量、配置中心或密钥管理服务来获取。

5. 常见问题、陷阱与排查指南

在实际开发和运维中,你会遇到各种各样与JWT相关的问题。下面是一些典型场景和排查思路。

5.1 典型错误与解决方案

问题现象可能原因排查步骤与解决方案
sign-in could not be completed token exchange failedtoken exchange failed: token endpoint returned status 403 forbidden1. 客户端请求Token的格式错误(如grant_type不对)。
2. 客户端身份无效(如OAuth中的client_id/secret错误)。
3. 用户凭证错误。
4. 服务器端认证服务配置问题或内部错误。
1. 检查客户端发送的Token请求体,确认所有必填字段(grant_type,client_id,client_secret,username,password等)存在且值正确。
2. 检查服务器日志,看认证服务是否抛出了更具体的错误信息(如“invalid_client”, “invalid_grant”)。
3. 确认认证服务端点(/oauth/token)的URL是否正确,网络是否可达。
invalid token1. Token格式错误(不是三段式,点分隔)。
2. Token已过期(exp时间已过)。
3. Token签名验证失败(密钥不匹配或Token被篡改)。
4. 算法不匹配(Header中声明的alg与服务器验证用的算法不一致)。
1. 将Token粘贴到 jwt.io 调试器,检查结构是否完整,手动解码Header和Payload。
2. 检查Payload中的exp字段,转换为本地时间看是否已过期。
3.最重要:确认服务器验证Token时使用的密钥(secret或公钥)与签发时使用的完全一致。在分布式部署中,确保所有实例的密钥同步。
4. 检查Header中的alg声明,确保服务器支持并使用该算法验证。
your access token could not be refreshed1. Refresh Token已过期。
2. Refresh Token已被服务器撤销(如用户登出)。
3. 用于刷新Token的请求格式错误或认证失败。
1. 检查Refresh Token的存储记录,看其是否已超过设置的TTL。
2. 检查数据库或Redis中,该用户对应的Refresh Token是否还存在。
3. 引导用户重新登录。
登录成功,但后续API请求返回401/4031. 客户端未正确在请求头中携带Token。
2. Token放置的位置不对(如放在了自定义头而非Authorization头)。
3. 跨域请求(CORS)时,服务器未正确设置Access-Control-Allow-Headers以允许Authorization头。
4. 前端路由守卫或Axios拦截器逻辑有误,未成功附加Token。
1. 打开浏览器开发者工具的“网络”选项卡,查看出错的请求,检查Request Headers中是否有Authorization: Bearer <token>
2. 确认后端鉴权中间件是从Authorization头中提取Token。
3. 检查后端CORS配置,确保包含了Authorization头。
4. 调试前端代码,确认在登录后Token被正确存储,并在拦截器中正确读取和附加。
Token泄露导致的安全问题1. Token存储在localStorage易受XSS攻击窃取。
2. Token通过不安全的HTTP传输被窃听。
3. 日志、错误信息中打印了完整的Token。
1. 考虑使用HttpOnly Cookie存储Token(防XSS),但需处理好CSRF防护。
2.强制使用HTTPS
3. 在服务器和客户端日志中,对Token进行脱敏处理(如只打印前几位)。
4. 缩短Access Token有效期,并使用Refresh Token机制。

5.2 调试与排查工具

  1. 在线JWT调试器: jwt.io 是最常用的工具,可以直观地解码、验证和调试JWT。你可以粘贴Token,查看Header和Payload,甚至修改内容后观察签名变化。
  2. 浏览器开发者工具Network标签页是查看HTTP请求/响应头、确认Token是否被正确携带和接收的利器。Application标签页可以查看localStorage/sessionStorage/Cookies中存储的Token。
  3. 命令行工具
    • 使用echo -n ‘<your-jwt>’ | cut -d ‘.’ -f 1 | base64 -d可以解码Header(Linux/macOS)。注意JWT是Base64Url编码,标准base64解码可能失败,需要替换字符或使用base64url工具。
    • 使用jq工具可以漂亮地打印解码后的JSON:echo -n ‘<payload-part>’ | base64 -d | jq .
  4. 后端日志:在鉴权中间件中增加详细的日志,记录Token验证的成功与失败原因(如过期、签名无效等),这是定位服务器端问题最快的方式。

5.3 关于“单点登录”与“令牌中转站”

从热词中可以看到“jwt实现单点登录详解”和“token中转站”这样的概念。这里简要说明一下:

  • 单点登录:JWT是实现SSO的一种优秀载体。在一个统一的认证中心(CAS)登录后,CAS签发一个全局的JWT(或一个授权码,用于换取JWT)。用户访问其他子系统时,携带这个Token,子系统通过向CAS验证或自行验证JWT签名来确认用户身份,无需再次登录。
  • Token中转站:在某些架构中(如后端即服务BaaS,或特定的API网关模式),客户端可能不直接向最终的业务服务器请求,而是先向一个“中转”或“代理”服务发送请求,该服务负责添加、刷新或转换Token,然后再转发给真正的业务服务器。这通常用于集中管理认证逻辑、适配不同的下游服务认证协议等。

JWT作为一种标准化的、自包含的、可验证的身份凭证,在这些复杂的认证授权场景中,因其无状态和易于传播的特性,发挥着核心作用。理解Token和JWT,是构建现代安全、可扩展应用架构的基石。从简单的登录验证,到复杂的微服务间认证、第三方授权,这套机制无处不在。掌握它,意味着你掌握了连接数字身份与业务服务的关键钥匙。

← 返回列表