1. 项目结构
2. 数据库相关操作
create database user_profiles;
use user_profiles;
CREATE TABLE `user`
(
`id` INT AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(255) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) UNIQUE,
`role` VARCHAR(255) DEFAULT 'USER',
`enabled` BOOLEAN DEFAULT TRUE
);
3. Maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>spring_jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring_jwt</name>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.lettuce/lettuce-core -->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.3.0.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4. application.yml
spring:
application:
name: spring_jwt
cache:
type: redis
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/user_profiles?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
redis:
host: 192.168.186.77
port: 6379
jwt:
secret_key: abc123
expire_time: 15
5. SpringJwtApplication.java
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootApplication
public class SpringJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringJwtApplication.class, args);
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
6. RedisJwtUtil.java
package org.example.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import io.lettuce.core.api.sync.RedisCommands;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
@Component
public class RedisJwtUtil {
private static final Logger logger = LoggerFactory.getLogger(RedisJwtUtil.class);
private final String secretKey;
private final long expirationTime;
private final RedisCommands<String, String> redisCommands;
private static final String BLACKLIST_KEY_PREFIX = "blacklist_"; //黑名单标识前缀
public RedisJwtUtil(@Value("${jwt.secret_key}") String secretKey,
@Value("${jwt.expire_time}") long expirationTime,
RedisCommands<String, String> redisCommands) {
this.secretKey = secretKey;
this.expirationTime = expirationTime;
this.redisCommands = redisCommands;
}
public String generateToken(String username) {
Date issuedAt = new Date();
Date expiresAt = new Date(issuedAt.getTime() + expirationTime*60*1000); // 设置过期时间
return JWT.create()
.withSubject(username)
.withIssuedAt(issuedAt)
.withExpiresAt(expiresAt)
.sign(Algorithm.HMAC256(secretKey));
}
public boolean validateToken(String token) {
try {
String username = extractUsername(token);
if (username == null) {
return false;
}
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier verifier = JWT.require(algorithm)
.withSubject(username)
.build();
DecodedJWT jwt = verifier.verify(token);
return !isTokenExpired(jwt);
} catch (JWTVerificationException exception) {
logger.error("JWT Verification failed", exception);
return false;
}
}
public String extractUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getSubject();
} catch (JWTVerificationException exception) {
logger.error("Error decoding JWT", exception);
return null;
}
}
private boolean isTokenExpired(DecodedJWT jwt) {
return jwt.getExpiresAt().before(new Date());
}
public void saveToken(String username, String token) {
try {
redisCommands.setex(username, expirationTime*60, token); // 使用 setex 方法设置过期时间,单位为秒
logger.info("Token saved for user: {}", username);
} catch (Exception e) {
logger.error("Error saving token to Redis", e);
}
}
public boolean redisValidate(String token) {
try {
String username = extractUsername(token);
if (username == null) {
return false;
}
String redisToken = redisCommands.get(username);
return token.equals(redisToken) && validateToken(redisToken);
} catch (Exception e) {
logger.error("Error validating token with Redis", e);
return false;
}
}
}
说明:RedisJwtUtil
类的主要功能是处理 JWT(JSON Web Token)的生成、验证和存储。它利用了 Redis 来存储生成的JWT,以实现更有效的令牌管理。
7. UserService.java
package org.example.service;
import org.example.entity.User;
import org.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.time.Duration;
import java.util.Collections;
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private final Duration CACHE_EXPIRATION = Duration.ofMinutes(3); // 设置缓存过期时间3分钟
private static final String USER_CACHE_KEY = "userCache:";
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + user.getRole());
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(Collections.singletonList(authority))
.build();
}
public void register(User user) throws Exception {
if (userMapper.findByUsername(user.getUsername()) != null) {
throw new Exception("User already exists!");
}
userMapper.register(user);
cacheUser(user); // 缓存用户数据
}
public User findByUsername(String username){
User user = (User) redisTemplate.opsForValue().get(USER_CACHE_KEY + username);
if (user != null) {
return user;
}
user = userMapper.findByUsername(username);
if (user != null) {
cacheUser(user); // 缓存用户数据
}
return user;
}
private void cacheUser(User user) {
try {
redisTemplate.opsForValue().set(USER_CACHE_KEY + user.getUsername(), user, CACHE_EXPIRATION);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
说明:UserService
类的主要功能是管理用户信息,包括从数据库加载用户信息、用户注册和用户缓存。
8. UserMapper.java
package org.example.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Select;
import org.example.entity.User;
@Mapper
public interface UserMapper {
@Insert("INSERT INTO user(username, password, email, role, enabled) VALUES(#{username}, #{password}, #{email}, #{role}, #{enabled})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void register(User user);
@Select("SELECT * FROM user WHERE username = #{username} ")
User findByUsername(String username);
}
9. JwtAuthenticationFilter.java
package org.example.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.util.RedisJwtUtil;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisJwtUtil redisJwtUtil;
private final ObjectMapper objectMapper = new ObjectMapper();
private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
response.setStatus(status);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", message);
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException {
try {
// 从请求头中获取 Authorization 字段
String header = request.getHeader("Authorization");
String token = null;
String username = null;
// JWT Token的形式为"Bearer token",移除 Bearer 单词,只获取 Token 部分
if (header != null && header.startsWith("Bearer ")) {
token = header.substring(7);
try {
// 从 Token 中提取用户名
username = redisJwtUtil.extractUsername(token);
} catch (Exception e) {
// 无效的 JWT Token 或无法解析
sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "未认证,请先登录!");
return;
}
}
// 获取到Token后,进行验证
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 验证 Token 是否在 Redis 中存在且有效
if (redisJwtUtil.redisValidate(token)) {
// 根据用户名加载用户详情
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
// 如果 Token 有效,配置 Spring Security 手动设置认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 在设置 Authentication 之后,指定当前用户已认证
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else {
sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "未认证,请先登录!");
return;
}
} else {
sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "未认证,请先登录!");
return;
}
}
// 如果没有token或token无效,将请求传递到过滤器链的下一个过滤器
filterChain.doFilter(request, response);
} catch (Exception e) {
// 捕获所有异常,并发送错误响应
sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "服务器内部错误");
}
}
}
说明:JwtAuthenticationFilter
类主要负责通过 JWT 验证用户身份并将认证信息存储到 Spring Security 上下文中。
10. Vo.java
package org.example.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Vo<T> {
private int status;
private String token;
private String csrfToken;
private String message;
private T data;
public Vo(int status, String message, T data) {
this.status = status;
this.message = message;
this.data = data;
}
public Vo(int status, String message, T data, String Token) {
this.status = status;
this.message = message;
this.data = data;
this.token = Token;
}
}
说明:Vo类充当封装JSON对象的作用,响应数据为JSON格式。
11. User.java
package org.example.entity;
import lombok.Data;
@Data
public class User {
private Integer id;
private String username;
private String password;
private String email;
private String role;
private Boolean enabled;
}
12. UserController.java
package org.example.controller;
import org.example.entity.Vo;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/home")
public ResponseEntity<Vo<?>> home() {
return ResponseEntity.ok(new Vo<>(HttpStatus.OK.value(), "success", "欢迎你,普通用户!"));
}
@PostMapping("/double")
public ResponseEntity<Vo<?>> Double() {
return ResponseEntity.ok(new Vo<>(HttpStatus.OK.value(), "success", "普通用户:认证成功通过!"));
}
}
说明:普通用户的接口。
13. AuthController.java
package org.example.controller;
import org.example.entity.User;
import org.example.entity.Vo;
import org.example.service.UserService;
import org.example.util.RedisJwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserService userService;
@Autowired
private RedisJwtUtil redisJwtUtil;
@PostMapping("/register")
public ResponseEntity<Vo<?>> register(@RequestBody User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
try {
userService.register(user);
String token = redisJwtUtil.generateToken(user.getUsername());
redisJwtUtil.saveToken(user.getUsername(), token);
return ResponseEntity.ok(new Vo<>(HttpStatus.OK.value(), "Register Success", null,"Bearer "+token)); // 返回成功响应
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(new Vo<>(HttpStatus.CONFLICT.value(), "Register failed: Username already exists!", null)); // 返回失败响应
}
}
@PostMapping("/login")
public ResponseEntity<Vo<?>> login(@RequestBody User user) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = redisJwtUtil.generateToken(user.getUsername());
redisJwtUtil.saveToken(user.getUsername(), token);
return ResponseEntity.ok(new Vo<>(HttpStatus.OK.value(), "Login Success", null,"Bearer "+token));
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new Vo<>(HttpStatus.UNAUTHORIZED.value(), "Login failed: Invalid username or password", null));
}
}
}
说明:用户认证接口,注册和登录。
14. AdminController.java
package org.example.controller;
import org.example.entity.Vo;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/home")
public ResponseEntity<Vo<?>> home() {
return ResponseEntity.ok(new Vo<>(HttpStatus.OK.value(), "success", "欢迎你,超级用户!"));
}
@PostMapping("/double")
public ResponseEntity<Vo<?>> doubleTest() {
return ResponseEntity.ok(new Vo<>(HttpStatus.OK.value(), "success", "超级用户:认证成功通过!"));
}
}
说明:超级用户接口。
15. SecurityConfig.java
package org.example.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.filter.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
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.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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 配置请求处理器
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(requestHandler)
.ignoringRequestMatchers("/auth/**"))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/static/**", "/css/**", "/js/**", "/images/**","/auth/**").permitAll() // 允许公开访问静态资源路径
.requestMatchers("/admin/**").hasRole("ADMIN") // 限制 admin 路径只有 ADMIN 角色能访问
.requestMatchers("/user/**").hasRole("USER") // 限制 user 路径只有 USER 角色能访问
.anyRequest().authenticated()) // 其他请求需要认证
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 使用无状态会话
.exceptionHandling(exception -> exception
.accessDeniedHandler((request, response, accessDeniedException) -> sendErrorResponse(response, HttpServletResponse.SC_FORBIDDEN, "无权访问该资源"))
.authenticationEntryPoint((request, response, authException) -> sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "未认证,请先登录"))) // 使用匿名类处理未认证和越权访问
.formLogin(AbstractHttpConfigurer::disable)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
response.setStatus(status);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", message);
response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse));
}
}
说明:SecurityConfig
类主要负责配置 Spring Security 的安全设置,结合 JWT 认证和 CSRF 保护
16. LettuceConfig.java
package org.example.config;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class LettuceConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Value("${redis.host}")
private String redisHost;
@Value("${redis.port}")
private int redisPort;
@Bean
public RedisClient redisClient() {
RedisURI redisURI = RedisURI.builder()
.withHost(redisHost)
.withPort(redisPort)
.build();
return RedisClient.create(redisURI);
}
@Bean
public StatefulRedisConnection<String, String> connection(RedisClient redisClient) {
return redisClient.connect();
}
@Bean
public RedisCommands<String, String> redisCommands(StatefulRedisConnection<String, String> connection) {
return connection.sync();
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}
}
说明:LettuceConfig
类负责配置 Redis 客户端与 Spring Boot 的集成,通过 Lettuce
库来管理 Redis 连接。
17. CsrfTokenAdvice.java
package org.example.advice;
import org.example.entity.Vo;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.server.ServletServerHttpRequest;
@ControllerAdvice
public class CsrfTokenAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType,@NonNull MediaType selectedContentType,
@NonNull Class selectedConverterType, @NonNull ServerHttpRequest request,@NonNull ServerHttpResponse response) {
if (!HttpMethod.GET.matches(request.getMethod().name())) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
CsrfToken csrfToken = (CsrfToken) servletRequest.getAttribute(CsrfToken.class.getName());
if (body instanceof Vo<?> voBody) {
voBody.setCsrfToken(csrfToken.getToken());
}
System.out.println(" CSRF 最新令牌:"+csrfToken.getToken());
}
return body;
}
}
说明:CsrfTokenAdvice
类通过实现 ResponseBodyAdvice
接口来拦截每一个控制层返回的响应体,并在响应体中添加 CSRF 令牌 。
18. 测试验证
18.1 注册
18.1.1 超级用户注册
18.1.2 普通用户注册
18.2 登录
18.2.1 超级用户登录
![](https://i-blog.csdnimg.cn/direct/06fbf6e3f8a449c28748f745ed61caef.png)
18.2.2 普通用户登录
18.3 JWT令牌认证
18.3.1 超级用户验证
说明:使用注册或者登录生成的token,Headers
的 Authorization 携带认证信息,此请求是GET请求不需要进行CSRD认证
。
18.3.2 普通用户验证
18.4 CSRF+JWT认证
18.4.1 只携带JWT令牌认证
说明:只携带JWT令牌的token进行认证,是无效的。
18.4.2 只携带CSRF认证
说明:只携带CSRF令牌的token进行认证,也是无效的。
18.4.3 CSRF+JWT
说明:CSRF+JWT令牌同时验证POST、PUT、DELETE请求才有效。
18.4.4 其他
说明:拿超级用户的CSRF来验证普通用户,说明同一个会话内,即使登录的账号不同,CSRF也是一致的,看控制台输出的最新CSRF如下,得证。
19. 总结
19.1 CSRF的工作原理
当用户进行认证时,服务器生成一个 CSRF 令牌并存储在用户的会话或发送到客户端cookie 中。服务器将 CSRF 令牌发送给客户端,客户端在随后的请求中必须包含此令牌。服务器验证每个请求中包含的令牌是否与存储在会话或 cookie 中的令牌匹配。
19.2 CSRF的问题
CSRF 令牌通常与会话绑定,而不是用户身份,这意味着多个用户使用相同会话时可能会使用相同的 CSRF 令牌。如果多个用户使用同一会话,则相同的 CSRF 令牌可以在这些用户之间共享。
19.3 CSRF的安全性
确保 CSRF 令牌与用户会话绑定。这通常通过在用户登录时将令牌存储在会话中来实现。在验证 CSRF 令牌时,也检查用户上下文以确保令牌与用户会话匹配。
19.4 请求中包含CSRF
每次需要进行 CSRF 验证的请求(如 POST、PUT、DELETE 请求),都会在请求中包含 CSRF 令牌,要求生成新的令牌 ,GET请求令牌一般不变。
19.5 涉及到的技术栈
Spring Security ,Redis,Mybatis,MySQL ,Spring Boot,JWT,CSRF 等简单整合。