JWT
定义与作用
JSON Web Token(JWT)是一种基于 JSON 的开放标准(RFC 7519),用于在各方之间安全传输声明(Claims)。与传统 Session 认证相比,JWT 具有天然的无状态特性,使其在分布式系统和微服务架构中成为首选的认证凭证载体。
Session 认证的痛点
| 痛点 | 说明 |
|---|---|
| 服务端存储压力 | 每个用户会话需在服务端存储 Session 数据,用户量增大时内存或数据库压力剧增 |
| 横向扩展困难 | 多实例部署时必须引入 Redis 等共享 Session 存储,增加架构复杂度 |
| CSRF 风险 | 基于 Cookie 的 Session 机制天然携带 Cookie,需额外配置 CSRF 防护 |
| 跨域限制 | Cookie 的同源策略使前后端分离架构下的跨域认证变得复杂 |
JWT 通过自包含的结构将用户身份和权限信息直接编码在 Token 中,服务端无需存储会话状态,仅凭 Token 自身即可完成身份验证。
核心原理
三部分结构
JWT 由 Header、Payload、Signature 三部分组成,以点号(.)连接:
Base64URL(Header).Base64URL(Payload).Base64URL(Signature)
Header(头部)
声明 Token 类型和签名算法:
{
"alg": "HS256",
"typ": "JWT"
}
常用算法:
HS256/HS512:HMAC 对称签名,密钥双方共享RS256/RS512:RSA 非对称签名,私钥签发、公钥验证ES256/ES512:ECDSA 非对称签名,密钥更短、性能更高
Payload(载荷)
承载 Claims(声明),即 Token 中包含的数据:
Registered Claims(注册声明):
| Claim | 含义 | 示例 |
|---|---|---|
iss | Issuer,签发者 | "iss": "auth-server" |
sub | Subject,主题(用户标识) | "sub": "user123" |
aud | Audience,受众 | "aud": "api-client" |
exp | Expiration Time,过期时间戳 | "exp": 1718000000 |
nbf | Not Before,生效时间戳 | "nbf": 1717900000 |
iat | Issued At,签发时间戳 | "iat": 1717900000 |
jti | JWT ID,唯一标识 | "jti": "uuid-xxx" |
Public / Private Claims:业务自定义声明,如 roles、userId、permissions 等。
Signature(签名)
防止 Token 被篡改。以 HS256 为例:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
RS256 场景下,授权服务器使用私钥签名,资源服务器使用公钥验证,私钥不外泄。
无状态特性
JWT 的无状态意味着服务端不保存 Token 副本,每次请求携带 Token,服务端通过签名验证和 Claims 解析即可确认身份。这一特性消除了会话存储,但也带来了无法主动吊销的局限——Token 在过期前始终有效。
签发与验证时序
示例一:观察 JWT 的完整结构
场景:手工拆解一个 JWT,理解三部分的具体内容。
操作:
在 jwt.io 调试工具中输入标准 JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyMTIzIiwiaXNzIjoiYXV0aC1zZXJ2ZXIiLCJleHAiOjE3MTgwMDAwMDAsInJvbGVzIjpbIlJPTEVfVVNFUiJdfQ.
SflKxwRJSMeKKF2QT4fwpMe...
结果分析:
| 部分 | Base64URL 解码后内容 |
|---|---|
| Header | {"alg":"HS256","typ":"JWT"} |
| Payload | {"sub":"user123","iss":"auth-server","exp":1718000000,"roles":["ROLE_USER"]} |
| Signature | HMACSHA256 计算结果,由前两部分 + 密钥生成 |
- 任何对 Header 或 Payload 的修改都会导致 Signature 验证失败
- Payload 中的
exp决定了 Token 的存活窗口,过期后资源服务器拒绝接受 roles是自定义 Private Claim,Spring Security 的JwtAuthenticationConverter可据此提取权限
示例二:从 Session 认证迁移到 JWT 无状态认证
场景:一个电商系统原有 Session 认证,需改造为前后端分离架构下的 JWT 认证。
操作前配置(Session 认证):
@EnableWebSecurity
public class SessionSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/public/**").permitAll()
.antMatchers("/api/orders/**").authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home")
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
}
问题分析:
- 用户登录后 Session 存储在服务端内存
- 订单服务部署 3 个实例,用户请求被负载均衡到不同实例时 Session 丢失
- 引入 Redis 解决共享 Session,增加运维成本
操作后配置(JWT 无状态认证):
@EnableWebSecurity
public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/orders/**").authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}
application.yml 配置:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth-server.example.com
结果分析:
sessionCreationPolicy(SessionCreationPolicy.STATELESS)禁用 Session 创建,服务端不再存储会话状态oauth2ResourceServer().jwt()启用 JWT 认证,从请求头Authorization: Bearer <token>解析 JWT- 三个实例均可独立验证 JWT 签名,无需共享 Session 存储
- 前端每次请求在 Header 中携带 Token,天然支持跨域
易错场景:JWT 中存放敏感信息 + 密钥泄露
面试高频考点
错误做法:将用户密码、银行卡号等敏感信息放入 JWT Payload,并使用弱密钥或硬编码密钥。
问题分析:
Payload 不是加密存储:JWT 的 Payload 仅做 Base64URL 编码,任何人可解码读取。以下结构看似安全,实则是明文暴露:
{ "sub": "user123", "password": "123456", "creditCard": "622202************" }攻击者截取 Token 后可直接解码获取敏感信息。
弱密钥导致签名伪造:
HS256使用短密钥或常见字符串(如"secret"、"123456")时,攻击者可通过字典暴力破解密钥,进而伪造任意 Token,冒充任意用户身份。无法主动撤销:JWT 一旦签发,在
exp过期前服务端无法强制作废。若 Token 泄露,攻击者可长期冒用,直到 Token 自然过期。
正确做法:
- Payload 只放用户标识、角色、权限等非敏感声明,禁止放入密码、Token、个人隐私数据
- 使用强随机密钥(HS256 推荐至少 256 位),生产环境通过环境变量或密钥管理服务注入,禁止硬编码
- 敏感数据应通过服务端根据
sub查询数据库获取 - 设置合理的
exp过期时间(通常 15 分钟 ~ 2 小时),配合 Refresh Token 机制实现长期会话