SpringBoot整合Spring Security实现权限控制

Spring Security介绍

要对Web资源进行保护,最好的办法莫过于Filter
要想对方法调用进行保护,最好的办法莫过于AOP。

Spring Security进行认证和鉴权的时候,就是利用的一系列的Filter来进行拦截的。
SpringBoot整合Spring Security实现权限控制_第1张图片
如图所示,一个请求想要访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。
SpringBoot整合Spring Security实现权限控制_第2张图片
这里面我们只需要重点关注两个过滤器即可:
UsernamePasswordAuthenticationFilter负责登录认证,
FilterSecurityInterceptor负责权限授权。

一般Web应用的需要进行认证和授权。

认证(Authentication)验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权(Authorization)经过认证后判断当前用户是否有权限进行某个操作

​ 而认证和授权就是SpringSecurity作为安全框架的核心功能。

说明:Spring Security的核心逻辑全在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件你就掌握了Spring Security!这个框架的使用方式就是对这些过滤器和组件进行扩展。

Spring Security案例

1、快速搭建一个springboot工程

此步骤省略

建议整合knif4j方便调试

SpringBoot整合Spring Security实现权限控制_第3张图片
访问接口文档:http://localhost:8081/doc.html(端口为自己设置的端口,默认是8080)
SpringBoot整合Spring Security实现权限控制_第4张图片
springboot工程搭建完成

2、导入SpringSecurity整合springboot工程

  1. 导入SpringSecurity依赖(版本跟随boot版本)
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

  1. 重新启动项目进行测试:访问路径:http://localhost:8081/ayo/hhy/1

这时我们可以看到当我们访问我们的接口的时候,就会自动跳转到一个SpringSecurity的默认登陆页面(后期是需要整合到自己系统的登录页面来做认证)

这时候需要我们登录才可以进行访问,我们可以看到控制台有一串字符串,其实那就是SpringSecurity初始化生成给我的密码

SpringBoot整合Spring Security实现权限控制_第5张图片
用户名默认为 :user
密码在控制台打印出来了
在这里插入图片描述
输入用户和密码登录后,就可以正常访问接口了
SpringBoot整合Spring Security实现权限控制_第6张图片

3、认证

3.1、登录流程校验

核心是通过用户输入的用户名和密码去和数据库中的数据进行比对,如果比对无误

那么就会使用jwt工具根据用户名和密码去生成一个token传给前端存储起来

在登录过后用户想要访问别的资源都要在请求头里面携带token,后端会对前端请求的token拿到并且解析出来用户的id

根据用户的id获取相关的信息,来判断用户是否有访问资格,如果有,则访问目标资源返回给前端,若没有,则返回错误信息(无权限访问)
SpringBoot整合Spring Security实现权限控制_第7张图片

3.2、入门案例的原理

前后端进行认证的流程:
SpringBoot整合Spring Security实现权限控制_第8张图片
具体流程:

SpringBoot整合Spring Security实现权限控制_第9张图片

在上面的案例整合springSecurity,进入默认的登录页面进行登录的时候,是查询内存中的用户名和密码的,后期我们做项目肯定是要改成去数据库里面查询用户名和密码比对

其中:

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

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

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

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

3.3、实现思路

登录流程:

  1. 登录认证成功之后根据用户名和密码通过jwt生成一个token返回给前端(前端每次请求都会携带token到请求里面),同时将token存入redis(其中用户id作为key,用户信息作为value)(主要是为了不让后续的校验流程频繁的查询数据库)
  2. 这里的UserDetailsService默认是去查内存来认证用户名和密码的,需要自定义UserDetailsService这个接口去查自己的数据库的数据进行认证
  3. 我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。

SpringBoot整合Spring Security实现权限控制_第10张图片
校验流程:

获取token,解析token获取到其中的userId;
根据userId,从redis中获取用户信息,存入SecurityContextHolder。

  1. 前端每次发送请求的时候,请求头都会携带token。
  2. 后端从redis根据key去拿到token,并且通过jwt解析出用户的信息。
  3. 通过用户的信息查询用户可以使用的权限,然后返回给前端去访问能访问的资源

SpringBoot整合Spring Security实现权限控制_第11张图片

3.4、实现认证流程(自定义)

准备工作:
1. 引入依赖(jjwt、reids、fastjson,mysql,mybatis-plus)

<!--redis-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
<!--		fastjson2-->
		<dependency>
			<groupId>com.alibaba.fastjson2</groupId>
			<artifactId>fastjson2</artifactId>
			<version>2.0.25</version>
		</dependency>
<!--jjwt-->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.1</version>
		</dependency>
		
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>


2. 添加相关配置

redis建议直接使用stringRedisTemplate,如果使用redisTemplate需要对数据进行序列化做相关的配置
参考链接:redisTemplate和stringRedisTemplate对比、redisTemplate几种序列化方式比较

因为是前后端分离,所以需要一统一结果返回类 R类(不唯一能用就行)

@Data
public class R<T> implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

    public static <T> R<T> success(T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }
    public static <T> R<T> success(String msg,T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        r.msg = msg;
        return r;
    }

    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }

    public static <T> R<T> success(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 1;
        return r;
    }

    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }

}

导入jwt工具类

/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "qx";

    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    
    /**
     * 生成jtw  jwt加密
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw  jwt加密
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }


    /**
     * 创建token jwt加密
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("sg")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }



    public static void main(String[] args) throws Exception {
        //jwt加密
        String jwt = createJWT("123456");

        //jwt解密
        Claims claims = parseJWT(jwt);
        String subject = claims.getSubject();



        System.out.println(subject);
        System.out.println(jwt);
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * jwt解密
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }


}

导入实体类

/**
 * 用户表(User)实体类
 *
 *
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable {
    private static final long serialVersionUID = -40356785423868312L;
    
    /**
    * 主键
    */
    @TableId
    private Long id;
    /**
    * 用户名
    */
    private String userName;
    /**
    * 昵称
    */
    private String nickName;
    /**
    * 密码
    */
    private String password;
    /**
    * 账号状态(0正常 1停用)
    */
    private String status;
    /**
    * 邮箱
    */
    private String email;
    /**
    * 手机号
    */
    private String phonenumber;
    /**
    * 用户性别(0男,1女,2未知)
    */
    private String sex;
    /**
    * 头像
    */
    private String avatar;
    /**
    * 用户类型(0管理员,1普通用户)
    */
    private String userType;
    /**
    * 创建人的用户id
    */
    private Long createBy;
    /**
    * 创建时间
    */
    private Date createTime;
    /**
    * 更新人
    */
    private Long updateBy;
    /**
    * 更新时间
    */
    private Date updateTime;
    /**
    * 删除标志(0代表未删除,1代表已删除)
    */
    private Integer delFlag;
}

数据库建表并且配置数据库

CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
  `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
  `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
  `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
  `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
  `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
  `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
  `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
	`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型:1代表普通用户,0代表管理员',
  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14787164048663 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/security?characterEncoding=utf-8&useSSL=false
      username: root
      password: root

定义mapper、service、serviceImpl并测试mapper能正常使用
省略(可以使用代码生成器)
SpringBoot整合Spring Security实现权限控制_第12张图片

3.5、正式实现

3.5.1 实现数据库的校验
  • 创建一个类实现UserDetailsService接口,重写其中的(loadUserByUsername)方法。增加用户名从数据库中查询用户信息
    SpringBoot整合Spring Security实现权限控制_第13张图片 - 这里只是查了用户信息,对于用户权限还没有查(这部分是属于授权部分的内容),因为最后是把用户信息和权限信息封装成UserDetails进行返回的
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息
        User user = userService.getOne(new LambdaUpdateWrapper<User>().eq(User::getUserName, username));
        //如果没有查询到用户就抛出异常
        if (Objects.isNull(user)) {
            throw new CustomException("用户名或密码错误");
        }
        //TODO 查询对应的权限信息
        //把数据封装成UserDetails返回
        return new LoginUser(user);
    }
}

自定义一个实现UserDetails的(LoginUser)类,实现接口的方法;用来接收存储数据(用户信息和权限信息)

  • 将用户属性注入进来,更改里面获取用户名和密码的get方法,使其获得user的信息
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

//这里先不管权限信息这个集合
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
//获取用户的用户的密码
    @Override
    public String getPassword() {
        return user.getPassword();
    }

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

此次再次启动服务访问任意接口:(这个时候走的就是查我们自己的数据库了,可以在实现类里面打断点测试)

此时后端控制台就不会打印出在内存中的密码了,因为我们重写了UserDetailsService 接口的loadUserByUsername,程序走我们自己的方法。
SpringBoot整合Spring Security实现权限控制_第14张图片

注意:点击登录后,这里会出现一个问题

SpringBoot整合Spring Security实现权限控制_第15张图片
当输入正确的用户名和密码还是会显示密码错误

控制台打印的错误信息可以看出
在这里插入图片描述
在这里插入图片描述

  • 因为我们数据库的密码是明文保存的,而PasswordEncoder方法其实是将数据库的加密的密码解密后再和用户输入的明文密码进行比对的

处理办法:在 Spring Security 中,{noop} 前缀告诉系统直接将密码存储为明文,即未经过哈希或其他加密算法处理。
在这里插入图片描述
再次登录测试:能够成功访问接口方法

3.5.2 密码加密存储
  • 实际项目中我们不会把密码明文存储在数据库中。
    默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password,它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式,所以需要替换PasswordEncoder。\
  • 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder
  • 我们只需要把BCryptPasswordEncoder对象注入Spring容器,SpringSecurity就会使用该PasswordEncoder来进行密码校验。

我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 //创建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

相当于将原来的PasswordEncoder 中的方法覆盖掉,转而执行BCryptPasswordEncoder中的方法

其中BCryptPasswordEncoder对象中的三个方法分别作用是:
SpringBoot整合Spring Security实现权限控制_第16张图片

  • encode : 用于对密码进行加密
  • matches:用于判断给定的原始密码与已加密的密码是否匹配
  • upgradeEncoding:用于升级密码的加密方式

这个时候数据库里面的用户密码必须是加密过的密码,这样在进行认证的时候,根据用户名查询到加密的密码,然后再根据passwordEncoder中的密码匹配方法来判断密码是否正确。(若数据库还是明文密码,那么肯定是会密码错误认证失败的)

咱可以先把数据库的明文密码加密放回原用户:(编写Test方法加密用户密码)

	@Test
	public void testPasswordEncoder() {
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
		String encode = passwordEncoder.encode("123456");
		System.out.println(encode);

	}

得到加密的密码填入数据库:
SpringBoot整合Spring Security实现权限控制_第17张图片
SpringBoot整合Spring Security实现权限控制_第18张图片

重启系统登录测试,登陆成功,正常访问接口

3.5.3 自定义登陆接口实现

其实质就是不让登录走整合SpringSecurity默认的登录页面了,而是走我们自己定义的登录接口

接下我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问

​ 在接口中我们通过AuthenticationManagerauthenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器

​ 认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

创建LoginController 登录接口

  • 其中LoginService 接口和实现接口类省略
@RestController
@RequestMapping("/user")
public class LoginController {
    
    @Autowired
    private LoginService loginService;

    @PostMapping("/login")
    public R login(@RequestBody User user){
        return loginService.login(user);
    }

那么我们可以在loginService实现类中编写login方法
其中验证是通过通过AuthenticationManager的authenticate方法来进行用户认证,需要在SecurityConfig中配置把AuthenticationManager注入容器

并且进行一个configure的配置才能使用

·SecurityConfig·

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //创建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    //把AuthenticationManager注入容器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }

这个时候就可以编写登录接口了

  1. 通过UsernamePasswordAuthenticationToken获取用户名和密码
    2.AuthenticationManager委托机制对authenticationToken 进行用户认证
  2. 如果认证没有通过,给出对应的提示
  3. 如果认证通过,使用user生成jwt , jwt存入R返回
  4. 如果认证通过,拿到这个当前登录用户信息
  5. 把完整的用户信息存入redis userid为key 用户信息为value

此时只有用户信息,还没有包括权限信息,后续授权会完善

@Service
public class LoginServiceImpl implements LoginService {



    @Autowired
    private UserService userService;

    //认证委托
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private StringRedisTemplate redisTemplate;


    @Override
    public R login(User user) {
//通过UsernamePasswordAuthenticationToken获取用户名和密码
        UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken
                                                                (user.getUserName(),user.getPassword());


        //AuthenticationManager委托机制对authenticationToken 进行用户认证

        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        //如果认证没有通过,给出对应的提示
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("登录失败");
        }

        //如果认证通过,使用user生成jwt  jwt存入R 返回

        //如果认证通过,拿到这个当前登录用户信息
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();

        //获取当前用户的userid
        String userid = loginUser.getUser().getId().toString();

        String jwt = JwtUtil.createJWT(userid);
        Map<String, String> map = new HashMap<>();
        map.put("token",jwt);

        //把完整的用户信息存入redis  userid为key   用户信息为value
  将对象转换为 JSON 字符串  存入redis
        String jsonString = JSONUtil.toJsonStr(loginUser);
        redisTemplate.opsForValue().set("login"+userid, jsonString );

        return R.success("登陆成功",map);
    }
}

重启服务进行登录测试(建议打断点一步步测试数据)

这里我们用postman进行登录测试
user/login被放行,可以不用登录认证直接访问

在这里插入图片描述

输入url:localhost:8081/user/login

通过json传输用户信息
SpringBoot整合Spring Security实现权限控制_第19张图片
结果返回:
SpringBoot整合Spring Security实现权限控制_第20张图片
并且查看reidsSpringBoot整合Spring Security实现权限控制_第21张图片

此时认证成功~

3.5.4 自定义实现认证过滤器

我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid

使用userid去redis中获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder

定义过滤类:JwtAuthenticationTokenFilter

流程:

  1. 过滤器拦截到请求,并且从请求头中拿到token
  2. 根据拿到的token根据jwt工具类解析得到用户的ID(userId)
  3. 根据userId(也就是之前reids存token的key)去redis拿到用户信息
  4. 若用户信息不为null,代表该用户没有登录过,抛异常
  5. 若能拿到用户信息封装Authentication对象存入SecurityContextHolder并且获取权限信息封装到Authentication中(这里属于授权过程,暂不实现)
  6. 放行

封装Authentication对象:包括两部分

  1. 用户信息
  2. 用户权限信息
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {


    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
          //从前端发起请求的请求头获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }

        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new CustomException("token非法");
        }

        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        //将json转换为对象类
        String json = redisTemplate.opsForValue().get(redisKey);
        LoginUser loginUser = JSONUtil.toBean(json, LoginUser.class);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //封装Authentication对象存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);


    }
}

把token校验过滤器添加到过滤器链中(注入到配置类(SecurityConfig)加入过滤器链)


   //把token校验过滤器注入配置类
    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

//把token校验过滤器添加到过滤器链中并且添加到UsernamePasswordAuthenticationFilter之前
  http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //创建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

  //把AuthenticationManager注入容器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
   //把token校验过滤器注入配置类
    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

  

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        //把token校验过滤器添加到过滤器链中
                http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    }

}

测试:

  • 编写一个接口来判断登录过后是否可以拿到token并且解析出用户信息
    SpringBoot整合Spring Security实现权限控制_第22张图片

测试1:不登录直接访问:访问路径localhost:8081/ayo/coo
SpringBoot整合Spring Security实现权限控制_第23张图片

断点调试JwtAuthenticationTokenFilter

  • 此时过滤器拦截到请求,发现请求头里面没有token(代表用户没有登录),直接放行retrun 认证失败

SpringBoot整合Spring Security实现权限控制_第24张图片

测试2:登录后设置请求头(登录生成的token)再次访问 :访问路径localhost:8081/ayo/coo

记录登录成功的token值

SpringBoot整合Spring Security实现权限控制_第25张图片
在访问路径localhost:8081/ayo/coo时,在请求头加上登录过后生成的token
SpringBoot整合Spring Security实现权限控制_第26张图片
此时断点调试JwtAuthenticationTokenFilter:成功从请求头拿到token
SpringBoot整合Spring Security实现权限控制_第27张图片

然后根据token解析到用户的id(userId),根据userId从redis取出用户的信息,然后将用户的信息(和权限信息)封装成Authentication对象存入SecurityContextHolder

  • SecurityContextHolder

SecurityContextHolder 类提供了一种方便的方式来获取当前用户的安全信息,如身份验证信息和角色信息。它是一个线程本地的存储,可以在应用程序的不同层级中访问

SpringBoot整合Spring Security实现权限控制_第28张图片
返回此次调用接口的数据:
SpringBoot整合Spring Security实现权限控制_第29张图片

3.5.5 退出登录

流程:

  1. 执行退出登录的请求时,请求头携带token
  2. 进入doFilterInternal过滤器,解析token,得到用户userId
  3. 根据userId从redis中拿到用户信息,并且将用户信息封装成 Authentication对象,并且存入SecurityContextHolder

SecurityContextHolder.getContext().setAuthentication(authenticationToken);

  1. 执行controller的方法—>service中的logout方法
  • SecurityContextHolder中获取用户的userid
  • 根据userid找到redis对应值进行删除

controller层

    @GetMapping("/logout")
    public R login(){
        return loginService.logout();
    }

service实现类:

    /**
     * 注销接口
     * @return
     */
    @Override
    public R logout() {
        //从SecurityContextHolder中的userid
        UsernamePasswordAuthenticationToken authentication =
                (UsernamePasswordAuthenticationToken)
                        SecurityContextHolder.getContext().getAuthentication();

        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();

        //根据userid找到redis对应值进行删除
        redisTemplate.delete("login:"+userid);
        return R.success("注销成功");
    }

其中的核心是通过过滤器将用户的userId放入SecurityContextHolder中中了,在注销登录的时候可以直接拿到userId再去删除redis中的token信息

SpringBoot整合Spring Security实现权限控制_第30张图片

注意:这个时候再去访问其他接口的时候,发送过来的请求的请求头所携带的还是之前的登录时的token,这个时候进入过滤器的时候可以根据token获得用户的userId,但是在根据userId去redis获得用户信息的时候是获取不到的(注销的时候已经删除该用户对应的信息),抛出未登录异常.

SpringBoot整合Spring Security实现权限控制_第31张图片
SpringBoot整合Spring Security实现权限控制_第32张图片
控制台报错:
在这里插入图片描述

、、、、、、、、更新中

你可能感兴趣的:(Java,#,SpringBoot,#,SpringSecurity,spring,spring,boot,java)