Java:JWT 从原理到高频面试题解析
目录
一、什么是 JWT?
二、JWT 的结构
1. Header(头部)
2. Payload(载荷)
3. Signature(签名)
三、Java 中实现 JWT(基于 JJWT 库)
1. 引入依赖(Maven)
2. JWT 工具类实现
四、JWT 在 Java Web 中的应用流程
五、JWT 的优缺点及注意事项
优点:
缺点:
注意事项:
六、Java 秋招 JWT 高频面试题
1. JWT 由哪几部分组成?各部分作用是什么?
2. JWT 和 Session 认证的区别是什么?
3. 如何处理 JWT 令牌过期问题?
4. JWT 的 Payload 可以存储敏感信息吗?为什么?
5. JWT 的签名算法 HS256 和 RS256 有什么区别?
6. 如何防止 JWT 令牌被篡改?
7. JWT 的缺点是什么?如何规避?
总结
一、什么是 JWT?
JWT(JSON Web Token)是一种基于 JSON 的轻量级令牌格式,用于在网络应用间安全传递声明信息。它通过数字签名保证信息的完整性和真实性,常用于身份认证、授权和信息交换场景。
与传统的 Session 认证相比,JWT 具有无状态特性:服务器无需存储会话信息,只需通过令牌本身即可验证用户身份,非常适合分布式系统和微服务架构。
二、JWT 的结构
JWT 由三部分组成,用英文句号(.
)分隔,格式为:Header.Payload.Signature
,三部分均通过 Base64URL 编码(便于在 URL 中传输)。
1. Header(头部)
声明令牌类型(typ: "JWT"
)和签名算法(如HS256
、RS256
等)。
{"alg": "HS256", // 签名算法:HMAC SHA-256"typ": "JWT" // 令牌类型
}
2. Payload(载荷)
存储需要传递的声明信息(Claims),分为三种类型:
- 注册声明(标准字段,可选):
iss
(签发者)、exp
(过期时间,时间戳)、sub
(主题)、iat
(签发时间)等。 - 公共声明:自定义字段(需避免与标准字段冲突)。
- 私有声明:服务端与客户端协商的自定义业务字段(如用户 ID、角色)。
{"iss": "backend-server", // 签发者"exp": 1690848000, // 过期时间(2023-08-01 00:00:00)"sub": "user-auth", // 主题"userId": 1001, // 私有声明:用户ID"role": "admin" // 私有声明:用户角色
}
⚠️ 注意:Payload 仅经过 Base64 编码(可解码),不加密,因此禁止存储敏感信息(如密码)。
3. Signature(签名)
JWT 的安全核心,用于验证令牌是否被篡改。生成规则:
- 用 Header 中指定的算法,将编码后的 Header、编码后的 Payload,以及服务器端密钥(secret)进行加密。
- 公式(以 HS256 为例):
plaintext
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret )
三、Java 中实现 JWT(基于 JJWT 库)
JJWT(Java JWT)是 Java 生态中常用的 JWT 处理库,以下是完整实现示例。
1. 引入依赖(Maven)
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope>
</dependency>
2. JWT 工具类实现
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;public class JwtUtils {// 密钥(实际开发中需从配置文件读取,且确保足够复杂)private static final String SECRET_KEY = "your-256-bit-secret-key-which-should-be-very-long-and-secure";// 令牌过期时间(3600秒 = 1小时)private static final long EXPIRATION_TIME = 3600 * 1000;// 生成密钥(基于HS256算法,需要256位密钥)private static SecretKey getSigningKey() {byte[] keyBytes = SECRET_KEY.getBytes();return Keys.hmacShaKeyFor(keyBytes);}// 生成JWT令牌public static String generateToken(String username, Map<String, Object> claims) {Date now = new Date();Date expirationDate = new Date(now.getTime() + EXPIRATION_TIME);return Jwts.builder().setClaims(claims) // 自定义声明.setSubject(username) // 主题(如用户名).setIssuedAt(now) // 签发时间.setExpiration(expirationDate) // 过期时间.signWith(getSigningKey(), SignatureAlgorithm.HS256) // 签名.compact();}// 从令牌中获取用户名(主题)public static String getUsernameFromToken(String token) {return getClaimFromToken(token, Claims::getSubject);}// 验证令牌是否有效(未过期且签名正确)public static boolean validateToken(String token, String username) {String tokenUsername = getUsernameFromToken(token);return (username.equals(tokenUsername) && !isTokenExpired(token));}// 从令牌中获取指定声明private static <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {Claims claims = getAllClaimsFromToken(token);return claimsResolver.apply(claims);}// 解析令牌获取所有声明private static Claims getAllClaimsFromToken(String token) {return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();}// 判断令牌是否过期private static boolean isTokenExpired(String token) {Date expiration = getClaimFromToken(token, Claims::getExpiration);return expiration.before(new Date());}// 测试方法public static void main(String[] args) {// 自定义声明(如用户角色、ID)Map<String, Object> claims = new HashMap<>();claims.put("userId", 1001);claims.put("role", "admin");// 生成令牌String token = generateToken("zhangsan", claims);System.out.println("生成的JWT令牌:" + token);// 解析令牌String username = getUsernameFromToken(token);Integer userId = getClaimFromToken(token, claims1 -> claims1.get("userId", Integer.class));System.out.println("解析结果:用户名=" + username + ",userId=" + userId);// 验证令牌boolean isValid = validateToken(token, "zhangsan");System.out.println("令牌是否有效:" + isValid);}
}
四、JWT 在 Java Web 中的应用流程
以 Spring Boot 身份认证为例:
-
用户登录:
- 客户端提交用户名和密码。
- 服务端验证通过后,调用
JwtUtils.generateToken()
生成 JWT。 - 服务端将 JWT 返回给客户端(客户端通常存储在
localStorage
或Cookie
中)。
-
客户端请求资源:
- 客户端在 HTTP 请求头中携带 JWT:
Authorization: Bearer <token>
。
- 客户端在 HTTP 请求头中携带 JWT:
-
服务端验证:
- 服务端通过拦截器 / 过滤器提取请求头中的 JWT。
- 调用
JwtUtils.validateToken()
验证令牌有效性。 - 验证通过则允许访问,否则返回 401(未授权)。
五、JWT 的优缺点及注意事项
优点:
- 无状态:服务器无需存储会话,减轻服务器压力,适合分布式系统。
- 自包含:Payload 可携带用户信息,减少数据库查询。
- 跨域支持:可在不同域名间传递,适合前后端分离架构。
缺点:
- 无法作废已颁发的令牌:一旦生成,在过期前始终有效(除非服务端维护黑名单)。
- Payload 无加密:敏感信息不能存储在 Payload 中。
注意事项:
- 密钥必须安全存储(如配置中心),避免泄露。
- 令牌过期时间不宜过长(建议 1 小时内),降低被盗用风险。
- 传输需通过 HTTPS,防止令牌被拦截。
- 避免在 Payload 中存储敏感信息(如密码、token 等)。
六、Java 秋招 JWT 高频面试题
1. JWT 由哪几部分组成?各部分作用是什么?
答:JWT 由 Header、Payload、Signature 三部分组成。
- Header:声明令牌类型和签名算法。
- Payload:存储声明信息(如用户 ID、过期时间),仅 Base64 编码,不加密。
- Signature:通过密钥和算法对前两部分签名,用于验证令牌完整性和真实性。
2. JWT 和 Session 认证的区别是什么?
答:
- 存储位置:Session 存储在服务端,JWT 存储在客户端。
- 状态性:Session 是有状态的(服务端需维护会话),JWT 是无状态的(服务端无需存储)。
- 分布式支持:Session 需考虑共享(如 Redis),JWT 天然支持分布式。
- 安全性:Session 依赖 Cookie,可能遭遇 CSRF 攻击;JWT 需注意密钥安全和 HTTPS 传输。
3. 如何处理 JWT 令牌过期问题?
答:
- 方案 1:设置合理的过期时间(如 1 小时),过期后要求用户重新登录。
- 方案 2:使用 “双令牌机制”:
- 访问令牌(Access Token):短期有效(如 30 分钟),用于接口访问。
- 刷新令牌(Refresh Token):长期有效(如 7 天),用于过期后重新获取访问令牌。
4. JWT 的 Payload 可以存储敏感信息吗?为什么?
答:不可以。因为 Payload 仅经过 Base64 编码(可解码为明文),未加密,敏感信息(如密码、银行卡号)会被泄露。
5. JWT 的签名算法 HS256 和 RS256 有什么区别?
答:
- HS256:对称加密算法,签名和验证使用同一个密钥,需确保密钥在服务端安全存储。
- RS256:非对称加密算法,使用私钥签名、公钥验证,适合多服务间共享验证(只需分发公钥)。
6. 如何防止 JWT 令牌被篡改?
答:通过 Signature 部分保证。服务端生成令牌时用密钥签名,验证时重新计算签名并与令牌中的 Signature 比对,若不一致则说明令牌被篡改。
7. JWT 的缺点是什么?如何规避?
答:
- 缺点 1:令牌生成后无法主动作废(除非过期)。
规避:维护令牌黑名单(如 Redis 存储已注销的令牌),验证时检查黑名单。 - 缺点 2:Payload 无加密,敏感信息易泄露。
规避:不在 Payload 中存储敏感信息,必要时对 Payload 单独加密。
总结
JWT 是 Java 开发中处理身份认证和信息传递的重要工具,其无状态特性使其在分布式系统中极具优势。掌握 JWT 的原理、实现及安全注意事项,不仅能提升项目开发能力,也是秋招面试中的高频考点。实际开发中需结合业务场景合理使用,平衡安全性和便利性。