SpringBoot整合SpringSecurity、JWT、Redis

简介

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

使用

导入依赖

<?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.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>SpringSecurity-Demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringSecurity-Demo</name>
    <description>SpringSecurity-Demo</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- mybatisPlus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.9</version>
        </dependency>

        <!-- 德鲁伊数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.23</version>
        </dependency>

        <!--    Redis    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--连接池依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!--    Mysql连接    -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <!-- jwt令牌 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.4.0</version>
        </dependency>

        <!-- fastjson序列化 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.53</version>
        </dependency>
        <!--jackson依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置application.yml文件

spring:
  application:
    # 项目名称
    name: spring-security-demo
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/数据库名称?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: password
    # druid数据源配置
    druid:
      # 初始化连接池大小
      initialSize: 5
      # 最小连接数
      minIdle: 5
      # 最大连接数
      maxActive: 40
      # 获取连接时的最大等待时间
      maxWait: 60000
      #间隔多长时间进行一次检测;
      timeBetweenEvictionRunsMillis: 60000
      #配置一个最小的生存对象的空闲时间
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1
      testWhileIdle: true
      #申请连接时执行validationQuery检测连接是否有效,默认true,开启后会降低性能
      testOnBorrow: false
      #归还连接时执行validationQuery检测连接是否有效,默认false,开启后会降低性能
      testOnReturn: false
      poolPreparedStatements: true
      #配置监控统计拦截的filters。stat:监控统计、wall:防御sql注入、log4j:日志记录
      filters: stat,log4j
      maxPoolPreparedStatementPerConnectionSize: 20
      useGlobalDataSourceStat: true
      #执行时间超过3000毫秒的sql会被标记为慢sql
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=3000
      #配置过滤器,过滤掉静态文件
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: /druid/*,*.js,*.css,*.gif,*.jpg,*.bmp,*.png,*.ico
  data:
    # redis数据库
    redis:
      # redis服务器地址
      host: 127.0.0.1
      # 端口
      port: 6379
      # 超时时间
      timeout: 1800000
      # 数据库索引
      database: 0
      # 密码
      password:
      lettuce:
        pool:
          # 最大等待时间,负数表示没有限制
          max-wait: 5000ms
          # 最大空闲连接数
          max-idle: 5
          # 最小空闲连接数
          min-idle: 0
          # 最大连接数,负数表示没有限制
          max-active: 20
  #fastjson序列化
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
#MybatisPlus
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml  # mapper映射文件(默认存在)
  type-aliases-package: cn.system.entity # 别名包
  configuration:
    map-underscore-to-camel-case: true # 是否开启下划线与驼峰的映射(默认打开)
    cache-enabled: false # 是否开启二级缓存(默认关闭)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启日志打印
  global-config:      # 全局配置
    db-config:
      id-type: auto #id+1自增策略(默认assign_id雪花算法)
      # 下面的配置:将所有的删除语句更改为修改语句
      logic-delete-field: del_flag # 是否删除;全局逻辑删除的实体字段名,字段类型可以是boolean、integer
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

配置SpringSecurity的配置文件

package com.example.springsecuritydemo.config;

import com.example.springsecuritydemo.filter.JwtAuthenticationTokenFilter;
import com.example.springsecuritydemo.handler.AnonymousAuthenticationEntryPointHandler;
import com.example.springsecuritydemo.handler.CustomerAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.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.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

//开启方法鉴权
@EnableMethodSecurity
@Configuration
public class SecurityConfig {

    /**
     * 认证用户无权限访问处理器
     */
    @Autowired
    private CustomerAccessDeniedHandler customerAccessDeniedHandler;

    /**
     * JWT的token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 客户端进行认证数据的提交时出现异常,或匿名用户访问无权限资源时的处理器
     */
    @Autowired
    private AnonymousAuthenticationEntryPointHandler anonymousAuthenticationEntryPointHandler;
    

    /**
     * 配置过滤器链
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.authorizeHttpRequests(author -> {
                    author
                            //放行的接口
                            .requestMatchers("/login").permitAll()
                            .requestMatchers("/logout").permitAll()
                            //其他的接口都需要通过验证
                            .anyRequest().authenticated();
                })
                // 关闭CSRF保护
                .csrf(AbstractHttpConfigurer::disable)
                // 不通过session创建管理SecurityContextHolder
                .sessionManagement(session ->
                    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                // 禁用默认登录接口
                .formLogin(AbstractHttpConfigurer::disable)
                // SpringSecurity默认的登录和登出接口为login和logout,如果自己想自定义这两接口并且名称想取一样,就把这两接口给禁用掉
                // 禁用退出
                .logout(AbstractHttpConfigurer::disable)
                // 添加过滤器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                // 添加自定义异常处理
                .exceptionHandling(ex ->
                        // 认证用户无权限访问处理器
                        ex.accessDeniedHandler(customerAccessDeniedHandler)
                        // 客户端进行认证数据的提交时出现异常,或匿名用户访问无权限资源时的处理器
                        .authenticationEntryPoint(anonymousAuthenticationEntryPointHandler));
        return http.build();
    }

    /**
     * 认证逻辑
     * @param auth
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration auth) throws Exception {
        return auth.getAuthenticationManager();
    }

    /**
     * 密码加密规则
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

因为需要使用到Redis,所以还需配置Redis的配置

package com.example.springsecuritydemo.config;

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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        //创建RedisTemplate对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        //设置连接工厂
        template.setConnectionFactory(connectionFactory);
        //创建JSON序列化工具
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
        //设置key的序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashValueSerializer(RedisSerializer.string());
        //设置value的序列化
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        return template;
    }

}

创建登录(login)和退出接口(logout)

package com.example.springsecuritydemo.controller;

import com.example.springsecuritydemo.entity.SysUser;
import com.example.springsecuritydemo.entity.vo.ResultVo;
import com.example.springsecuritydemo.service.LoginService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    /**
     * 登录请求
     * @param user
     * @return
     */
    @PostMapping("/login")
    public ResultVo login(@RequestBody SysUser user){
        return loginService.login(user);
    }

    /**
     * 退出登录请求
     * @param request
     * @param response
     * @return
     */
    @PostMapping("/logout")
    public ResultVo logout(HttpServletRequest request, HttpServletResponse response){
        return loginService.logout(request, response);
    }

}

注意:如果你的登录和退出接口是login和logout,需要在SpringSecurity的配置文件中将默认的登录和退出接口禁用掉

实现LoginService接口

package com.example.springsecuritydemo.service.impl;

import com.example.springsecuritydemo.entity.SysUser;
import com.example.springsecuritydemo.entity.vo.LoginUser;
import com.example.springsecuritydemo.entity.vo.ResultVo;
import com.example.springsecuritydemo.exception.CustomerAuthenticationException;
import com.example.springsecuritydemo.service.LoginService;
import com.example.springsecuritydemo.utils.JwtUtils;
import com.example.springsecuritydemo.utils.RedisUtils;
import com.example.springsecuritydemo.utils.ResultVoUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

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

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager automaticManager;

    @Autowired
    private RedisUtils redisUtils;

    /**
     * 登录
     * @param user 请求用户
     * @return
     */
    @Override
    public ResultVo login(SysUser user) {
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authentication = null;
        try {
            authentication = automaticManager.authenticate(token);
        } catch (AuthenticationException e) {
            throw new RuntimeException(e);
        }
        // 获取登录用户
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 设置token
        String accessToken = JwtUtils.createToken("access_token", loginUser);
        // 存入redis中
        redisUtils.set(redisUtils.LOGIN_TOKEN+accessToken,accessToken,JwtUtils.TOKEN_EXPIRE_TIME / 1000);
        // 创建Map集合保存token
        Map<String, Object> map = new HashMap<>();
        map.put("access_token",accessToken);
        // 返回登录用户
        return ResultVoUtils.success("登录成功" ,map);
    }

    /**
     * 退出
     * @param request
     * @param response
     * @return
     */
    @Override
    public ResultVo logout(HttpServletRequest request, HttpServletResponse response) {
        // 获取携带的token
        String token = request.getHeader("Authorization");
        token = token.replace("Bearer ", "");
        // 获取当前用户上下文对象
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (!Objects.isNull(auth)){
            redisUtils.del(redisUtils.LOGIN_TOKEN+token);
            new SecurityContextLogoutHandler().logout(request, response, auth);
        }
        return ResultVoUtils.success("退出系统成功!");
    }
}

SpringSecurity默认通过内存进行查询,而我们需要通过数据库进行查询需要重写UserDetailsService的loadUserByUsername方法,SpringSecurity它是通过这个方法进行查询用户,使用JWT创建一个token进行校验,把token存入redis中进行保存

重写UserDetailsService

package com.example.springsecuritydemo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.springsecuritydemo.entity.SysUser;
import com.example.springsecuritydemo.entity.vo.LoginUser;
import com.example.springsecuritydemo.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
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 org.springframework.util.StringUtils;

import java.util.List;
import java.util.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if(!StringUtils.hasText(username)){
            throw new InternalAuthenticationServiceException("");
        }
        // 根据用户名查询用户
        SysUser user = sysUserMapper.selectOne(new QueryWrapper<SysUser>().eq("user_name", username));
        if (Objects.isNull(user)){
            throw new UsernameNotFoundException("");
        }
        // 查询该用户权限
        List<String> perms = sysUserMapper.selectAuthorityByUserId(user.getId());

        // 返回登录用户
        return new LoginUser(user, perms);
    }
}

在该代码中,通过判断传入进来的username是否为空,如果为空就抛出异常(这个后面统一处理异常类型),不为空则通过mapper在数据库中进行查询,判断是否能查到该用户,没有则报异常,有则继续通过该用户编号查询该用户所拥有的权限,最后loadUserByUsername这个方法返回的是UserDetails类型,我们需要一个实体类继承该类型进行操作。

创建LoginUser进行保存用户权限和数据

package com.example.springsecuritydemo.entity.vo;

import com.example.springsecuritydemo.entity.SysUser;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
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.Collections;
import java.util.List;
import java.util.Objects;

@Data
public class LoginUser implements UserDetails {

    /**
     * 用户
     */
    private SysUser user;
	/**
	 * 权限
	 */
    private List<String> perms;

    // 忽略JSON序列化
    @JsonIgnore
    private Collection<? extends GrantedAuthority> grantedAuthorities;

    /**
     * 构造方法
     * @param user 赋值
     */
    public LoginUser(SysUser user, List<String> perms){
        this.user = user;
        this.perms = perms;
    }

    /**
     * 权限
     * @return 权限列表
     */
    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
		// 判断grantedAuthorities不为空,则直接返回
        if(!Objects.isNull(grantedAuthorities)){
            return grantedAuthorities;
        }
		// 为空,则遍历perms
        grantedAuthorities = perms.stream().map(SimpleGrantedAuthority::new).toList();
        return grantedAuthorities;
    }

    /**
     * 获取密码
     * @return 密码
     */
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    /**
     * 获取用户名
     * @return 用户名
     */
    @Override
    public String getUsername() {
        return user.getUserName();
    }

    /**
     * 判断是否用户未过期
     * @return true未过期;false已过期
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 判断用户是否未锁定
     * @return true未锁定;false已锁定
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 判断凭证是否未过期
     * @return true未过期;false已过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 判断用户是否可用
     * @return true可用;false不可用
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

通过创建一个实体类来实现UserDetails接口,在该实体类中分别创建变量保存用户信息、用户权限。判断凭证是否未过期、用户是否可用等等内容可以通过数据库中字段来判断,这里我们直接返回true

校验token

在登录的时候,当用户登录成功后,我们创建了一个token,我们需要让前端发送的请求都要携带这个token来进行校验,这时我们需要配置一个过滤器类使它继承OncePerRequestFilter,

OncePerRequestFilter是Spring提供的一个过滤器基类,它确保了在一次完整的HTTP请求中,无论请求经过多少次内部转发,过滤器的逻辑都只会被执行一次。这对于需要在请求处理之前或之后进行一次性设置或清理资源的场景特别有用。

package com.example.springsecuritydemo.filter;

import com.example.springsecuritydemo.entity.vo.LoginUser;
import com.example.springsecuritydemo.exception.CustomerAuthenticationException;
import com.example.springsecuritydemo.handler.AnonymousAuthenticationEntryPointHandler;
import com.example.springsecuritydemo.utils.JwtUtils;
import com.example.springsecuritydemo.utils.RedisUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
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;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private AnonymousAuthenticationEntryPointHandler entryPointHandler;

    @Autowired
    private RedisUtils redisUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            // 获取请求路径
            String URI = request.getRequestURI();
            if (!URI.equals("/login")) {
                this.validateToken(request);
            }
        } catch (AuthenticationException e) {
            entryPointHandler.commence(request, response, e);
        }
        // 放行
        filterChain.doFilter(request, response);
    }

    /**
     * 验证token
     * @param request
     */
    public void validateToken(HttpServletRequest request) {
        // 通过请求头获取,支队POST请求有用,而对GET请求需要通过参数
        String token = request.getHeader("Authorization");
        // 判断token是否为空
        if (!StringUtils.hasText(token)) {
            throw new CustomerAuthenticationException("token为空");
        }
        // 去掉Bearer
        token = token.replace("Bearer ", "");
        // 从redis中获取token
        String redisToken = (String) redisUtils.get(redisUtils.LOGIN_TOKEN + token);
        if (!StringUtils.hasText(redisToken)){
            throw new CustomerAuthenticationException("token已过期");
        }

        LoginUser loginUser = null;
        // 校验token
        if (!JwtUtils.verify(token)){
            throw new CustomerAuthenticationException("token校验失败");
        }
        // 从token中取出登录用户
        loginUser = JwtUtils.getObject(token, "access_token", LoginUser.class);
        // 将验证完后的用户再次放入SpringSecurity的上下文中
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authToken);
    }

}

我们创建了一个JwtAuthenticationTokenFilter 来继承 OncePerRequestFilter类,实现它的doFilterInternal方法,在该方法中,我们先判断请求地址是否是登录(login)请求,如果是则直接过,因为都还没登录就必然没有token的产生,所以我们让它直接过,而不是登录(login)请求时,我们需要取出token进行校验,先将redis中对应的值取出来,如果没有则提示token已过期,有则使用JwtUtils工具类中的校验方法进行校验token是否正确,最后还需要取出登录用户(LoginUser),将其保存到SpringSecurity上下文中。这时登录接口就算做完了,而退出接口则是通过token来删除redis数据库内容并将SpringSecurity上下文中的对应的数据删除,则是退出。

授权和鉴权

在UserDetailsService实现类中(UserDetailsServiceImpl ),查询到用户后,再通过该用户ID查询权限,再LoginUser实体类中创建变量(perms)接收权限集合。而LoginUser实体类中实现UserDetails接口,需要重新getAuthorities方法,该方法需要返回的类型是Collection<? extends GrantedAuthority>我们还需要创建一个该类型的变量(grantedAuthorities),再该方法中,我们遍历集合(perms)将里面的值赋予变量(grantedAuthorities),最后将变量(grantedAuthorities)返回

@Data
public class LoginUser implements UserDetails {

    /**
     * 用户
     */
    private SysUser user;
	/**
	 * 权限
	 */
    private List<String> perms;

    // 忽略JSON序列化
    @JsonIgnore
    private Collection<? extends GrantedAuthority> grantedAuthorities;

    /**
     * 构造方法
     * @param user 赋值
     */
    public LoginUser(SysUser user, List<String> perms){
        this.user = user;
        this.perms = perms;
    }

    /**
     * 权限
     * @return 权限列表
     */
    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 判断grantedAuthorities不为空,则直接返回
        if(!Objects.isNull(grantedAuthorities)){
            return grantedAuthorities;
        }
		// 为空,则遍历perms
        grantedAuthorities = perms.stream().map(SimpleGrantedAuthority::new).toList();
        return grantedAuthorities;
    }

}

在这里我们获取到了该用户的所有权限,最后我们还需要去controller层的接口方法中,进行权限校验

package com.example.springsecuritydemo.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class helloController {


    @GetMapping("/hello")
    @PreAuthorize("hasAuthority('sys:data:insert')")		// 使用PreAuthorize进行校验权限
    public String hello(){
        return "你好";
    }

    @GetMapping("/select")
    @PreAuthorize("hasAuthority('sys:data:select')")		// 使用PreAuthorize进行校验权限
    public String select(){
        return "查询";
    }

}

@PreAuthorize内置一些方便的方法,例如

  1. hasRole('ROLE_NAME') 检查用户是否具有指定角色。
  2. hasAnyRole('ROLE1', 'ROLE2') 检查用户是否具有给定角色中的任意一个。
  3. hasAuthority('AUTHORITY_NAME') 检查用户是否具有指定权限。
  4. hasAnyAuthority('AUTHORITY1', 'AUTHORITY2') 检查用户是否具有给定权限中的任意一个。
  5. hasPermission(targetObject, 'permission') 检查用户是否具有特定对象的特定权限。

在controller层中,hello请求需要sys:data:insert权限,select请求需要sys:data:select权限。如果没有该权限访问时,则会报403错,但不会返回任何结果,只会返回403状态码,而我们需要没有权限时也返回json格式数据提示时,我们需要进行异常配置。

异常配置

首先我们需要开启方法鉴权,在SpringSecurity的配置类上方添加@EnableMethodSecurity注解,还需要自定义创建一个异常类继承AuthenticationException

package com.example.springsecuritydemo.exception;

import org.springframework.security.core.AuthenticationException;

/**
 * 自定义认证异常类
 */
public class CustomerAuthenticationException extends AuthenticationException {
    public CustomerAuthenticationException(String msg) {
        super(msg);
    }
}

接着,我们需要配置认证和授权处理器

1.认证时校验继承AuthenticationEntryPoint

package com.example.springsecuritydemo.handler;

import com.alibaba.fastjson.JSON;
import com.example.springsecuritydemo.em.HttpCode;
import com.example.springsecuritydemo.exception.CustomerAuthenticationException;
import com.example.springsecuritydemo.utils.ResultVoUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * 客户端进行认证数据的提交时出现异常,或匿名用户访问无权限资源时的处理器
 * 认证时校验
 */
@Component
public class AnonymousAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        // 设置返回类型
        response.setContentType("application/json;charset=utf-8");
        // 获取输出流
        ServletOutputStream outputStream = response.getOutputStream();
        String msg = "";
        int code = HttpCode.INTERNAL_SERVER_ERROR.getCode();
        if (exception instanceof AccountExpiredException){
            msg = "用户过期,登陆失败";
        }else if(exception instanceof BadCredentialsException){
            msg = "用户名或密码错误,登陆失败";
        }else if(exception instanceof CredentialsExpiredException){
            msg = "密码过期,登陆失败";
        }else if(exception instanceof DisabledException){
            msg = "用户被禁用,登陆失败";
        }else if(exception instanceof LockedException){
            msg = "用户被锁定,登陆失败";
        }else if(exception instanceof InternalAuthenticationServiceException){
            msg = "用户名为空,登录失败";
        }else if(exception instanceof CustomerAuthenticationException){
            msg = exception.getMessage();
            code = HttpCode.FAIL.getCode();
        }else{
            msg = "未知错误";
        }
        String result = JSON.toJSONString(ResultVoUtils.error(code, msg));
        outputStream.write(result.getBytes());
        outputStream.flush();
        outputStream.close();
    }
}

2.授权时校验继承AccessDeniedHandler

package com.example.springsecuritydemo.handler;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.springsecuritydemo.em.HttpCode;
import com.example.springsecuritydemo.utils.ResultVoUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 用户无权限访问处理器
 * 授权时校验
 */
@Component
public class CustomerAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 设置返回类型
        response.setContentType("application/json;charset=utf-8");
        // 获取输出流
        ServletOutputStream outputStream = response.getOutputStream();
        // 将返回的数据转为String类型
        String result = JSON.toJSONString(ResultVoUtils.error(HttpCode.UNAUTHORIZED), SerializerFeature.DisableCircularReferenceDetect);
        // 返回数据
        outputStream.write(result.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

配置完认证和授权处理器后,我们还需要让它们进行使用,还需在SpringSecurity的配置文件中进行配置,这里我们在上面已经配置过了

				// 添加自定义异常处理
                http.exceptionHandling(ex ->
                        // 认证用户无权限访问处理器
                        ex.accessDeniedHandler(customerAccessDeniedHandler)
                        // 客户端进行认证数据的提交时出现异常,或匿名用户访问无权限资源时的处理器
                        .authenticationEntryPoint(anonymousAuthenticationEntryPointHandler));

至此,SpringSecurity的安全框架差不多已经搭建完成了。

工具类Utils

JWT工具类

package com.example.springsecuritydemo.utils;

import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.util.StringUtils;

import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtils {
    //过期时间 一小时
    public static final long TOKEN_EXPIRE_TIME = 60 * 60 * 1000;
    //私钥,随机的uuid
    private static final String TOKEN_SECRET = "550543e5-5b72-497b-b4cb-0c13c3949df1";

    /**
     * 生成签名,60分钟过期
     * 根据内部改造,支持6中类型,Integer,Long,Boolean,Double,String,Date
     * @param map
     * @return
     */
    public static String createToken(Map<String,Object> map) {
        try {
            // 设置过期时间
            Date date = new Date(System.currentTimeMillis() + TOKEN_EXPIRE_TIME);
            // 私钥和加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            // 设置头部信息
            Map<String, Object> header = new HashMap<>(2);
            header.put("typ", "jwt");
            // 返回token字符串
            JWTCreator.Builder builder =  JWT.create()
                    .withHeader(header)
                    .withIssuedAt(new Date()) //发证时间
                    .withExpiresAt(date);  //过期时间
            //   .sign(algorithm);  //密钥
            // map.entrySet().forEach(entry -> builder.withClaim( entry.getKey(),entry.getValue()));
            map.forEach((key, value) -> {
                if (value instanceof Integer) {
                    builder.withClaim(key, (Integer) value);
                } else if (value instanceof Long) {
                    builder.withClaim(key, (Long) value);
                } else if (value instanceof Boolean) {
                    builder.withClaim(key, (Boolean) value);
                } else if (value instanceof String) {
                    builder.withClaim(key, String.valueOf(value));
                } else if (value instanceof Double) {
                    builder.withClaim(key, (Double) value);
                } else if (value instanceof Date) {
                    builder.withClaim(key, (Date) value);
                }
            });
            return builder.sign(algorithm);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 生成签名,60分钟过期
     * 根据内部改造,支持6中类型,Integer,Long,Boolean,Double,String,Date
     * @param o 对象
     * @param key 键
     * @return 秘钥
     */
    public static String createToken(String key,Object o) {
        try {
            // 设置过期时间
            Date date = new Date(System.currentTimeMillis() + TOKEN_EXPIRE_TIME);
            // 私钥和加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            // 设置头部信息
            Map<String, Object> header = new HashMap<>(2);
            header.put("typ", "jwt");
            // 返回token字符串
            JWTCreator.Builder builder =  JWT.create()
                    .withHeader(header)
                    .withIssuedAt(new Date()) //发证时间
                    .withExpiresAt(date)  //过期时间
                    .withClaim(key, JSON.toJSONString(o));
            return builder.sign(algorithm);
        } catch (Exception e) {
            return null;
        }
    }


    /**
     * 检验token是否正确
     * @param **token**
     * @return
     */
    public static boolean verify(String token){
        try {
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(token);
            return true;
        } catch (Exception e){
            return false;
        }
    }

    /**
     *获取用户自定义Claim集合
     * @param token
     * @return
     */
    public static Map<String, Claim> getClaims(String token){
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        JWTVerifier verifier = JWT.require(algorithm).build();
        return verifier.verify(token).getClaims();
    }

    /**
     *获取用户自定义根据token和字符串拿到String数据
     * @param token
     * @return String
     */
    public static String getString(String token,String z) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(z).asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     *获取用户自定义根据token和字符串拿到对象
     * @param token Jwt生成的token
     * @param z token生成的时的key
     * @param tClass 返回类型
     * @return String
     */
    public static <T> T getObject(String token, String z, Class<T> tClass) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            String o = jwt.getClaim(z).asString();
            return JSON.parseObject(o,tClass);
        } catch (JWTDecodeException e) {
            return null;
        }
    }
    /**
     *获取用户自定义根据token和字符串拿到Integer数据
     * @param token
     * @return Integer
     */
    public static Integer getInteger(String token,String z) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(z).asInt();
        } catch (JWTDecodeException e) {
            return null;
        }
    }


    /**
     * 获取过期时间
     * @param token
     * @return
     */
    public static Date getExpiresAt(String token){
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        return  JWT.require(algorithm).build().verify(token).getExpiresAt();
    }

    /**
     * 获取jwt发布时间
     */
    public static Date getIssuedAt(String token){
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        return  JWT.require(algorithm).build().verify(token).getIssuedAt();
    }

    /**
     * 验证token是否失效
     *
     * @param token
     * @return true:过期   false:没过期
     */
    public static boolean isExpired(String token) {
        try {
            final Date expiration = getExpiresAt(token);
            return expiration.before(new Date());
        }catch (TokenExpiredException e) {
            return true;
        }

    }

    /**
     * 直接Base64解密获取header内容
     * @param token
     * @return
     */
    public static String getHeaderByBase64(String token){
        if (StringUtils.isEmpty(token)){
            return null;
        }else {
            byte[] header_byte = Base64.getDecoder().decode(token.split("\\.")[0]);
            return new String(header_byte);
        }

    }

    /**
     * 直接Base64解密获取payload内容
     * @param token
     * @return
     */
    public static String getPayloadByBase64(String token){

        if (StringUtils.isEmpty(token)){
            return null;
        }else {
            byte[] payload_byte = Base64.getDecoder().decode(token.split("\\.")[1]);
            return new String(payload_byte);
        }

    }
}

Redis工具类

package com.example.springsecuritydemo.utils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

@Component
public final class RedisUtils {

    /**
     * 存入redis的token前缀
     */
    public final String LOGIN_TOKEN = "LOGIN_TOKEN:";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(String.valueOf(CollectionUtils.arrayToList(key)));
            }
        }
    }

    // ============================String=============================
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    // ================================Map=================================
    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }

    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }

    // ============================set=============================
    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ===============================list=================================

    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束 0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

统一返回格式工具类

package com.example.springsecuritydemo.utils;

import com.example.springsecuritydemo.em.HttpCode;
import com.example.springsecuritydemo.entity.vo.ResultVo;


/**
 * 返回结果工具类
 */
public class ResultVoUtils {

    /**
     * 返回成功
     * @param data 数据
     */
    public static <T> ResultVo<T> success(T data) {
        return new ResultVo<>(HttpCode.SUCCESS.getCode(), HttpCode.SUCCESS.getMsg(), data);
    }

    /**
     * 返回成功
     * @param data 数据
     */
    public static ResultVo success(int data){
        return new ResultVo(HttpCode.SUCCESS.getCode(), HttpCode.SUCCESS.getMsg(), data);
    }

    /**
     * 返回成功
     * @param msg 自定义消息
     */
    public static ResultVo success(String msg){
        return new ResultVo(HttpCode.SUCCESS.getCode(), msg);
    }

    /**
     * 返回成功
     * @param msg 自定义消息
     * @param data 数据
     */
    public static <T>ResultVo<T> success(String msg, T data){
        return new ResultVo(HttpCode.SUCCESS.getCode(), msg, data);
    }

    /**
     * 返回失败
     * @param httpCode 状态码
     */
    public static ResultVo error(HttpCode httpCode){
        return new ResultVo(httpCode.getCode(), httpCode.getMsg());
    }

    /**
     * 返回失败
     * @param msg 自定义消息
     */
    public static ResultVo error(String msg){
        return new ResultVo(HttpCode.FAIL.getCode(), msg);
    }

    /**
     * 返回失败
     * @param msg 自定义消息
     */
    public static ResultVo error(int code,String msg){
        return new ResultVo(code, msg);
    }


}

统一返回格式

package com.example.springsecuritydemo.entity.vo;

import lombok.Data;

import java.io.Serializable;

@Data
public class ResultVo<T> implements Serializable {
    private Integer code;
    private String msg;
    private T data;

    /**
     * 构造函数
     * @param code 状态码
     * @param msg 消息
     * @param data 数据
     */
    public ResultVo(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    /**
     * 构造函数
     * @param code 状态码
     * @param msg 消息
     */
    public ResultVo(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }


    /**
     * 构造函数
     * @param code 状态码
     * @param data 数据
     */
    public ResultVo(Integer code, T data){
        this(code, null, data);
    }
}

枚举Code码

package com.example.springsecuritydemo.em;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 *  请求状态码
 */
@Getter
@AllArgsConstructor
public enum HttpCode {
    SUCCESS(200, "操作成功"),
    FAIL(201, "操作失败"),
    UNAUTHORIZED(401, "用户未被授予该权限"),
    NOT_FOUND(404, "未找到资源"),
    METHOD_NOT_ALLOWED(405, "不允许的请求方法"),
    INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
    BAD_REQUEST(400, "请求参数错误"),
    UNSUPPORTED_MEDIA_TYPE(415, "不支持的媒体类型"),
    FORBIDDEN(403, "禁止访问"),
    NOT_ACCEPTABLE(406, "请求头信息不匹配"),
    CONFLICT(409, "请求冲突"),
    UNPROCESSABLE_ENTITY(422, "请求参数校验失败"),
    TOO_MANY_REQUESTS(429, "请求次数过多"),
    ANONYMOUS_FORBID(407, "匿名用户禁止访问,请先登录");

    private final int code;

    private final String msg;

}

包位置

-java 
 -example
  -springsecuritydemo
   -config	//配置文件
	-RedisConfig
	-SecurityConfig
   -controller // 控制层
	-helloController
	-LoginController
   -em	//枚举类型
	-HttpCode
   -entity //实体类
	-vo
	 -LoginUser
	 -ResultVo
	-SysUser
   -exception //自定义异常类
	-CustomerAuthenticationException
   -filter //自定义过滤器
	-JwtAuthenticationTokenFilter
   -handler //自定义处理器
	-AnonymousAuthenticationEntryPointHandler
	-CustomerAccessDeniedHandler
   -mapper 数据层
	-SysUserMapper
   -service 业务层
	-impl 业务实现层
	 -LoginServiceImpl
	 -UserDetailsServiceImpl
   -utils 工具
	-JwtUtils
	-RedisUtils
	-ResultVoUtils
   -SpringSecurityDemoApplication	SpringBoot启动类