看了一圈,发现不少代码已经有些过时且不完整,所以索性再写一版供大家参考了。
1. 技术栈 及开发环境
- Spring Boot 3.3.9
- Spring Security 6.3.7
- JJWT 0.12.5
- Lombok 1.18.30
- JDK 17 或更高版本
- Maven 或 Gradle
2. 配置 JWT 工具类
生成及解析 JWT (基于0.12.5 版本的 jjwt API)
代码示例
配置文件
# application.properties
jwt.secret-key=my-ultra-secure-secret-key-1234567890
jwt.expiration-time=86400000
JwtUtil
类的实现:
package com.example.demo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Setter
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtUtil {
// @Setter in class needed if no @Value here
private String secretKey;
private long expirationTime;
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
// 1. 将 SECRET KEY 转换为 Key 对象
SecretKey signingKey = Keys.hmacShaKeyFor(secretKey.getBytes());
return Jwts.builder()
.claims(claims)
.subject(username)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(signingKey)
.compact();
}
public String extractUsername(String token) {
return extractClaims(token).getSubject();
}
public Date extractExpiration(String token) {
return extractClaims(token).getExpiration();
}
private Claims extractClaims(String token) {
// 1. 将 SECRET KEY 转换为 Key 对象
SecretKey signingKey = Keys.hmacShaKeyFor(secretKey.getBytes());
// 2. 使用新 API 解析 Token
return Jwts.parser().verifyWith(signingKey).build().parseSignedClaims(token).getPayload();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Boolean validateToken(String token, String username) {
final String extractedUsername = extractUsername(token);
return (extractedUsername.equals(username) && !isTokenExpired(token));
}
}
3. 创建用户实体和UserDetails实现
用户实体类
@Data
public class User {
// 此两个字段必须,其他视情况添加
private String username;
private String password;
}
UserDetails的实现类,用于验证
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.example.demo.entity.User;
import java.util.Collection;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public String getPassword() {
return user.getPassword();
}
}
4. 创建UserDetails的服务
实现 UserDetailsService
接口用于加载用户信息
package com.example.demo.service;
import com.example.demo.domain.LoginUser;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库加载用户信息的逻辑
User user = userMapper.findByUsername(username);
if (Objects.isNull(user))
throw new UsernameNotFoundException("用户不存在");
// 实际将由 DaoAuthenticationProvider 验证此返回的UserDetails的账号密码
LoginUser loginUser = new LoginUser(user);
return loginUser;
}
}
数据访问可用MyBatis或JPA等实现 User findByUsername(String username);
5. 创建 JWT 请求过滤器
创建一个自定义过滤器用于解析 JWT 并将其设置到 Spring Security 上下文中
package com.example.demo.security;
import com.example.demo.service.CustomUserDetailsService;
import com.example.demo.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
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.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
chain.doFilter(request, response);
}
}
6. 配置 Spring Security
创建 SecurityConfig
类以配置 Spring Security,使其支持 JWT 认证
package com.example.demo.config;
import com.example.demo.exception.DelegateAccessDeniedHandler;
import com.example.demo.exception.DelegateAuthenticationEntryPoint;
import com.example.demo.security.JwtRequestFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
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
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtRequestFilter jwtRequestFilter;
public SecurityConfig(JwtRequestFilter jwtRequestFilter) {
this.jwtRequestFilter = jwtRequestFilter;
}
// 将 Exception Handler 委托给 HandlerExceptionResolver 以便仍然使用全局异常处理, 当然亦可选择使用自定义
@Bean
public DelegateAccessDeniedHandler delegateAccessDeniedHandler() {
return new DelegateAccessDeniedHandler();
}
@Bean
public DelegateAuthenticationEntryPoint delegateAuthenticationEntryPoint() {
return new DelegateAuthenticationEntryPoint();
}
@Bean
// After 5.7的写法
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/token").permitAll()
.requestMatchers("/api/test").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
// 自定义异常处理,也可使用其委托回全局异常处理
http.exceptionHandling(configurer -> {
configurer
.accessDeniedHandler(delegateAccessDeniedHandler())
.authenticationEntryPoint(delegateAuthenticationEntryPoint());
});
return http.build();
}
// 鉴权
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config
) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); //安全且广泛使用的加密算法
}
}
7. 异常处理 (6中的实现类)
package com.example.demo.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerExceptionResolver;
import java.io.IOException;
// 将 Exception Handler 委托给 HandlerExceptionResolver
public class DelegateAccessDeniedHandler implements AccessDeniedHandler {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException, ServletException {
resolver.resolveException(request, response, null, accessDeniedException);
}
}
package com.example.demo.exception;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.web.servlet.HandlerExceptionResolver;
// 将 Exception Handler 委托给 HandlerExceptionResolver
public class DelegateAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
resolver.resolveException(request, response, null, authException);
}
}
此统一的全局异常处理可能较为简洁
package com.example.demo.exception;
import com.example.demo.pojo.Result;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.StringUtils;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import java.util.List;
import java.util.stream.Collectors;
/*
Spring Security 核心异常(如 AuthenticationException 和 AccessDeniedException)属于运行时异常。
由于这些异常是由 DispatcherServlet 后面的 Authentication Filter 在调用 Controller 方法之前抛出的,
因此默认情况下 @RestControllerAdvice 无法捕获这些异常。
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public Result<String> handleAccessDeniedException(AccessDeniedException e, HttpServletResponse response) {
response.setStatus(HttpStatus.FORBIDDEN.value());
String errorMsg = e.getMessage();
return Result.failure(
StringUtils.hasLength(errorMsg) ? errorMsg : "Access Denied!"
);
}
@ExceptionHandler(AuthenticationException.class)
public Result<String> handleAuthenticationException(AuthenticationException e, HttpServletResponse response) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
String errorMsg = e.getMessage();
return Result.failure(
StringUtils.hasLength(errorMsg) ? errorMsg : "Authentication Failed!"
);
}
@ExceptionHandler({HttpRequestMethodNotSupportedException.class})
public Result<String> handleBadRequestExceptions(Exception e, HttpServletResponse response) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
String errorMsg = e.getMessage();
return Result.failure(
StringUtils.hasLength(errorMsg) ? errorMsg : "Unsupported HTTP Method!"
);
}
// 其他所有异常将会走到这里, 在此之前定义其他需要的异常处理
@ExceptionHandler(Exception.class)
public Result<String> handleAllExceptions(Exception e, HttpServletResponse response) {
e.printStackTrace();
String message = e.getMessage();
response.setStatus(500);
return Result.failure(
StringUtils.hasLength(message) ? message : "Fatal error!"
);
}
}
8. 创建认证接口
创建一个控制器用于处理用户登录并生成 JWT
首先创建请求及返回值的POJO
package com.example.demo.pojo;
import lombok.Data;
@Data
public class AuthRequest {
private String grantType;
private String username;
private String password;
}
package com.example.demo.pojo;
import lombok.Data;
@Data
public class AuthToken {
private String token;
}
package com.example.demo.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// 统一响应结果
@NoArgsConstructor
@AllArgsConstructor
@Data //用于转换为JSON对象
public class Result<T> {
private Integer code;
private String message;
private T data;
// public static final Map<Integer, String> SUCCESS = new HashMap<>(1);
public static final Integer SUCCESS = 0;
public static final Integer FAILURE = 1;
public static <E> Result<E> success(E data) {
return new Result<>(SUCCESS, "操作成功", data);
}
public static <E> Result<E> failure(String message) {
return new Result<>(FAILURE, message, null);
}
}
package com.example.demo.controller;
import com.example.demo.pojo.AuthRequest;
import com.example.demo.pojo.AuthToken;
import com.example.demo.pojo.Result;
import com.example.demo.service.CustomUserDetailsService;
import com.example.demo.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@Slf4j
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/token")
public Result<AuthToken> token(@RequestBody AuthRequest authRequest) {
// AuthenticationManager 将调用 UserDetailsService 的"实现类"来加载用户信息和验证密码。
Authentication authenticate = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authRequest.getUsername(),
authRequest.getPassword()
)
);
if (Objects.isNull(authenticate)) {
throw new RuntimeException("验证失败");
}
// 返回Token
AuthToken authToken = new AuthToken();
authToken.setToken(jwtUtil.generateToken(authRequest.getUsername()));
return Result.success(authToken);
}
}
9. 测试代码
使用一个测试类添加测试用户数据
@SpringBootTest
public class DummyDataTest {
@Autowired
private UserMapper userMapper;
@Test
public void testInsertUser() {
// 创建一个 User 对象
User user = new User();
user.setUsername("testuser");
user.setPassword("123");
// 插入用户
userMapper.insert(user);
// 验证插入是否成功
assertNotNull(user.getId(), "插入后用户 ID 不应为空");
}
}
测试接口
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class TestController {
@GetMapping(value = "/test")
public String test() {
return "this is a test";
}
@GetMapping(value = "/testauth")
public String testAuth() {
return "this is a test for auth";
}
}
10. 测试
使用Postman之类的发送HTTP请求,获取Token
POST http://127.0.0.1:8080/api/auth/token
Content-Type: application/json
Payload:
{
"username":"testuser",
"password":"123"
}
不验证Token的API
GET http://127.0.0.1:8080/api/test
需验证Header中的Bearer Token的API
GET http://127.0.0.1:8080/api/testauth
Authorization: Bearer <Generated Token>