首页 > 数据库 >Spring Boot + Spring Security + Redis + JWT + CSRF 双认证简单整合

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

时间:2024-08-02 22:29:14浏览次数:15  
标签:String Spring JWT springframework user CSRF import org public

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

 

标签:String,Spring,JWT,springframework,user,CSRF,import,org,public
From: https://blog.csdn.net/qq_71387716/article/details/140859067

相关文章

  • 【Spring Boot简介】
    什么是SpringBoot呢,就是一个JavaWeb的开发框架,和SpringMVC类似,对比其他JavaWeb框架的好处,官方说是简化开发,约定大于配置,能够迅速的开发web应用。所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂的应用场景,逐渐衍生一种规范框架,只需要进行各种配置而不需要自己去实现......
  • 接口文档,jwt,base64编码解码
    Ⅰ接口文档【一】接口文档了解#作为后端,接口写完了--->接口给前端使用 -登录接口:username,password,code#写接口的人负责写接口文档 -如何写?-写在哪?#通常在公司中: 1使用world编写,放在公共平台上2使用MD编写3第三方平台编写:showdoc -http......
  • MyBatis代码生成器:SpringBoot 引入MybatisGenerator
    1.引入插件<plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>1.3.5</version>......
  • springboot 自动配置原理
    @SpringBootApplication》@EnableAutoConfiguration》@Import(AutoConfigurationImportSelector.class)AutoConfigurationImportSelector.javapublicclassAutoConfigurationImportSelectorimplementsDeferredImportSelector,BeanClassLoaderAware,ResourceLoaderAware,......
  • Springboot 计算机毕业设计“爱艺创”特长培训管理系统程序
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表学生,教职工,班级信息,课程分类,课程信息,风采展示,学生档案,教职工档案开题报告内容一、研究背景与意义1.研究背景随着社会的发展和进步,特长培训在培养学......
  • Springboot计算机毕业设计“漫画之家”系统+程序+源码
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,漫画,同人插画,漫画活动,商品,约稿,约稿公告开题报告内容一、选题的背景及意义1.1背景随着科学技术的飞速发展和互联网的普及,漫画作为一种独特的艺术......
  • Springboot计算机毕业设计《C语言程序设计》题库管理系统39b5j
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表学生,教师,课程章节,课程信息,考点信息开题报告内容一、选题背景及意义随着计算机技术的快速发展,C语言作为一种基础且功能强大的高级程序设计语言,在操作系统......
  • [Spring]SpringMVC
    SpringMVC工作原理SpringMVC的核心组件有哪些?记住了下面这些组件,也就记住了SpringMVC的工作原理。DispatcherServlet:核心的中央处理器,负责接收请求、分发,并给予客户端响应。HandlerMapping:处理器映射器,根据URL去匹配查找能处理的Handler,并会将请求涉及到的拦截器......
  • Spring Boot 实现动态修改定时任务的执行时间
    SpringBoot实现动态修改定时任务的执行时间前提大家通过SpringBoot跑定时任务,用的最多的是@Scheduled,通过指定cron来定时的执行任务,cron可以使用SPEL表达式来通过配置文件配置,但是项目一旦启动,执行时间便无法修改,不够方便,本文基于此来优化项目启动之后不能动态修改cron的问题......
  • SpringCloud入门学习笔记(四)
    Sentinel篇 SpringCloud入门学习笔记(一)-CSDN博客SpringCloud入门学习笔记(二)-CSDN博客SpringCloud入门学习笔记(三)-CSDN博客前言 在互联网应用过程中,有很多的高并发访问场景,类似于双十一这种活动,特点是访问量剧增,访问量超出系统所能处理的最大并发数。 如果没有保护机......