Spring Boot项目中成功集成了JWT
JWT 原理解释
什么是 JWT?
JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在网络应用环境间安全地将信息作为JSON对象传输。JWT通常用于身份验证和信息交换。
JWT 的结构
JWT由三部分组成,以点.
分隔:
- Header(头部):包含令牌的类型(
typ
)和签名算法(alg
)。 - Payload(负载):包含声明(claims),即实体(通常是用户)和其他数据。
- Signature(签名):通过将Base64Url编码的Header和Payload连接起来,并使用密钥进行签名得到。
生成与验证流程
-
生成JWT:
- 构建Header和Payload。
- 使用指定的签名算法和密钥生成签名。
- 将Header、Payload和Signature拼接成JWT字符串。
-
验证JWT:
- 解码Header和Payload。
- 根据Header中的签名算法和提供的密钥重新计算签名。
- 比较新计算的签名与JWT中的签名是否一致。
实现步骤
以下是详细的代码实现步骤,包括生成密钥、配置JWT、创建工具类和控制器,并加入JWT自动续期功能。
1. 准备工作
添加依赖
首先,在 pom.xml
中添加 com.auth0
的 java-jwt
依赖:
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Auth0 JWT Library -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.0</version>
</dependency>
</dependencies>
2. 生成密钥文件
对称密钥(HMAC)
我们可以使用 Java 代码生成一个对称密钥并保存到文件中:
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.SecureRandom;
public class KeyGenerator {
public static void main(String[] args) throws Exception {
// 定义一个32字节的数组用于存储密钥
byte[] secretKey = new byte[32];
// 使用强随机数生成器填充密钥数组
SecureRandom.getInstanceStrong().nextBytes(secretKey);
// 将生成的密钥写入名为 jwt_secret.key 的文件
Files.write(Paths.get("jwt_secret.key"), secretKey);
// 输出提示信息
System.out.println("密钥已生成并保存到 jwt_secret.key 文件中");
}
}
非对称密钥(RSA)
使用 OpenSSL 生成 RSA 密钥对,并将其保存到文件中:
# 生成私钥
openssl genpkey -algorithm RSA -out private.key -aes256 -pass pass:your_password -pkeyopt rsa_keygen_bits:2048
# 从私钥中提取公钥
openssl rsa -pubout -in private.key -out public.key -passin pass:your_password
3. 在Spring Boot中集成JWT
创建配置类
加载密钥文件并配置 JWT 相关的参数。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@Configuration
public class JwtConfig {
/**
* 读取对称密钥文件
* @return 密钥字符串
* @throws IOException 如果读取文件失败
*/
@Bean
public String getSecretKey() throws IOException {
// 从 jwt_secret.key 文件读取密钥字节数组
byte[] keyBytes = Files.readAllBytes(Paths.get("jwt_secret.key"));
// 使用Base64编码将字节数组转换为字符串
return Base64.getEncoder().encodeToString(keyBytes);
}
/**
* 读取私钥文件
* @return 私钥对象
* @throws Exception 如果解析私钥失败
*/
@Bean
public PrivateKey getPrivateKey() throws Exception {
// 从 private.key 文件读取私钥字节数组
byte[] keyBytes = Files.readAllBytes(Paths.get("private.key"));
// 创建PKCS8EncodedKeySpec对象
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
// 获取RSA算法的KeyFactory实例
KeyFactory kf = KeyFactory.getInstance("RSA");
// 生成私钥对象
return kf.generatePrivate(spec);
}
/**
* 读取公钥文件
* @return 公钥对象
* @throws Exception 如果解析公钥失败
*/
@Bean
public PublicKey getPublicKey() throws Exception {
// 从 public.key 文件读取公钥字节数组
byte[] keyBytes = Files.readAllBytes(Paths.get("public.key"));
// 创建X509EncodedKeySpec对象
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
// 获取RSA算法的KeyFactory实例
KeyFactory kf = KeyFactory.getInstance("RSA");
// 生成公钥对象
return kf.generatePublic(spec);
}
}
创建JWT工具类
实现 JWT 的生成、验证和自动续期功能。
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
@Component
public class JwtUtil {
@Autowired
private String secretKey; // 对称密钥
@Autowired
private PrivateKey privateKey; // 私钥
@Autowired
private PublicKey publicKey; // 公钥
private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1小时
private static final long REFRESH_THRESHOLD = 1000 * 60 * 55; // 55分钟
/**
* 使用对称密钥生成JWT
* @param username 用户名
* @return 生成的JWT字符串
*/
public String generateToken(String username) {
// 使用对称密钥创建算法实例
Algorithm algorithm = Algorithm.HMAC256(secretKey);
// 创建JWT构建器
return JWT.create()
// 设置主题为用户名
.withSubject(username)
// 设置签发时间
.withIssuedAt(new Date())
// 设置过期时间为1小时后
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
// 使用指定算法签名并生成JWT字符串
.sign(algorithm);
}
/**
* 使用私钥生成JWT
* @param username 用户名
* @return 生成的JWT字符串
*/
public String generateTokenWithRsa(String username) {
// 使用RSA密钥创建算法实例
Algorithm algorithm = Algorithm.RSA256((com.auth0.jwt.impl.crypto.RSAPublicKey) publicKey, (com.auth0.jwt.impl.crypto.RSAPrivateKey) privateKey);
// 创建JWT构建器
return JWT.create()
// 设置主题为用户名
.withSubject(username)
// 设置签发时间
.withIssuedAt(new Date())
// 设置过期时间为1小时后
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
// 使用指定算法签名并生成JWT字符串
.sign(algorithm);
}
/**
* 验证JWT(使用对称密钥)
* @param token 要验证的JWT字符串
* @return 解析后的DecodedJWT对象
*/
public DecodedJWT validateToken(String token) {
// 使用对称密钥创建算法实例
Algorithm algorithm = Algorithm.HMAC256(secretKey);
// 创建验证器并构建验证器
return JWT.require(algorithm)
.build()
// 验证并返回解码后的JWT
.verify(token);
}
/**
* 使用公钥验证JWT
* @param token 要验证的JWT字符串
* @return 解析后的DecodedJWT对象
*/
public DecodedJWT validateTokenWithRsa(String token) {
// 使用公钥创建算法实例
Algorithm algorithm = Algorithm.RSA256((com.auth0.jwt.impl.crypto.RSAPublicKey) publicKey, null);
// 创建验证器并构建验证器
return JWT.require(algorithm)
.build()
// 验证并返回解码后的JWT
.verify(token);
}
/**
* 判断JWT是否需要刷新
* @param decodedJWT 解码后的JWT
* @return 是否需要刷新
*/
public boolean shouldRefresh(DecodedJWT decodedJWT) {
// 获取JWT的过期时间
Date expiresAt = decodedJWT.getExpiresAt();
// 计算当前时间和过期时间的差值
long timeLeft = expiresAt.getTime() - System.currentTimeMillis();
// 如果剩余时间小于刷新阈值,则返回true,表示需要刷新
return timeLeft <= REFRESH_THRESHOLD;
}
/**
* 刷新JWT
* @param username 用户名
* @return 新的JWT字符串
*/
public String refreshToken(String username) {
// 使用对称密钥创建算法实例
Algorithm algorithm = Algorithm.HMAC256(secretKey);
// 创建JWT构建器
return JWT.create()
// 设置主题为用户名
.withSubject(username)
// 设置签发时间
.withIssuedAt(new Date())
// 设置过期时间为1小时后
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
// 使用指定算法签名并生成JWT字符串
.sign(algorithm);
}
}
创建过滤器
在请求到达控制器之前检查JWT的有效性,并根据需要刷新JWT。
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Autowired;
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 {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 从请求头中获取Authorization字段
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
// 如果没有Authorization字段或格式不正确,直接放行
filterChain.doFilter(request, response);
return;
}
try {
// 提取JWT字符串
String token = header.substring(7);
// 验证JWT
DecodedJWT decodedJWT = jwtUtil.validateToken(token);
// 判断是否需要刷新JWT
if (jwtUtil.shouldRefresh(decodedJWT)) {
// 获取用户名
String username = decodedJWT.getSubject();
// 刷新JWT
String newToken = jwtUtil.refreshToken(username);
// 将新的JWT放入响应头
response.setHeader("New-Token", newToken);
}
// 放行请求
filterChain.doFilter(request, response);
} catch (Exception e) {
// 处理异常情况,如无效的JWT
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid JWT");
}
}
}
创建控制器
提供 API 接口来测试 JWT 的生成、验证和刷新功能。
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class JwtController {
@Autowired
private JwtUtil jwtUtil;
/**
* 生成JWT(使用对称密钥)
* @param username 用户名
* @return 生成的JWT字符串
*/
@GetMapping("/generate-token")
public String generateToken(@RequestParam String username) {
// 调用JwtUtil生成JWT
return jwtUtil.generateToken(username);
}
/**
* 使用私钥生成JWT
* @param username 用户名
* @return 生成的JWT字符串
*/
@GetMapping("/generate-token-rsa")
public String generateTokenWithRsa(@RequestParam String username) {
// 调用JwtUtil使用私钥生成JWT
return jwtUtil.generateTokenWithRsa(username);
}
/**
* 验证JWT(使用对称密钥)
* @param request 包含要验证的JWT字符串的请求体
* @return 解析后的DecodedJWT对象
*/
@PostMapping("/validate-token")
public DecodedJWT validateToken(@RequestBody TokenRequest request) {
// 调用JwtUtil验证JWT
return jwtUtil.validateToken(request.getToken());
}
/**
* 使用公钥验证JWT
* @param request 包含要验证的JWT字符串的请求体
* @return 解析后的DecodedJWT对象
*/
@PostMapping("/validate-token-rsa")
public DecodedJWT validateTokenWithRsa(@RequestBody TokenRequest request) {
// 调用JwtUtil使用公钥验证JWT
return jwtUtil.validateTokenWithRsa(request.getToken());
}
}
/**
* 请求体类,用于接收包含JWT字符串的请求
*/
class TokenRequest {
private String token;
public String getToken() {
// 返回token字段值
return token;
}
public void setToken(String token) {
// 设置token字段值
this.token = token;
}
}
4. 完整的Spring Boot启动类
为了完整性,这里是一个简单的 Spring Boot 启动类,并注册过滤器:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class JwtDemoApplication {
public static void main(String[] args) {
// 运行Spring Boot应用
SpringApplication.run(JwtDemoApplication.class, args);
}
@Bean
public FilterRegistrationBean<JwtAuthenticationFilter> loggingFilter(){
FilterRegistrationBean<JwtAuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtAuthenticationFilter());
registrationBean.addUrlPatterns("/api/*");
return registrationBean;
}
}
测试API
生成JWT
-
使用对称密钥生成JWT:
GET /api/generate-token?username=john_doe
-
使用私钥生成JWT:
GET /api/generate-token-rsa?username=john_doe
验证JWT
-
使用对称密钥验证JWT:
POST /api/validate-token Content-Type: application/json { "token": "your_jwt_token" }
-
使用公钥验证JWT:
POST /api/validate-token-rsa Content-Type: application/json { "token": "your_jwt_token" }
自动续期
当JWT接近过期时(例如剩余有效期少于55分钟),过滤器会在响应头中返回一个新的JWT,客户端可以从中获取新的JWT并更新本地存储的JWT。
自动续期原理
工作流程
- 检查JWT有效期:每次请求到来时,过滤器会从请求头中提取JWT,并解析出其有效期。
- 判断是否需要续期:如果JWT的有效期剩余时间小于设定的阈值(例如55分钟),则认为该JWT即将过期,需要续期。
- 生成新JWT:调用
JwtUtil
类中的refreshToken
方法生成一个新的JWT,并将其放入响应头中返回给客户端。 - 更新客户端JWT:客户端接收到带有新JWT的响应后,更新本地存储的JWT。
关键代码解释
判断是否需要续期
public boolean shouldRefresh(DecodedJWT decodedJWT) {
// 获取JWT的过期时间
Date expiresAt = decodedJWT.getExpiresAt();
// 计算当前时间和过期时间的差值
long timeLeft = expiresAt.getTime() - System.currentTimeMillis();
// 如果剩余时间小于刷新阈值,则返回true,表示需要刷新
return timeLeft <= REFRESH_THRESHOLD;
}
此方法计算JWT的剩余有效时间,如果剩余时间小于设定的阈值(如55分钟),则返回 true
表示需要刷新。
刷新JWT
public String refreshToken(String username) {
// 使用对称密钥创建算法实例
Algorithm algorithm = Algorithm.HMAC256(secretKey);
// 创建JWT构建器
return JWT.create()
// 设置主题为用户名
.withSubject(username)
// 设置签发时间
.withIssuedAt(new Date())
// 设置过期时间为1小时后
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
// 使用指定算法签名并生成JWT字符串
.sign(algorithm);
}
此方法重新生成一个新的JWT,并设置其过期时间为1小时后。客户端在收到新的JWT后应更新本地存储的JWT。
过滤器处理逻辑
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 从请求头中获取Authorization字段
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
// 如果没有Authorization字段或格式不正确,直接放行
filterChain.doFilter(request, response);
return;
}
try {
// 提取JWT字符串
String token = header.substring(7);
// 验证JWT
DecodedJWT decodedJWT = jwtUtil.validateToken(token);
// 判断是否需要刷新JWT
if (jwtUtil.shouldRefresh(decodedJWT)) {
// 获取用户名
String username = decodedJWT.getSubject();
// 刷新JWT
String newToken = jwtUtil.refreshToken(username);
// 将新的JWT放入响应头
response.setHeader("New-Token", newToken);
}
// 放行请求
filterChain.doFilter(request, response);
} catch (Exception e) {
// 处理异常情况,如无效的JWT
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid JWT");
}
}
此过滤器负责在每个请求到来时检查JWT的有效性,并在需要时刷新JWT。
注意事项
-
密钥管理:
- 对称密钥应妥善保管,避免泄露。
- 非对称密钥中的私钥应严格保密,公钥可以公开。
-
安全性:
- 设置合理的过期时间,防止长时间有效的令牌被滥用。
- 使用HTTPS传输JWT,防止中间人攻击。
-
错误处理:
- 在验证JWT时捕获异常,处理无效或过期的令牌。
-
性能优化:
- 缓存解析后的Claims对象,减少重复解析开销。
通过上述步骤,我们已经在Spring Boot项目中成功集成了JWT,并实现了自动续期功能。