Spring Boot + Spring Security + Redis + JWT + CSRF 双认证简单整合

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 超级用户登录



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 等简单整合。 

 

  • 51
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot 是一个用于构建微服务的开源框架,它能够快速搭建项目并且提供了许多便捷的功能和特性。Spring Security 是一个用于处理认证和授权的框架,可以保护我们的应用程序免受恶意攻击。JWT(JSON Web Token)是一种用于身份验证的开放标准,可以被用于安全地传输信息。Spring MVC 是一个用于构建 Web 应用程序的框架,它能够处理 HTTP 请求和响应。MyBatis 是一个用于操作数据库的框架,可以简化数据库操作和提高效率。Redis 是一种高性能的键值存储系统,可以用于缓存与数据存储。 基于这些技术,可以搭建一个商城项目。Spring Boot 可以用于构建商城项目的后端服务,Spring Security 可以确保用户信息的安全性,JWT 可以用于用户的身份验证,Spring MVC 可以处理前端请求,MyBatis 可以操作数据库,Redis 可以用于缓存用户信息和商品信息。 商城项目的后端可以使用 Spring BootSpring Security 来搭建,通过 JWT 来处理用户的身份验证和授权。数据库操作可以使用 MyBatis 来简化与提高效率,同时可以利用 Redis 来缓存一些常用的数据和信息,提升系统的性能。前端请求则可以通过 Spring MVC 来处理,实现商城项目的整体功能。 综上所述,借助于 Spring BootSpring SecurityJWTSpring MVC、MyBatisRedis 这些技术,可以构建出一个高性能、安全可靠的商城项目,为用户提供良好的购物体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值