JWT认证方式详尽教程
原理、架构、实现与最佳实践
info JWT简介
help_outline 什么是JWT?
JWT (JSON Web Token) 是一种开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象安全地传输信息。这些信息可以被验证和信任,因为它是数字签名的。
JWT通常用于身份验证和信息交换,特别是在分布式系统中,因为它具有无状态特性,不需要服务器存储会话信息。
compare_arrows JWT与传统Session认证的区别
特性 | JWT认证 | Session认证 |
---|---|---|
状态存储 | 无状态,信息存储在令牌中 | 有状态,信息存储在服务器 |
扩展性 | 高,适合分布式系统 | 低,需要共享会话存储 |
跨域支持 | 良好,可放在请求头中 | 有限,依赖Cookie |
移动端支持 | 良好 | 有限,Cookie支持不完善 |
sync_disabled JWT的无状态特性
JWT最大的特点是其无状态性,服务器不需要存储任何会话信息。每个请求都包含了足够的信息,使服务端能够验证用户。这种特性带来了以下优势:
- 减轻服务器存储压力
- 提高系统可用性和伸缩性
- 更符合RESTful API的设计原则
- 便于实现分布式系统
architecture JWT结构
JWT由三部分组成,用点(.)分隔:Header.Payload.Signature
Header
描述JWT的元数据
Payload
包含声明(Claims)
Signature
验证数据完整性
title Header(头部)
Header通常由两部分组成:令牌的类型(typ)和所使用的签名算法(alg)。
{
"alg": "HS256",
"typ": "JWT"
}
然后,这个JSON对象会被Base64Url编码,形成JWT的第一部分。
inventory_2 Payload(载荷)
Payload是JWT的主体部分,包含声明(Claims)。声明是关于实体(通常是用户)和其他数据的声明。
声明分为三种类型:
- 注册声明(Registered Claims):预定义的一些声明,如iss(发行者)、exp(过期时间)、sub(主题)等
- 公共声明(Public Claims):自定义字段,可以用于交换信息
- 私有声明(Private Claims):自定义声明字段,只有JWT的创建者和使用者知晓
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"exp": 1516239022
}
Header和Payload只是经过Base64Url编码,并没有加密!任何人都可以解码它们并看到原始内容。因此,绝对不能在Payload中放置密码等敏感信息。
verified Signature(签名)
Signature是对前两部分的签名,用于验证JWT的完整性,防止数据被篡改。
签名的生成需要用到:
- 编码后的Header
- 编码后的Payload
- 一个密钥(Secret)
- Header中指定的签名算法
签名的计算公式如下:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
最终,将Header、Payload、Signature三个部分用点(.)连接起来,就构成了一个完整的JWT。
account_tree JWT工作流程
用户登录
提交用户名和密码
生成JWT
验证成功后生成令牌
存储JWT
客户端保存令牌
携带JWT
每次请求携带令牌
验证JWT
服务端验证令牌
login 用户登录和令牌生成
用户通过用户名和密码发起登录请求。服务器验证用户凭证,若验证成功,则使用JWT工具类生成令牌:
- Header:指定算法(如HS256)和令牌类型(JWT)
- Payload:包含用户信息(如用户ID、角色)和声明(如过期时间exp)
- Signature:使用密钥对Header和Payload进行签名,确保令牌不可篡改
save 客户端存储令牌
服务端将生成的JWT返回给客户端(通常通过响应体或Header)。客户端(如浏览器或移动端)将令牌存储在本地(如LocalStorage或Cookie)。
建议将JWT存储在localStorage中,放在Cookie中会有CSRF风险。如果必须使用Cookie,应设置httpOnly和secure标志。
send 请求携带令牌
客户端在后续请求的Authorization Header中以Bearer格式携带JWT:
Authorization: Bearer <JWT>
gpp_good 服务端验证令牌
服务器收到请求后,从请求头中提取JWT,并验证其合法性:
- 签名验证:使用密钥校验签名是否有效
- 过期检查:检查exp字段是否过期
- 用户信息提取:解析Payload中的用户信息(如用户ID),用于后续权限控制
若验证通过,服务端处理请求并返回数据;若验证失败(如令牌过期或签名错误),返回401状态码或自定义错误信息。
balance JWT优势与局限性
thumb_up 优势
- 无状态性:服务器不需要存储会话信息,减轻服务器压力
- 跨域支持:JWT可以放在HTTP请求头中,轻松实现跨域认证
- 移动端友好:不依赖Cookie,适合移动应用
- 避免CSRF攻击:不使用Cookie,天然避免CSRF攻击
- 自包含:JWT包含所有必要信息,减少数据库查询
- 可扩展:适合分布式系统和微服务架构
thumb_down 局限性
- 无法即时撤销:JWT一旦签发,在过期前无法撤销
- 信息泄露风险:Payload仅Base64编码,不应包含敏感信息
- 令牌体积大:相比Session ID,JWT体积更大,增加网络开销
- 续签困难:JWT过期时间固定,续签需要重新生成令牌
- 权限更新延迟:用户权限变更后,需等待令牌过期才能生效
security JWT安全考虑
vpn_key 密钥管理
- 使用强密钥(至少256位)
- 定期更换密钥
- 不要将密钥硬编码在代码中
- 使用环境变量或配置文件存储密钥
- 考虑使用密钥管理服务
visibility_off 敏感信息处理
- 不要在Payload中存储敏感信息(如密码、信用卡号)
- 如需传输敏感信息,考虑使用JWE(JSON Web Encryption)
- 最小化Payload中的信息量
timer 过期时间设置
- 设置合理的过期时间(通常15分钟到几小时)
- 对于长时间操作,使用刷新令牌机制
- 考虑在用户活动时自动延长令牌有效期
enhanced_encryption 算法选择
- 避免使用’none’算法
- 优先使用强算法如RS256(非对称)或HS256(对称)
- 考虑使用ES256(ECDSA)以获得更小的签名
- 根据安全需求选择合适的算法强度
code JWT实现示例
library_add Maven依赖配置
首先,在pom.xml文件中添加JWT相关依赖:
<!-- JWT Support -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
build JWT工具类实现
创建一个JWT工具类,用于生成和验证JWT令牌:
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private int jwtExpirationMs;
// 获取签名密钥
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtSecret.getBytes());
}
// 生成JWT令牌
public String generateToken(String username) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 从令牌中获取用户名
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
// 验证令牌
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
System.err.println("Invalid JWT signature");
} catch (MalformedJwtException ex) {
System.err.println("Invalid JWT token");
} catch (ExpiredJwtException ex) {
System.err.println("Expired JWT token");
} catch (UnsupportedJwtException ex) {
System.err.println("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
System.err.println("JWT claims string is empty.");
}
return false;
}
}
filter_alt 拦截器/过滤器实现
创建一个JWT过滤器,用于在Spring Security过滤器链中验证请求中的JWT:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.getUsernameFromToken(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
tips_and_updates JWT最佳实践
storage 令牌存储方式
- 对于Web应用,推荐使用localStorage存储JWT
- 如果使用Cookie,设置httpOnly和secure标志
- 考虑使用短期访问令牌和长期刷新令牌的组合
- 实现令牌黑名单机制,支持主动撤销
refresh 刷新令牌机制
- 使用短期访问令牌(如15分钟)和长期刷新令牌(如7天)
- 刷新令牌应存储在安全的地方(如httpOnly Cookie)
- 实现令牌刷新接口,在访问令牌过期时自动刷新
- 考虑实现滑动会话,在用户活动时延长令牌有效期
admin_panel_settings 权限控制
- 在JWT中包含角色和权限信息
- 实现基于角色的访问控制(RBAC)
- 考虑实现细粒度的权限控制
- 定期审查和更新用户权限
monitor_heart 监控与日志
- 记录JWT的生成、验证和失效事件
- 监控异常的JWT使用模式
- 实现令牌使用统计和分析
- 设置警报,检测潜在的安全威胁