SpringBoot3.x + SpringSecurity6.x + Redis + Mybatis-plus前后端分离验证

 介绍

想要完整了解SpringSecurity框架强烈推荐以下视频,使用SpringSecurity6.x的话以我的代码为准。

SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权-挑战黑马&尚硅谷_哔哩哔哩_bilibili

SpringSecurity的大致流程

登录

1.没有携带token的api访问请求会被拒绝,并且重定向到安全框架的login接口进行登录验证

2.在login接口中,安全框架会获取请求中包含用户名、密码的参数,经过UsernamePasswordAuthenticationFilter过滤器进行认证

3.UsernamePasswordAuthenticationFilter会将用户名传递给UserDetailsService.loadUserByUsername方法,通过用户名查找用户信息。我们需要通过自己自定义Service实现类,继承UserDetailsService接口,通过重写loadUserByUsername方法,自己实现查找用户信息。

4.如果没有查找到用户,此时我们可以抛出错误,交给ExceptionTranslationFilter处理,认证直接结束。如果找到了用户,loadUserByUsername将返回一个UserDetails对象,以供内部进行密码比较。

5.我们将自己定义一个LoginUser类型来封装我们的User对象,继承UserDetails接口,实现接口方法,返回user的用户名和密码,并让loadUserByUsername返回这个LoginUser类

而我们是前后端分离项目,是不会用到SpringSecurity自带的登录接口的,我们需要自己写登录接口,流程如下:

1.自定义SecurityConfig配置类

2.定义SecurityFilterChain方法,将自定义登录接口默认放行,让未登录用户可以访问

3.定义AuthenticationManager方法,这是关键,该Bean可以让我们在自己的登录业务层中调用SpringSecurity的认证,它将进入我们之前配置的loadUserByUsername进行用户认证操作

4.在登录接口里调用的ServiceImpl层中,创建继承了Authentication接口的UsernamePasswordAuthenticationToken对象,传递用户名、密码

5.调用AuthenticationManager,将UsernamePasswordAuthenticationToken对象作为参数,让SpringSecurity去完成认证,等待返回Authentication

6.如果返回的Authentication对象为null,说明认证失败。如果不为null,说明认证成功,用userid生成jwt token

7.利用封装好的reids操作类,将userid作为key,登录用户实体类作为value,存入redis中,方便SpringSecurity的token过滤器读取该jwt,判断用户是否登录。再把jwt发送到前端。

API的token验证过滤 

当API发起请求时,会首先经过SpringSecurity拦截器,拦截器有多层过滤链,我们需要自己定义一个过滤器JwtAuthenticationTokenFilter extends OncePerRequestFilter,在SecurityConfig中通过addFilterBefore函数设置将其放在SecurityConfig之前

doFilterInternal函数中,检查前来的API请求是否携带token,如果没有携带token,有可能是登录请求,有可能是非法信息,因此放行该请求。登录请求会被第二层SecurityFilterChain给放行,直达登录接口;非法请求会直接被拦截,抛出异常。

如果携带了token,通过JWT解码得出其用户id信息,然后在redis中查询是否曾有过该id记录,如果没有则抛出错误;如果有,则从redis中读取相关的用户信息体,并将其存入到SecurityContextHolder中,然后放行。后面的过滤器检测到SecurityContextHolder有实体类,也会继续放行,直达指定的API接口,访问成功

 

 UsernamePasswordAuthenticationFilter:负责处理带有用户名、密码的登录请求,用于认证

ExceptionTranslationFilter:处理过滤器中抛出的任何AcessDeniedException和AuthenticationException

FilterSecurityInterceptor:负责权限校验的过滤器

认证流程:

Authentication接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息

AuthenticationManager接口:定义了认证Authentication的方法

UserDetailsService接口:加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法

UserDetails接口:提供核心用户信息,通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回,然后将这些信息封装到Authentication对象中

代码

依赖

项目pom.xml的添加如下依赖

<?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.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>secure</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>secure</name>
    <description>secure</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
        <jjwt.version>0.9.1</jjwt.version>
        <fastjson.version>2.0.25</fastjson.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <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.4</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>3.0.4</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.7</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <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>

application.yml文件

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: 123456

  mvc:
    servlet:
      path: /api

JwtTokenHead: token

工具类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;
import java.util.ArrayList;

// 将对象序列化为字符串存入redis
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
    public static final Charset CHARSET = Charset.forName("UTF-8");
    private Class<T> clazz;

    static {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T value) throws SerializationException {
        if (value == null) {
            return new byte[0];
        }
        return JSON.toJSONString(value, SerializerFeature.WriteClassName).getBytes(CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if(bytes == null || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, CHARSET);
        return JSON.parseObject(str, clazz);
    }

    protected JavaType getCollectionType(Class<?> clazz) {
        return TypeFactory.defaultInstance().constructParametricType(ArrayList.class, clazz);
    }
}
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

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

public class JwtUtil {
    public static final String SECRET = "secret";
    public static final Long TTL = 60 * 60 * 1000L;

    public static String getUUID(){
        String uuid = UUID.randomUUID().toString().replaceAll("-","");
        return uuid;
    }

    public static SecretKey generalKey(){
        byte[] encodedKey = Base64.getDecoder().decode(SECRET.getBytes());
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    public static JwtBuilder getJwtBuilder(String subject, Long ttl, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttl == null){
            ttl = JwtUtil.TTL;
        }
        long expMillis = nowMillis + ttl;
        Date exp = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)
                .setSubject(subject)
                .setIssuer("sg")
                .setIssuedAt(now)
                .signWith(signatureAlgorithm, secretKey)
                .setExpiration(exp);
    }

    public static String createJWT(String subject, Long ttl) {
        JwtBuilder builder = getJwtBuilder(subject, ttl, getUUID());
        return builder.compact();
    }

    public static Claims parseJWT(String jwt)throws Exception{
        SecretKey key = generalKey();
        return Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(jwt)
                .getBody();
    }
}
package com.example.secure.service;

import com.example.secure.entity.User;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.concurrent.TimeUnit;
 
@Service
public class RedisService {
    private final RedisTemplate<String, Object> redisTemplate;
 
    public RedisService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
 
 
    public <T> void setValue(final String key, final T value) {
        redisTemplate.opsForValue().set(key, value);
    }
 
    // 获取值
    public User getValue(String key) {
        return (User) redisTemplate.opsForValue().get(key);
    }
 
    // 删除键
    public void deleteKey(String key) {
        redisTemplate.delete(key);
    }
}
import com.example.secure.utils.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
@Configuration
public class RedisConfig {
 
    @Bean

    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
        // 设置键的序列化器为字符串序列化器
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        // 设置值的序列化器为通用的 Jackson JSON 序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();

        return template;
    }
}

统一API返回

package com.example.secure.result;

import lombok.Data;

@Data
public class ApiResponse<T> {
    private Integer code;
    private String message;
    private T data;
    private Long timestamp;

    public ApiResponse() {
        this.timestamp = System.currentTimeMillis();
    }

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(200);
        response.setMessage("请求成功");
        response.setData(data);
        return response;
    }

    public static <T> ApiResponse<T> error(Integer code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(code);
        response.setMessage(message);
        return response;
    }
}

登录异常型

package com.example.secure.exception;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginErrorException extends RuntimeException{
    private String message;

    @Override
    public String getMessage() {
        return "登录错误";
    }
}

全局错误处理类

package com.example.secure.config;

import com.example.secure.exception.BusinessException;
import com.example.secure.exception.LoginErrorException;
import com.example.secure.result.ApiResponse;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ApiResponse<Void> handleBusinessException(BusinessException ex) {
        return ApiResponse.error(500, ex.getMessage());
    }

    @ExceptionHandler(LoginErrorException.class)
    public ApiResponse<Void> handleLoginException(LoginErrorException ex) {
        return ApiResponse.error(500, ex.getMessage());
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ApiResponse<Void> handleRequestParameterException() {
        return ApiResponse.error(500, "参数错误");
    }

    @ExceptionHandler(Exception.class)
    public ApiResponse<Void> handleException(Exception ex) {
        return ApiResponse.error(500, "错误:" + ex.getMessage());
    }
}

User类

存储着用户的信息,与数据库中的users表结构一一对应

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("users")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private String password;
}

user的service层到mapper层使用了mybatis-plus,所以略过。

loadUserByUsername:实现UserDetailsService接口

 安全框架内部会调用UserDetailsService的loadUserByUsername方法来获取用户的信息,我们需要自己定义一个获取用户信息的Service实现类,并且实现UserDetailsService接口,这样安全框架就会调用我们写的实现类,从我们想要的数据表中查询我们自己的用户信息。

package com.example.secure.service.ServiceImpl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.secure.entity.LoginUser;
import com.example.secure.entity.User;
import com.example.secure.exception.LoginErrorException;
import com.example.secure.mapper.UserMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import java.util.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserMapper userMapper;

    public UserDetailsServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws LoginErrorException {
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getName, username));
        if(Objects.isNull(user)){
            throw new LoginErrorException();
        }
        return new LoginUser(user);
    }
}

现在安全框架寻找用户信息时,会调用我们写好的loadUserByUsername方法从users表中查询用户信息,并将查找到的信息以我们自己定义的LoginUser类来返回。安全框架内部将自己进行密码的匹配比较。成功之后返回一个我们定义的LoginUsers对象,里面包含了该user的所有信息,我们根据其userid生成jwt返回出去,并把"token"、LoginUsers对象作为value放入redis中。

LoginUsers:重写UserDetails接口 

安全框架内部使用UserDetails对象来携带登录用户的基本信息,比如用户名和密码,是否已经过期和其他状态返回方法。我们需要自己定义一个DO类,通过实现UserDetails的接口,让SpringSecurity改用我们的DO。

这里我将我自己的user类封装到LoginUser里面,将安全框架规定的获取用户名和密码的方法全部返回user的用户名和密码

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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 getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
//        return UserDetails.super.isAccountNonExpired();
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
//        return UserDetails.super.isAccountNonLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
//        return UserDetails.super.isCredentialsNonExpired();
    }

    @Override
    public boolean isEnabled() {
        return true;
//        return UserDetails.super.isEnabled();
    }
}

现在SpringSecurity已经可以实现在我们自己定义的users表中进行用户验证了。由于我数据库中的测试密码是明文,没有进行任何加密,因此明文密码前面必须加{noop},告诉安全框架该密码是明文。

passwordEncoder:对加密密码进行配对

由于为了安全起见,密码不能以明文存储在数据库中,而是应该加密,当数据库中的密码为加密后密码时,我们需要提供一个passwordEncoder的Bean方法,能够让SpringSecurity通过该方法,将前端传递过来的明文密码参数与数据库中已经加密的密码进行比较。我们采用BCryptPasswordEncoder来进行加密与密码比较。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig{
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

BCryptPasswordEncoder对象有两个方法:

1.  boolean mathes(<明文密码>,<密文密码>)比较两个密码是否相同,相同返回ture,否则false

2. String encode(<明文密码>)加密明文密码

登录接口放行

在SecurityConfig中添加以下Bean,进行接口的放行配置。我的登录接口是/api/users/login

cors是为了实现跨域请求,这是前后端项目必须设置的。

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) throws Exception {
        http
                .cors(Customizer.withDefaults())
                .authorizeHttpRequests(auth -> auth
                        // 放行 Swagger 相关接口
                        .requestMatchers(
                                new AntPathRequestMatcher("/api/users/login") // 暂时放行所有 API 以供测试
                        ).permitAll()
                        // 其他请求需要身份认证
                        .anyRequest().authenticated()
                )
//                前后端分离项目采用无状态架构设计,因此不需要从Session中获取客户端状态,关闭该功能
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 关闭 CSRF(防止 Swagger 无法调用 POST 请求)
                .csrf(AbstractHttpConfigurer::disable)
                //添加API的token过滤器在该过滤器的前面
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

 CorsConfig配置,跨域请求配置文件

package com.example.secure.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.List;

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        // 允许所有域名携带凭证前来访问
        configuration.setAllowedOriginPatterns(List.of("*"));
        configuration.setAllowCredentials(true);
//        允许所有不携带凭证的域名来访问
//        configuration.setAllowedOrigins(List.of("*"));
//        configuration.setAllowCredentials(false);
        // 允许的请求方法
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        // 允许的请求头
        configuration.setAllowedHeaders(List.of("*"));
        // 是否允许携带凭证(如 Cookie)
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

AuthenticationManager配置

 该配置可以让我们在ServiceImpl层中自动注入AuthenticationManager,调用该方法进行SpringSecurity认证。

@Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

ServiceImpl业务实现类

在验证函数中,调用AuthenticationManager的authenticate函数。

package com.example.secure.service.ServiceImpl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.secure.config.RedisConfig;
import com.example.secure.entity.LoginUser;
import com.example.secure.entity.User;
import com.example.secure.exception.LoginErrorException;
import com.example.secure.mapper.UserMapper;
import com.example.secure.service.RedisService;
import com.example.secure.service.UserService;
import com.example.secure.utils.JwtUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import java.util.Objects;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    private final AuthenticationManager authenticationManager;
    private final RedisService redisService;

   public UserServiceImpl(AuthenticationManager authenticationManager, RedisService redisService) {
       this.authenticationManager = authenticationManager;
       this.redisService = redisService;
   }

    @Override
    public String login(User user){
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword());
        Authentication authentication= authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        if(Objects.isNull(authentication)){
            throw new LoginErrorException();
        }
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwtToken = JwtUtil.createJWT(userId);
        redisService.setValue("token:" + userId, jwtToken);
        return jwtToken;
    }
}

你需要通过UsernamePasswordAuthenticationToken类对象来向SpringSecurity传递用户名、密码这两个参数。AuthenticationManager.authenticate将返回Authentication对象,当返回的Authentication对象为空时,说明认证失败,一般会自动在ExceptionTranslationFilter中被处理掉,不会继续往下执行,但为了符合代码规范,这里我们加个登录失败异常抛出去即可。

我们只需要关注当该对象不为空的时候,此时将会返回关于user的所有信息,包括id、用户名、密码等等,我们可以将这些信息生成为jwt令牌,存入redis中,发送到前端去,让登录成功的请求以后都携带该token过来,后端访问redis来进行认证

API的token验证

我们自定义一个JwtAuthenticationTokenFilter的实体类,在这里实现API的token验证

package com.example.secure.config;

import com.example.secure.entity.LoginUser;
import com.example.secure.exception.LoginErrorException;
import com.example.secure.service.RedisService;
import com.example.secure.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Objects;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Value("${JwtTokenHead}")
    private String JwtTokenHead;

    private final RedisService redisService;

    public JwtAuthenticationTokenFilter(RedisService redisService) {
        this.redisService = redisService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//        获取token
        String token = request.getHeader("Authorization");
        if(!StringUtils.hasText(token)){
            System.out.println("无token");
//            放行该请求,未认证的请求会被后续的认证过滤器抛出异常处理掉
            filterChain.doFilter(request, response);
//            这里返回是为了让该请求被后续的过滤器处理后不再经过过滤器链,不再继续往下执行解析token
            return;
        }
        System.out.println("token");
        System.out.println(token);
//        解析token
        Claims claims = JwtUtil.parseJWT(token);
        String userId = claims.getSubject();

//        从reids中获取用户信息
        String redisKey = JwtTokenHead + ":" + userId;
        LoginUser loginUser = redisService.getValue(redisKey);
        if(Objects.isNull(loginUser)){
            throw new LoginErrorException();
        }
//        必须使用带三个参数的函数,该函数的构造函数会把是否认证设置为true
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);

//        存入SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//        放行,使其经过下一个过滤器
        filterChain.doFilter(request, response);
    }
}

登录用户鉴权

SpringSecurity提供了对登录用户进行权限甄别,对于那些无权限访问某些接口的用户进行限制,比如普通用户不能够调用删除用户接口,只有管理员才允许调用。

1.我们可以在数据库中添加权限表和角色表,每个角色拥有一组权限,用户与角色一对多联系。创建根据userid查询权限的mapper层接口。

2.在LoginUser中添加权限列表,用于存放该用户的权限;添加List<SimpleGrantedAuthority>对象,SpringSecurity需要该列表来获取用户的权限,该属性成员需要添加@JSONField(serialize = false),表示在存入redis中时,这个属性成员不参与字符串序列化,提高安全性。我们将权限列表转换为List<SimpleGrantedAuthority>,通过规定的getAuthorities接口函数返回出去

3. 在UserDetailsServiceImpl的loadUserByUsername中添加权限查询,我的代码是为了测试而特地写死了。这里可以使用查询接收权限列表,然后封装到LoginUser中

4.SecurityConfig类添加@EnableMethodSecurity(),表示为接口层添加权限限制

5.在某个需要规定权限的controller接口处添加权限注解,只有拥有该权限的用户才可以访问

SecurityConfig修改

package com.example.secure.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableMethodSecurity()
public class SecurityConfig{
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) throws Exception {
        http
                .cors(Customizer.withDefaults())
                .authorizeHttpRequests(auth -> auth
                        // 放行 Swagger 相关接口
                        .requestMatchers(
                                new AntPathRequestMatcher("/api/users/login") // 暂时放行所有 API 以供测试
                        ).permitAll()
                        // 其他请求需要身份认证
                        .anyRequest().authenticated()
                )
//                前后端分离项目采用无状态架构设计,因此不需要从Session中获取客户端状态,关闭该功能
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 关闭 CSRF(防止 Swagger 无法调用 POST 请求)
                .csrf(AbstractHttpConfigurer::disable)
//                .cors(Customizer.withDefaults())
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

LoginUser修改

package com.example.secure.entity;

import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;
    private List<String> roles;

    //    该变量成员不进行序列化
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    public LoginUser(User user, List<String> roles) {
        this.user = user;
        this.roles = roles;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) {
            return authorities;
        }
        authorities = roles.stream()
                .map(SimpleGrantedAuthority::new)
                .toList();
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
//        return UserDetails.super.isAccountNonExpired();
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
//        return UserDetails.super.isAccountNonLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
//        return UserDetails.super.isCredentialsNonExpired();
    }

    @Override
    public boolean isEnabled() {
        return true;
//        return UserDetails.super.isEnabled();
    }
}
JwtAuthenticationTokenFilter.java代码修改:
package com.example.secure.config;

import com.example.secure.entity.LoginUser;
import com.example.secure.exception.LoginErrorException;
import com.example.secure.service.RedisService;
import com.example.secure.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Objects;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Value("${JwtTokenHead}")
    private String JwtTokenHead;

    private final RedisService redisService;

    public JwtAuthenticationTokenFilter(RedisService redisService) {
        this.redisService = redisService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//        获取token
        String token = request.getHeader("Authorization");
        if(!StringUtils.hasText(token)){
            System.out.println("无token");
//            放行该请求,未认证的请求会被后续的认证过滤器抛出异常处理掉
            filterChain.doFilter(request, response);
//            这里返回是为了让该请求被后续的过滤器处理后不再经过过滤器链,不再继续往下执行解析token
            return;
        }
        System.out.println("token");
        System.out.println(token);
//        解析token
        Claims claims = JwtUtil.parseJWT(token);
        String userId = claims.getSubject();

//        从reids中获取用户信息
        String redisKey = JwtTokenHead + ":" + userId;
        LoginUser loginUser = redisService.getValue(redisKey);
        if(Objects.isNull(loginUser)){
            throw new LoginErrorException();
        }
//        必须使用带三个参数的函数,该函数的构造函数会把是否认证设置为true
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());

//        存入SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//        放行,使其经过下一个过滤器
        filterChain.doFilter(request, response);
    }
}
UserDetailsServiceImpl.java
package com.example.secure.service.ServiceImpl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.secure.entity.LoginUser;
import com.example.secure.entity.User;
import com.example.secure.exception.LoginErrorException;
import com.example.secure.mapper.UserMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserMapper userMapper;

    public UserDetailsServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws LoginErrorException {
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getName, username));
        if(Objects.isNull(user)){
            throw new LoginErrorException();
        }
//        用户权限列表
        List<String> list = new ArrayList<>(Arrays.asList("admin", "user"));

        return new LoginUser(user, list);
    }
}

UserController

package com.example.secure.controller;

import com.example.secure.entity.DTO.LoginUserDTO;
import com.example.secure.entity.User;
import com.example.secure.result.ApiResponse;
import com.example.secure.service.UserService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;


@RestController
@RequestMapping("users")
@CrossOrigin
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping(value = "test")
    @PreAuthorize("hasAuthority('admin')")
    public ApiResponse<String> test() {
        return ApiResponse.success("test");
    }


    @PostMapping("login")
    public ApiResponse<Map<String,Object>> login(@RequestBody LoginUserDTO loginUserDTO) {
        User user = new User(null,loginUserDTO.getAccount(), loginUserDTO.getPassword());
        Map<String,Object> map = new HashMap<>();
        map.put("token",userService.login(user));
        return ApiResponse.success(map);
    }

    @GetMapping("logout")
    public ApiResponse<String> logout() {
        userService.logout();
        return ApiResponse.success("注销成功");
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值