Spring Boot中jwt的使用

记录一下现在我是怎么实现jwt的。为什么说是现在呢?说实话这是我第三次在Spring Boot里添加JWT,但是每次添加的方式和代码因为查阅的资料不同,实现的方式也不太一样。当然jwt终究是jwt,大同小异。

我用的版本比较新,所以很多方法都被标注了过期,我也没有去深入研究最新的代码究竟应该怎么实现。

Spring Boot版本是3.3.5, jjwt用的0.12.6。

引入依赖

用的是jjwt实现jwt,spring-security实现拦截。

在pom.xml里添加:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.6</version>
</dependency>

JwtUtil 文件

这是为了实现jwt而创建的工具类,建议放在项目文件夹的utils文件夹里,和controller文件夹同级。代码中的jwt.secret,jwt.expiration是写在application.properties里的变量。

1
2
jwt.secret=XXXXXXXXXXXXX
jwt.expiration=604800000

这里的jwt.secret必须满足一定的要求:HS256 算法要求签名密钥的大小至少为 256 位(即 32 字节)。并且写在文件里的是用Base64加密过的。Base64 编码的目的是让二进制密钥在传输和存储时更加安全和兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.airomance.easytravelroute.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;

@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;

@Value("${jwt.expiration}")
private long expirationTime;

private SecretKey getSecretKey() {
byte[] decodedKey = Base64.getDecoder().decode(secretKey);
return new SecretKeySpec(decodedKey, 0, decodedKey.length, "HmacSHA256");
}

public String generateToken(String username){
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSecretKey())
.compact();

}
// 从JWT中获取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

// 从JWT中获取任何信息
private <T> T extractClaim(String token, ClaimsResolver<T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.resolve(claims);
}

// 从JWT中提取所有声明
private Claims extractAllClaims(String token) {
return Jwts.parser()
.setSigningKey(getSecretKey())
.build()
.parseSignedClaims(token)
.getPayload();
}

// 验证JWT
public boolean validateToken(String token, String username) {
return (username.equals(extractUsername(token)) && !isTokenExpired(token));
}

// 检查JWT是否过期
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

// 提取JWT过期时间
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

@FunctionalInterface
private interface ClaimsResolver<T> {
T resolve(Claims claims);
}

}

JwtAuthenticationFilter 文件

JWT 认证过滤器 (JwtAuthenticationFilter),用于在每次 HTTP 请求时检查 JWT(JSON Web Token)是否有效,并在验证成功后设置用户身份认证信息。它是基于 Spring Security 的 OncePerRequestFilter 类,用于确保每个请求只被过滤一次。

  • 检查请求中的 JWT Token。
  • 验证 Token 的有效性和格式。
  • 如果 Token 有效,将用户信息设置到 Spring Security 的上下文中。
  • 如果请求在白名单路径中,则跳过 JWT 检查。
  • 如果 Token 无效或缺失,返回 HTTP 401 Unauthorized 错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package com.airomance.easytravelroute.filter;


import com.airomance.easytravelroute.utils.JwtUtil;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.logging.Logger;
import java.util.logging.Level;



public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;

private static final Logger logger = Logger.getLogger(JwtAuthenticationFilter.class.getName());
// 白名单路径列表
private static final String[] WHITE_LIST_PATHS = {"/users/loginByEmail"};

public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

// 如果请求是白名单路径,跳过 JWT 校验
if (isWhiteListed(request)) {
filterChain.doFilter(request, response); // 直接继续请求链,不做认证
return;
}
String token = getJwtFromRequest(request);

// 如果 token 不为空且有效,进行身份认证
if (token != null) {
try {
if (jwtUtil.validateToken(token, jwtUtil.extractUsername(token))) {
// 如果 JWT 有效,设置身份认证
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(jwtUtil.extractUsername(token), null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
// Token 无效,返回 401 错误
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid or expired JWT token");
return;
}
} catch (JwtException e) {
// 捕获解析异常,返回 401 错误
logger.log(Level.SEVERE, "Your error message here", e);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid JWT token format");
return;
}
}else {
// Token 缺失,返回 401 错误
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("JWT token is missing");
return;
}

// 继续过滤链
filterChain.doFilter(request, response);
}

// 从请求头部获取JWT Token
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
// 判断请求路径是否在白名单中
private boolean isWhiteListed(HttpServletRequest request) {
String requestURI = request.getRequestURI();
for (String path : WHITE_LIST_PATHS) {
if (requestURI.equals(path)) {
return true;
}
}
return false;
}
}

SecurityConfig文件

Spring Security 的配置类,用于定义应用的安全策略,包括认证机制、过滤器链、密码加密方式等。它通过 SecurityFilterChain 自定义安全规则,并使用 JwtAuthenticationFilter 来实现基于 JWT 的认证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.airomance.easytravelroute.config;


import com.airomance.easytravelroute.filter.JwtAuthenticationFilter;
import com.airomance.easytravelroute.utils.JwtUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

private final JwtUtil jwtUtil;

public SecurityConfig(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 添加自定义的 JwtAuthenticationFilter
http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests(authorizeRequests -> authorizeRequests
.requestMatchers("/users/loginByEmail").permitAll() // 登录和注册不需要认证
.anyRequest().authenticated() // 其他请求需要认证
)
.csrf(csrf -> csrf.disable()); // 禁用 CSRF
return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

缺点和不足

现在可以看到白名单需要在两个文件里都写一遍,原因是我想要在需要token的时候如果没有tokne的请求返回”JWT token is missing”,但是我发现JwtAuthenticationFilter如果不加入白名单判断,就会把所有没有token的都拦截了。

而且没有暂时没有加入角色控制,因为相关接口还没有写。