SpringSecurity6.1

SpringSecurity6.1

简介

SpringSecurity中文文档

Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比 Shiro 丰富。

Spring Security是一个 功能强大且高度可定制 的,主要负责为 Java 程序提供声明式的身份验证访问控制安全框架。其前身是 Acegi Security ,后来被收纳为 Spring 的一个子项目,并更名为了 Spring Security。

Spring Security 的 底层主要是基于 Spring AOPServlet 过滤器 来实现安全控制 ,它提供了全面的安全解决方案,同时授权粒度可以在 Web 请求级和方法调用级来处理身份确认和授权。

SpringSecurity 核心功能

  • 认证(Authentication): 用户身份验证(Authentication): 验证用户的身份,确保用户是其所声明的身份。 自定义认证逻辑: 允许开发者定义自己的身份验证逻辑,以适应特定的应用程序需求。

  • 授权(Authorization): 访问控制(Access Control): 定义了用户对应用程序中受保护资源的访问权限。 角色和权限: Spring Security 支持基于角色和权限的授权机制,允许开发者定义用户的角色和相应的权限。

  • 防护攻击(Protection against common attacks): 防止 CSRF 攻击: 提供了防止跨站请求伪造(CSRF)攻击的机制。

  • 加密功能(Encryption Features): 密码加密: 提供了密码加密的支持,确保存储在数据库中的用户密码是安全的。

  • 会话功能(Session Management): 会话跟踪: 跟踪用户会话状态,可以配置会话超时等相关属性。 会话固定保护: 防止会话固定攻击,确保用户会话的安全性。

  • RememberMe功能: Remember-Me认证: 允许用户选择在登录后保持持久的认证状态,避免在每次访问时都要重新登录。 Token 持久化: 使用 Remember-Me Token 来实现持久的认证状态。

目前最新的版本是 6.2.0,提供了许多新功能,需使用 JDK 17 及以上版本。

1、SpringSecurity6 版本 新特性

首先该文章所使用的技术版本为:

  • SpringBoot:3.1.5

  • SpringSecurity:6.1.5

  • JDK : 17

  • MySQL : 8.2.0

  • Mybatis-plus : 3.5.4

  • jwt / redis

接下来我介绍一下新特性

1. WebSecurityConfigurerAdapter

首先第一点就是在 SpringSecurity6.1 中 WebSecurityConfigurerAdapter 已经被完全移除了,用不了一点。

还有就是.and()也不能用了。而且配置类中要使用 Lambda 表达式来书写

2.配置 SecurityFilterChain


@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http
          .authorizeHttpRequests((authz) -> authz
              .anyRequest().authenticated()
          )
          .httpBasic(withDefaults());
  }
}

以后就要改写为下面这个写法:

@Bean
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
       http.csrf(csrf -> csrf.disable());
       //放行登录请求地址
       http.authorizeHttpRequests(auth -> auth.requestMatchers("/auth/login").permitAll()
              .anyRequest().authenticated());
       return http.build();
  }

3.关于 AuthenticationManager 的获取以前可以通过重写父类的方法来获取这个 Bean,类似下面这样:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   @Override
   @Bean
   public AuthenticationManager authenticationManagerBean() throws Exception {
       return super.authenticationManagerBean();
  }
}

现在要改为下面这个写法:

/**
 * AuthenticationManager:负责认证的
 * DaoAuthenticationProvider:负责将userDetailsService,PasswordEncoder融合进去
 */
   @Bean
   public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){

       DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
       daoAuthenticationProvider.setUserDetailsService(userDetailsService);
       //关联加密编码器
       daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
       //将daoAuthenticationProvider放进ProviderManager中,包含进去
       ProviderManager providerManager = new ProviderManager(daoAuthenticationProvider);
       return providerManager;
  }
   @Bean
   public PasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
  }

除了上面的方法外还能从 HttpSecurity 提取出 AuthenticationManager

@Configuration
public class SpringSecurityConfiguration {

   AuthenticationManager authenticationManager;

   @Autowired
   UserDetailsService userDetailsService;

   @Bean
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

       AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
       authenticationManagerBuilder.userDetailsService(userDetailsService);
       authenticationManager = authenticationManagerBuilder.build();
       return http.build();
  }
}

2、入门示例搭建(基于内存来实现,并非数据库)

2.1 项目依赖

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.moon</groupId>
<artifactId>db_SpringSecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>db_SpringSecurity</name>
<description>db_SpringSecurity</description>
<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.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.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

2.2 创建一个 web 访问接口

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

   @GetMapping("/hello")
   public String hello() {
       return "hello spring security";
  }
}
  • 运行项目之后控制台会打印出密码(密码是由 UUID 生成的),页面会自动跳转到/login地址,在 SpringBoot 中应遵循约定大于配置的规则,项目加载了 SpringSecurity 依赖包就会自动安全限制访问,用户名默认为user

2.3 配置 Spring Security 账户密码

从上面的源码分析可知,默认的登录密码是利用 UUID 生成的随机字符串,很明显如果我们使用这个字符串作为登录密码,就太麻烦了。那么有没有更方便的登录账户呢?

Spring Security 框架允许我们自己配置用户名和密码,并且提供了 2 种方式来进行自定义用户名和密码:

  • 在配置文件中定义

  • 在配置类中定义

2.3.1 在配置文件中定义

application.yml配置文件新增以下内容:

spring:
security:
  user:
    name: admin
    password: 123456

2.3.2 在配置类中定义(基于内存)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * 使用的是 SpringSecurity6.1.5,配置类有一些变化
 * 1、该类不再需要继承其他的Security定义的类
 * 2、需要使用 @Configuration 才会被Spring容器加载
 * 3、废弃了很多方法,比如and()方法,建议使用Lambda表示实现
*/
@Configuration
// 标记为一个Security类,启用SpringSecurity的自定义配置
@EnableWebSecurity
public class SecurityConfig {

   // 自定义用户名和密码
   // UserDetailsService:根据用户名加载用户,找到的话返回用户信息【UserDetails类型】
   // UserDetails:存储了用户的信息
 @Bean
 public UserDetailsService userDetailsService() {
     // 定义用户信息
     // 构建管理员
     UserDetails adminUser = User.withUsername("admin")
    .password("$2a$10$csvUZnj/VG6wBkooT/mewO.WbJesVCiHqEoWTyQrOYTKJvk3xpQb6")
    .roles("admin", "user")
    .build();
     // 构建普通用户
     UserDetails vipUser = User.withUsername("user")
    .password("$2a$10$csvUZnj/VG6wBkooT/mewO.WbJesVCiHqEoWTyQrOYTKJvk3xpQb6")
    .roles("user")
    .build();
     // 将用户存储到SpringSecurity中
     InMemoryUserDetailsManager userDetailsManager = new
     InMemoryUserDetailsManager();
     // 创建两个用户,SpringSecurity在运行时就会知道有两个用户
     userDetailsManager.createUser(adminUser);
     userDetailsManager.createUser(vipUser);
     return userDetailsManager;
  }
}

UserDetailsService:提供查询用户功能,如根据用户名查询用户,并返回 UserDetails UserDetails:记录用户信息,如用户名,密码,权限等

SpringSecurity 提供密码加密工具:PasswordEncoder,具体实现又很多,此处使用BCryptPasswordEncoder

@Bean
   public PasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
  }

密码加密

String pass = "123456"; String result = passwordEncoder.encode(pass);

密码匹配我们可以使用 matches 方法

boolean isTrue = passwordEncoder.matches("111111",result); System.out.println("isTrue===>" + isTrue);

3、认证

3.1 登录认证校验流程(包含 jwt)

图片[1]| SpringSecurity6.1| 小妖客栈

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

3.1.1 认证流程讲解

图片[2]| SpringSecurity6.1| 小妖客栈

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

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

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

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

3.1.2 SpringSecurity 完整流程

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

图片[3]| SpringSecurity6.1| 小妖客栈 图中展示 UsernamePasswordAuthenticationFilter 过滤器:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。 当然还有别的过滤器

eg: ExceptionTranslationFilter: 处理过滤器链中抛出的任何 AccessDeniedException 和 AuthenticationException FilterSecurityInterceptor: 负责权限校验的过滤器。

4、授权

4.1 授权的作用:

  • 一个后台管理系统中有很多不同的角色,每个不同的角色肯定对应不同的功能,然后角色也对应着不同的用户,这就是授权要去实现的效果,而且我们后面要去实现动态权限。也是根据这个思路进行实现的,采用 RBAC 权限模型

  • 用户认证之后,会去存储用户对应的权限,并且给资源设置对应的权限,SpringSecurity 支持两种粒度 的权限

    • 基于请求的:在配置文件中配置路径,可以使用**的通配符

    • 基于方法的:在方法上使用注解实现

    • 动态权限:用户权限被修改之后,不需要用户退出,会自动刷新,也不需要修改代码

4.1.1 基于方法鉴权

在 SpringSecurity6 版本中@EnableGlobalMethodSecurity 被弃用,取而代之的是@EnableMethodSecurity。默认情况下,会激活 pre-post 注解,并在内部使用 AuthorizationManager。

4.1.2 新老 API 区别

此@EnableMethodSecurity 替代了@EnableGlobalMethodSecurity。提供了以下改进:

  1. 使用简化的 AuthorizationManager。

  2. 支持直接基于 bean 的配置,而不需要扩展 GlobalMethodSecurityConfiguration

  3. 使用 Spring AOP 构建,删除抽象并允许您使用 Spring AOP 构建块进行自定义

  4. 检查是否存在冲突的注释,以确保明确的安全配置

  5. 符合 JSR-250

  6. 默认情况下启用@PreAuthorize、@PostAuthorize、@PreFilter 和@PostFilter

4.2 请求级别和方法级别对比

  请求级别 方法级别
授权类型 粗细度 细粒度
配置位置 在配置类中配置 在方法上配置
配置样式 DSL 注解
授权定义 编程式 SpEL 表达式

主要的权衡似乎是您希望您的授权规则位于何处。重要的是要记住,当您使用基于注释的方法安全性 时,未注释的方法是不安全的。为了防止这种情况,请在 HttpSecurity 实例中声明一个兜底授权规则。

如果方法上也定义了权限,则会覆盖类上的权限

注意:使用注解的方式实现,如果接口的权限发生变化,需要修改代码了。后期会学习动态权限,无需 修改代码就可以实现接口权限的修

5、集成数据库实现认证和授权(实践了!!)

其中简单的流程文章上面就不放了,eg:创建 Maven 项目、配置 mysql、创建 service.....等

  • 数据库

CREATE TABLE `ums_sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT
NULL COMMENT '用户账号',
`nickname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT
NULL COMMENT '用户昵称',
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT
'' COMMENT '用户邮箱',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT
'' COMMENT '手机号码',
`sex` int DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
`avatar` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT
'' COMMENT '头像地址',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci
DEFAULT '' COMMENT '密码',
`status` int DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`creator` bigint DEFAULT '1' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`updater` bigint DEFAULT '1' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT
NULL COMMENT '备注',
`deleted` tinyint DEFAULT '0',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='后台用户表';
  • 创建实体类(实体类需要实现 UserDetails,是 SpringSecurity 的)

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Collection;
// @Data注解可以自动生成getter、setter、toString
// SpringSecurity会将认证的用户信息存储到UserDetails中
@Data
@TableName("ums_sys_user")
public class SysUser implements Serializable, UserDetails {
     @TableId
     private Long id;
     private String username;
     private String nickname;
     private String email;
     private Integer sex;
     private String avatar;
     private String password;
     private Integer status;
     private Long creator;
     private Long updater;
     private LocalDateTime createTime;
     private LocalDateTime updateTime;
     @TableLogic
     private Integer deleted;
     private String remark;
     //权限信息
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {
       return null;
      }
     @Override
     public String getPassword() {
         return password;
      }
     @Override
     public String getUsername() {
         return username;
      }
     @Override
     public boolean isAccountNonExpired() {
         return status == 0;
      }
     @Override
     public boolean isAccountNonLocked() {
         return status == 0;
      }
     @Override
     public boolean isCredentialsNonExpired() {
         return status == 0;
      }
     @Override
     public boolean isEnabled() {
         return status == 0;
      }
}

5.1 认证实现流程

  • 创建一个 UserDetailsService 实现 SpringSecurity 的 UserDetailsService 接口

    • 写的是查询用户信息的接口public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException

  • 通过配置类对 AuthenticationManager 与自定义的 UserDetailsService 进行关联

    • SpringSecurity 是通过 AuthenticationManager 实现的认证,会判断用户名和密码对不对

  • 在登录方法所在的类中注入 AuthenticationManager,调用 authenticate 实现认证逻辑

  • 认证之后返回认证后的用户信息

SysUserDetailsService:

@Service
@Slf4j
public class SysUserDetailsService implements UserDetailsService {

   @Autowired(required = false)
   private SysUserMapper sysUserMapper;

   /**
    * 根据用户名去查询用户,如果没有查询到则会抛出异常 UsernameNotFoundException
    * 返回UserDetails,SpringSecurity定义的类,用来存储用户信息
    * SysUser实现了UserDetails接口,根据多态,他就是一个UserDetails
    */
   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       //查询用户信息
       SysUser sysUser = sysUserMapper.selectUserByUsername(username);
       log.info("SysUser---->>{}",sysUser);
           // TODO 后面可以实现对权限、角色的查询,权限查询出来后设置进实体类中
      }
       return sysUser;
  }
}

LoginController:

package com.moon.controller;

import com.moon.domain.dto.SysUserDto;
import com.moon.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

/**
* @author:Y.0
* @date:2023/11/20
*/

@RestController
@RequestMapping("/auth")
@Slf4j
public class LoginController {

   @Autowired
   private  SysUserService userService;

   /**
    * 登录方法:返回一个token令牌给前端
    * 用户再次访问的时候,在请求头header中携带token
    * SysUserDto 中只含有username,password字段
    */
   @PostMapping("/login")
   public String doLogin(@RequestBody SysUserDto userDto){
       String token =  userService.doLogin(userDto);
       return token ;
  }
}

SysUserServiceImpl:

@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser>
   implements SysUserService{

   @Autowired(required = false)
   private AuthenticationManager authenticationManager;

   @Override
   public String doLogin(SysUserDto userDto) {
       // 传入用户名和密码 将是否认证标记设置为false
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDto.getUsername(),userDto.getPassword());

       //实现登录逻辑此时会去调用loadUserByUsername
       //返回的Authentication其实就是UserDetails
       Authentication authenticate = null;
       try {
           // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
           authentication = authenticationManager.authenticate(authenticationToken);
      } catch (BadCredentialsException e) {
           throw new ServiceException("密码不正确");
      } catch (UsernameNotFoundException e) {
           throw new ServiceException("用户名不存在");
      } catch (Exception e) {
           throw new ServiceException(e.getMessage());
      }
       SysUser sysUser = (SysUser) authenticate.getPrincipal();
       if (sysUser == null){
           return "用户名或密码错误";
      }

       // 生成一个token,返回给前端
       String token = UUID.randomUUID().toString().replaceAll("-", "")
       return token;
  }
}

SecurityConfig:

import com.moon.service.impl.SysUserDetailsService;
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.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.configuration.EnableWebSecurity;
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开启方法权限控制类 @PreAuthorize
*
* @author:Y.0
* @date:2023/11/20
*/

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

   @Autowired
   private SysUserDetailsService userDetailsService;

   @Bean
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
       http.csrf(csrf -> csrf.disable());
       //放行登录请求地址
       http.authorizeHttpRequests(auth -> auth.requestMatchers("/auth/login").permitAll()
              .anyRequest().authenticated());
       return http.build();
  }


   /**
    * AuthenticationManager:负责认证的
    * DaoAuthenticationProvider:负责将userDetailsService,PasswordEncoder融合进去
    *
    * @param passwordEncoder
    * @return
    */
   @Bean
   public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){

       DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
       daoAuthenticationProvider.setUserDetailsService(userDetailsService);
       //关联加密编码器
       daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
       //将daoAuthenticationProvider放进ProviderManager中,包含进去
       ProviderManager providerManager = new ProviderManager(daoAuthenticationProvider);
       return providerManager;
  }

   @Bean
   public PasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
  }

}

5.2 认证原理

SpringSecurity 的认证是通过 AuthenticationManager 的 authenticate 方法实现,该方法接收一个 Authentication 对象,通过也返回一个 Authentication 对象,Authentication 中存储用户的主体【账 号】、密码、权限等信息。 同时 AuthenticationManager 是一个接口,上述的例子是通过他的常用实现类 ProviderManager 实现 的,所以看明白 ProviderManager 的 authenticate 方法就可以了。

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDto.getUsername(),userDto.getPassword());

首先上述代码是构建一个 Authentication 对象。通过 UsernamePasswordAuthenticationToken 这个实 现类。该类主要是将用户名和密码读取进来,并进行存储,并设置认证标记为 false。具体的认证代码如 下:

Authentication authenticate =authenticationManager.authenticate(authentication);

首先进入 ProviderManager 类中,执行 authenticate 方法,方法最后将用户返回 图片[4]| SpringSecurity6.1| 小妖客栈

接下来看 result = provider.authenticate(authentication)执行,具体执行的是AbstractUserDetailsAuthenticationProvider类中的方法 图片[5]| SpringSecurity6.1| 小妖客栈

retrieveUser 方法的逻辑,具体也是执行 loadUserByUsername 方法,去根据用户名查找用户信息

  • 整个逻辑梳理:

    • UsernamePasswordAuthenticationToken 将用户填写的用户名密码存储下来,设置认证状态为 false,它就是一个 Authentication 类型的对象

    • 调用 AuthenticationManager.authenticate(Authentication authentication)进行用户认证,并返回 Authentication,其中记录用户信息和认证状态

1、首先执行 loadUserByUsername 方法,根据用户名获取用户

user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);

2、 再判断用户状态

this.preAuthenticationChecks.check(user);

public void check(UserDetails user) {
         //检查用户是否锁定
if (!user.isAccountNonLocked()) {
this.logger.debug("Failed to authenticate since user account is locked");
throw new LockedException(
this.messages.getMessage("AccountStatusUserDetailsChecker.locked", "User account is locked"));
}
       //检查用户是否可用
if (!user.isEnabled()) {
this.logger.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(
this.messages.getMessage("AccountStatusUserDetailsChecker.disabled", "User is disabled"));
}
       //查询用户是否过期
if (!user.isAccountNonExpired()) {
this.logger.debug("Failed to authenticate since user account is expired");
throw new AccountExpiredException(
this.messages.getMessage("AccountStatusUserDetailsChecker.expired", "User account has expired"));
}
        //查询用户密码是否锁定
if (!user.isCredentialsNonExpired()) {
this.logger.debug("Failed to authenticate since user account credentials have expired");
throw new CredentialsExpiredException(this.messages
.getMessage("AccountStatusUserDetailsChecker.credentialsExpired", "User credentials have expired"));
}
}

3、 再判断密码是否正确

additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);

protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
       //获取用户输入的密码
String presentedPassword = authentication.getCredentials().toString();
       //匹配密码是否正确使用matches
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}

4、如果失败则重试,最终错误抛出异常,正确则返回 Authentication

5.3 授权(首先可用先了解下 RBAC 权限模型)

  • RBAC【Role-based access control】,增加了角色的概念,即基于角色的访问控制,将权限分配给角 色,再将角色分配给用户。简单来说,就是【用户关联角色,角色关联权限】。

RBAC 遵循三条安全原则:

  • 最小权限原则:给角色配置最小但能满足使用需求的权限。

  • 责任分离原则:给比较重要或者敏感的事件设置不同的角色,不同的角色间是相互约束的,由其一 同参与完成。

  • 数据抽象原则:每个角色都只能访问其需要的数据,而不是全部数据,不同的角色能访问到的数据 也不同。

  • 角色表

CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色id',
`role_label` varchar(255) DEFAULT NULL COMMENT '角色标识',
`role_name` varchar(255) DEFAULT NULL COMMENT '角色名字',
`sort` int DEFAULT NULL COMMENT '排序',
`status` int DEFAULT NULL COMMENT '状态:0:可用,1:不可用',
`deleted` int DEFAULT NULL COMMENT '是否删除:0: 未删除,1:已删除',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci
  • 权限表

CREATE TABLE `sys_menu` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父id',
`menu_name` varchar(255) DEFAULT NULL COMMENT '菜单名',
`sort` int DEFAULT '0' COMMENT '排序',
`menu_type` int DEFAULT NULL COMMENT '类型:0,目录,1菜单,2:按钮',
`perms` varchar(255) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(255) DEFAULT NULL COMMENT '图标',
`deleted` int DEFAULT NULL COMMENT '是否删除',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci;
  • 用户角色表

CREATE TABLE `sys_user_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_id` bigint NOT NULL COMMENT '用户id',
`role_id` bigint NOT NULL COMMENT '角色id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  • 角色权限表

CREATE TABLE `sys_role_menu` (
`id` bigint NOT NULL AUTO_INCREMENT,
`role_id` bigint DEFAULT NULL,
`menu_id` bigint DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

1、SpringSecurity 权限认证 SpringSecurity 要求将身份认证信息存到 GrantedAuthority 对象列表中。代表了当前用户的权限。 GrantedAuthority 对象由 AuthenticationManager 插入到 Authentication 对象中,然后在做出授权决策 时由 AccessDecisionManager 实例读取。

AuthorizationManager 实例通过该方法来获得 GrantedAuthority。通过字符串的形式表示, GrantedAuthority 可以很容易地被大多数 AuthorizationManager 实现读取。如果 GrantedAuthority 不 能精确地表示为 String,则 GrantedAuthorization 被认为是复杂的,getAuthority()必须返回 null 2、权限流程

  • 登陆的时候需要查询用户权限,基于 RBAC 模型实现的权限设计

    • 先获取用户的角色,根据角色获取用户权限

    • 因为 SpringSecurity 识别权限的数据类型时 String,所以我们需要将查询出的权限对象,封装 到 String 类型的集合中【Set 集合,数据不可重复】

  • 后续操作的时候,需要携带登陆的标记【token 或者 cookie】,根据 token 获取用户的信息,在后 续的操作过程中继续识别权限

    • 后续的操作尽量不要每访问一次就查一次数据库,对数据库压力很大

  • 用户实体类

private Integer delFlag;

//角色信息
private Set<SysRole> roleSet = new HashSet<>();

//权限信息
private Set<String> perms = new HashSet<>();

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (perms != null && perms.size() > 0){
//将权限类型转换并告知SpringSecurity,将Set<String>转为Collection<GrantedAuthority>,通过lambda表达式来实现
return perms.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
}
return null;
}
  • 剩下两个实体类和数据库字段对应就可以了,不做修改

查询用户权限信息(完成前面我们写的那个 TODO)

@Service
@Slf4j
public class SysUserDetailsService implements UserDetailsService {

   @Autowired(required = false)
   private  SysUserMapper sysUserMapper;

   @Autowired(required = false)
   private SysMenuMapper sysMenuMapper;

   /**
    * 根据用户名去查询用户,如果没有查询到则会抛出异常 UsernameNotFoundException
    * 返回UserDetails,SpringSecurity定义的类,用来存储用户信息
    * SysUser实现了UserDetails接口,根据多态,他就是一个UserDetails
    */
   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

       //查询用户信息
       SysUser sysUser = sysUserMapper.selectUserByUsername(username);
       log.info("SysUser---->>{}",sysUser);
       if (sysUser != null){
           Set<SysRole> sysRoleSet = sysUser.getRoleSet();
           Set<Long> roleIds = new HashSet<>();
           for (SysRole sysRole : sysRoleSet) {
               roleIds.add(sysRole.getRoleId());
          }
           //权限的查询
           Set<String> permsSet = sysUser.getPerms();
           Set<SysMenu> menus = sysMenuMapper.selectMenuByRoleId(roleIds);
           for (SysMenu menu : menus) {
               String perms = menu.getPerms();
               //把权限添加到用户中
               permsSet.add(perms);
          }

           log.info("权限---->>{}",sysUser);
      }
       return sysUser;
  }
}
  • 查询角色 sql

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
       PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.moon.mapper.SysUserMapper">
   <resultMap id="BaseResultMap" type="com.moon.domain.entity.SysUser">
           <id property="id" column="id" jdbcType="BIGINT"/>
           <result property="userName" column="user_name" jdbcType="VARCHAR"/>
           <result property="nickName" column="nick_name" jdbcType="VARCHAR"/>
           <result property="password" column="password" jdbcType="VARCHAR"/>
           <result property="status" column="status" jdbcType="CHAR"/>
           <result property="email" column="email" jdbcType="VARCHAR"/>
           <result property="phonenumber" column="phonenumber" jdbcType="VARCHAR"/>
           <result property="sex" column="sex" jdbcType="CHAR"/>
           <result property="avatar" column="avatar" jdbcType="VARCHAR"/>
           <result property="createBy" column="create_by" jdbcType="BIGINT"/>
           <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
           <result property="updateBy" column="update_by" jdbcType="BIGINT"/>
           <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
           <result property="delFlag" column="del_flag" jdbcType="INTEGER"/>
           <collection property="roleSet" resultMap="RoleMap"/>
   </resultMap>

       <resultMap id="RoleMap" type="com.moon.domain.entity.SysRole">
               <id property="roleId" column="role_id"/>
               <result property="roleLabel" column="role_label" />
               <result property="roleName" column="role_name"/>
               <result property="sort" column="sort" />
               <result property="status" column="status" />
               <result property="deleted" column="deleted" />
               <result property="remark" column="remark"/>
               <result property="createTime" column="create_time" />
               <result property="updateTime" column="update_time"/>
       </resultMap>
       <select id="selectUserByUsername" resultMap="BaseResultMap">
              SELECT
                      a.id,
                      a.user_name,
                      a.nick_name,
                      a.`password`,
                      a.email,
                      a.`status`,
                      a.phonenumber,
                      a.sex,
                      a.avatar,
                      a.create_by,
                      a.create_time,
                      a.update_by,
                      a.update_time,
                      a.del_flag,
                      c.role_id,
                      c.role_label,
                      c.role_name,
                      c.sort,
                      c.`status`,
                      c.deleted,
                      c.remark,
                      c.create_time,
                      c.update_time
              FROM
                      sys_user AS a
                              left JOIN
                      sys_user_role AS b
                      ON
                              a.id = b.user_id
                              left JOIN
                      sys_role AS c
                      ON
                              b.role_id = c.role_id
              where a.del_flag = 0 and c.deleted = 0 and a.user_name = #{username}
       </select>
</mapper>

查询权限 sql

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.moon.mapper.SysMenuMapper">

   <resultMap id="BaseResultMap" type="com.moon.domain.entity.SysMenu">
           <id property="id" column="id" jdbcType="BIGINT"/>
           <result property="parentId" column="parent_id" jdbcType="BIGINT"/>
           <result property="menuName" column="menu_name" jdbcType="VARCHAR"/>
           <result property="sort" column="sort" jdbcType="INTEGER"/>
           <result property="menuType" column="menu_type" jdbcType="INTEGER"/>
           <result property="perms" column="perms" jdbcType="VARCHAR"/>
           <result property="icon" column="icon" jdbcType="VARCHAR"/>
           <result property="deleted" column="deleted" jdbcType="INTEGER"/>
           <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
           <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
   </resultMap>
   <select id="selectMenuByRoleId" resultType="com.moon.domain.entity.SysMenu">
       select m.id,
              m.parent_id,
              m.menu_name,
              m.sort,
              m.menu_type,
              m.perms,
              m.icon,
              m.deleted,
              m.create_time,
              m.update_time
           from sys_menu m left join sys_role_menu rome on m.id = rome.menu_id
       where rome.role_id in
       <foreach collection="roleIds" open="(" close=")" separator="," item="roleId">
           #{roleId}
       </foreach>
   </select>
</mapper>

5.4 携带登录信息(jwt 令牌 token)

HTTP 协议是无状态请求,即一次会话不会记录上一次会话的内容。所以需要通过携带数据的方式告知服 务器我是谁,以便服务器知道这个人有没有权限访问这个数据。最初使用 cookie 的方式携带

  • cookie Cookie,有时也用其复数形式 Cookies。类型为“小型文本文件”,是某些网站为了辨别用户身份,进行 Session 跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存 的信息 。身份信息,订单信息,浏览记录,这样做 cookie 会越来越大,之后就出现 session 了

  • session 考虑将用户的信息存储到服务器端,请求时只携带可以识别用户身份的身份信息就可以了,再根据用户 身份获取用户的其他资料比如:实名信息,订单信息,访问记录等

  1. 首先是用户登录,服务端会生成一个 session,再生成该 session 的唯一标识 sessionId,这个 sessionId 可以得知是哪个用户,再将此 sessionId 通过 cookie 告知客户端

  2. 之后客户端的请求都再 cookie 中携带这个 sessionId,服务端获取 sessionId,找到对应的用户,再 去处理请求就可以了 session 的作用就是缩小了 cookie 的体积,并且 cookie 存储在客户端,session 存储在服务端。 随着网民增长,软件用户增加,服务器会采用分布式,集群等方式部署,保障可以为更多用户提供服 务,session 就会出现问题。即 session 如果在 A 服务器生成,后续请求如果发送到 B 服务器,则 B 服务器就无法得知是哪个用户

  • token【令牌】 作用:其实就是为了知道你是谁。还有一种方式就是将用户信息加密变成字符串,直接发给客户端,客 户端后边请求都携带这个加密字符串,我们称之为 Token【令牌】,服务端拿到字符串之后可以解密获 取到用户信息,这样就不需要查询数据库了。多个服务端加密算法相同,也就可以完成解密工作,这个 生成 Token 的技术可以选用 JWT【Json Web Tokens】。而且这个字符串我们会放到请求头【header】 中,这个字段根据自己的需求定义 JWT 可以将用户信息【id,username,avatar,权限。密码,支付密码千万别放】变成字符串,也可以 加密。之后再次请求时携带这个字符串,可以解析为用户信息。就不用访问数据库了

重点:

  • token:token 是一种识别用户身份的机制,基于这种机制可以有很多种实现,就是在登陆完成之 后,再请求头中添加用户标识。放到请求头中,再次访问的时候,服务器从请求头中获取 token, 判断用户是否是合法的

  • jwt:就是生成 token 的一种具体实现,它可以直接将用户信息存储到字符串中,也可以根据字符串 解析出用户信息,不需要再查询数据库了。也支持加密

5.4.1 JWT 结构

JWT 是一个很长的字符串,由三部分组成。但是 JWT 内部并没有换行,而是由.隔开

eyJhbGciOiJIUzI1NiJ9. eyJpZCI6MTAwMCwiYXZhdGFyIjoi5p2p5qyQ5qe45raT77-95raT7oGE44GU6Y2N5b-T5rm06Y2n77- 9IiwidXNlcm5hbWUiOiLlr67nirHnrIEifQ. 9sIcaoCsaT-WXMqLSPVtFHj_zKh6OpwEboUF_Qit6G4

  • Header(头部) Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子

    { "alg": "HS256", "typ": "JWT" } alg:表示签名使用的算法,默认为 HMAC SHA256(写为 HS256) typ:表示令牌的类型,JWT 令牌统一写为 JWT。 最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存。

  • Payload(负载) Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了 7 个官方字段,供选用

    • iss (issuer):签发人/发行人

    • sub (subject):主题

    • aud (audience):用户

    • exp (expiration time):过期时间

    • nbf (Not Before):生效时间,在此之前是无效的

    • iat (Issued At):签发时间

    • jti (JWT ID):用于标识该 JWT 除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

      { "sub": "1234567890", "name": "John Doe", "admin": true } 注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。 这个 JSON 对象也要使用 Base64URL 算法转成字符串。

  • Signature(签名) Signature 部分是对前两部分的签名,防止数据篡改。 首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。 算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.) 分隔,就可以返回给用户。

  • Base64URL 前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有 一些小的不同。 JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。 Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成 _ 。这就是 Base64URL 算法

5.4.2 引入jwt

  • 依赖

<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.1</version>
</dependency>
<!-- JDK8以上需要加入以下依赖 -->
<dependency>
   <groupId>javax.xml.bind</groupId>
   <artifactId>jaxb-api</artifactId>
   <version>2.3.1</version>
</dependency>
  • jwt工具

package com.moon.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

/**
* jwt工具
* @author:Y.0
* @date:2023/11/21
*/
@Component
public class JwtUtil {
   /**
    * 生成jwt
    * 使用Hs256算法, 私匙使用固定秘钥
    *
    * @param secretKey jwt秘钥
    * @param ttlMillis jwt过期时间(毫秒)
    * @param claims   设置的信息
    * @return
    */
   public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
       // 指定签名的时候使用的签名算法,也就是header那部分
       SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

       // 生成JWT的时间
       long expMillis = System.currentTimeMillis() + ttlMillis;
       Date exp = new Date(expMillis);

       // 设置jwt的body
       JwtBuilder builder = Jwts.builder()
               // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
              .setClaims(claims)
               // 设置签名使用的签名算法和签名使用的秘钥
              .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
               // 设置过期时间
              .setExpiration(exp);

       return builder.compact();
  }

   /**
    * Token解密
    *
    * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
    * @param token     加密后的token
    * @return
    */
   public static Claims parseJWT(String secretKey, String token) {
       // 得到DefaultJwtParser
       Claims claims = Jwts.parser()
               // 设置签名的秘钥
              .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
               // 设置需要解析的jwt
              .parseClaimsJws(token).getBody();
       return claims;
  }

}

5.4.3 鉴权

访问其他接口的时候携带token,在接口上添加权限校验【基于方法的权限校验】。我们就需要在请求 时获取请求头的token字段,我们需要自定义过滤器,并且将过滤器添加到SpringSecurity的过滤器链 中。【使用了责任链模式】

  • 自定义过滤器

package com.moon.filter;

import com.moon.domain.entity.SysUser;
import com.moon.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

/**
* 捕获请求中的请求头,获取token字段,判断是否可以获取用户信息
* 我们可以继承OncePerRequestFilter 抽象类
*
* 1、获取到用户信息之后,需要将用户的信息告知SpringSecurity,SpringSecurity会去判断你访问的接口是否有相应的权限,在SpringSecurityConfig中引用就可以
* 2、告知SpringSecurity 就是使用Authentication告知框架,SpringSecurity会将信息存储到SecurityContext中然后会放到--->SecurityContextHolder中
*
* 登录的时候,放置的数据时用户名和密码,是要用来从数据库中查找用户的
* 后边的请求,判断权限的时候,放置进去的数据是用户的信息,密码就不需要了,还有用户的权限,在后面加上doFilter过滤器
*
* jwt过滤器 -- OncePerRequestFilter请求执行一次该过滤器就会执行一次
* @author:Y.0
* @date:2023/11/21
*/

@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

   @Autowired
   private JwtUtils jwtUtils;

   /**
    * 该方法会被doFilter调用
    */
   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
       //获取token
       String token = request.getHeader("Authorization");
       //login请求就没token,直接放行
       if (token == null){
           doFilter(request,response,filterChain);
           return;
      }
       //有token,通过jwt工具类,解析用户信息
       Claims claims = null;
       try {
           claims = jwtUtils.parseToken(token);
      } catch (SignatureException e) {
           throw new RuntimeException(e);
      }
       log.info("解析出的token信息--->{}",claims);
       //获取到了数据,将数据取出,放到SysUser中
       Long id = claims.get("id", Long.class);
       String username = claims.get("username", String.class);
       String avatar = claims.get("avatar", String.class);
       List<String> perms = claims.get("perms", ArrayList.class);
       //将用户信息放到SysUser中
       SysUser sysUser = SysUser.builder()
              .id(id)
              .avatar(avatar)
              .userName(username)
              .perms(perms)
              .build();
       log.info("将解析出的数据封装到用户信息实体类中--->{}",sysUser);
       //将用户信息数据放到SecurityContext中
       //需要将用户信息数据和权限信息封装到Authentication中,然后才放到SecurityContextHolder
       UsernamePasswordAuthenticationToken authentication =
               new UsernamePasswordAuthenticationToken(sysUser,null,sysUser.getAuthorities());
       SecurityContextHolder.getContext().setAuthentication(authentication);
       //放行
       doFilter(request,response,filterChain);
  }
}

!!!jwt解析数据时,集合类型,会转换为ArrayList

  • 下一步我们要在SpringSecurityConfig中加入我们自定义的JwtAuthenticationFilter过滤器

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

   @Autowired
   private SysUserDetailsService userDetailsService;

   @Autowired
   private JwtAuthenticationFilter jwtAuthenticationFilter;

   @Bean
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
       http.csrf(csrf -> csrf.disable());
       //放行登录请求地址
       http.authorizeHttpRequests(auth -> auth.requestMatchers("/auth/login").permitAll()
              .anyRequest().authenticated());
       //将JwtAuthenticationFilter过滤器添加到SpringSecurity过滤器链中
       //将过滤器添加到UsernamePasswordAuthenticationFilter之前,在这个过滤器中会有一个认证字段,就算登录了,也不会执行
       http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
       return http.build();
  }


   /**
    * AuthenticationManager:负责认证的
    * DaoAuthenticationProvider:负责将userDetailsService,PasswordEncoder融合进去
    *
    * @param passwordEncoder
    * @return
    */
   @Bean
   public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){

       DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
       daoAuthenticationProvider.setUserDetailsService(userDetailsService);
       //关联加密编码器
       daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
       //将daoAuthenticationProvider放进ProviderManager中,包含进去
       ProviderManager providerManager = new ProviderManager(daoAuthenticationProvider);
       return providerManager;
  }

   @Bean
   public PasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
  }

}
  • jwt有一个弊端:就是不能强制过期,它可以设置过期时间,但是不能强制过期,不能强迫用户下线,但是我们可以将token信息存放在redis中的形式就可以实现,只需要删除redis中的对应用户信息的token就能实现强迫用户下线。

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容