Spring Security

一、简介

Spring Security是Spring家族中的一个安全管理框架,一般Web应用都需要 认证 和 授权

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作

二、快速入门

2.1 开发步骤

1、导入坐标

Spring Security 启动器
<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>

2、随便编写一个接口

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/login")
    public String login() {
        return "Spring Security";
    }

}

3、启动

在这里插入图片描述

可以看到,Spring Security已经起作用了,默认的用户名是:user,而密码则在控制台上

当我们输入用户名密码后,就能访问到我们的接口数据了~

三、认证

3.1 登录校验流程

在一般的开发场景中,登录校验的流程大致如下:
1. 前端携带一个 用户名和密码 访问登录接口;
2. 后端通过数据库校验用户名和密码;
3. 如果用户名密码正确,则使用 User的部门信息(用户名、用户ID)生成一个JWT;
4. 将JWT响应给前端;
5. 前端登录后,每次发送请求都会携带Token;
6. 后端获取请求头中的Token进行解析,获取UserID;
7. 根据UserID获取用户相关信息,如果有权限则允许访问资源;
8. 访问到目标资源,响应给前端。

3.2 Spring Security完整流程

Spring Security的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器
1、UsernamePasswordAuthenticationFilter,负责处理登录页面填写了用户名密码后的登录请求。
2、ExceptionTranslationFilter:处理过滤器链中抛出的任何 AccessDeniedException
3、FilterSecurityInterceptor:负责权限校验的过滤器

在这里插入图片描述

3.3 解决问题

一、思路
登录:
1. 自定义登录接口
	调用ProviderManager的方法进行认证 如果认证通过生成JWT
	把用户信息存入Redis
2. 自定义UserDetailService
	在这个实现列中去查询数据库
	
校验:
1. 定义JWT认证过滤器
	获取Token
	解析Token获取其中的UserID
	从Redis中获取用户信息
	存入SecurityContextHolder
二、实现方式
  1. 导入坐标

    <dependencies>
        <!-- Spring Security -->
       <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-security</artifactId>
       </dependency>
        
        <!-- Spring Boot Web的启动类 -->
       <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
    	
        <!-- 处理Json -->
       <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>fastjson</artifactId>
          <version>1.2.78</version>
       </dependency>
        
       <!--  Redis相关  -->
       <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
       </dependency>
       <!--  Redis连接池相关  -->
       <dependency>
          <groupId>org.apache.commons</groupId>
          <artifactId>commons-pool2</artifactId>
       </dependency>
        
       <!-- jjwt -->
       <dependency>
          <groupId>io.jsonwebtoken</groupId>
          <artifactId>jjwt</artifactId>
          <version>0.9.1</version>
       </dependency>
    
    	<!-- 数据库相关 -->
       <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <scope>runtime</scope>
       </dependency>
       <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
          <version>2.1.3</version>
       </dependency>
    
       <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <optional>true</optional>
       </dependency>
        
        <!-- Java开发必备的万能工具包 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.22</version>
        </dependency>
       
    </dependencies>
    
  2. 统一响应体

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Result {
    
        private Boolean success;
        private String errorMsg;
        private Object data;
        private Long total;
    
        public static Result ok(){
            return new Result(true, null, null, null);
        }
        public static Result ok(Object data){
            return new Result(true, null, data, null);
        }
        public static Result ok(List<?> data, Long total){
            return new Result(true, null, data, total);
        }
        public static Result fail(String errorMsg){
            return new Result(false, errorMsg, null, null);
        }
    
    }
    
  3. 编写实体类

    @Data
    public class User implements Serializable {
        private Long id;
        private LocalDateTime birthday;
        private String gender;
        private String username;
        private String password;
        private String telephone;
        private String station;
        private String remark;
    }
    
  4. 编写 UserMapper.java 接口

    @Mapper
    @Repository
    public interface UserMapper {
        List<User> findAll();
    }
    
  5. 编写 UserService.java 接口

    public interface UserService {
        List<User> findAll();
    }
    
  6. 编写 UserServiceImpl.java 实现类

    @Service
    public class UserServiceImpl implements UserService {
    
        @Resource
        private UserMapper userMapper;
    
        @Override
        public List<User> findAll() {
            return userMapper.findAll();
        }
    
    }
    
  7. 编写 UserMapper.xml 映射配置文件

    <?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.rabbit.springbootsecurity.mapper.UserMapper">
    
        <select id="findAll" resultType="user">
            select * from t_user
        </select>
    
    </mapper>
    
  8. 编写 application.yaml 配置文件

    server:
      port: 8083
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/health?characterEncoding=utf-8&serverTimezone=UTC
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
    
    #MyBatis
    
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      type-aliases-package: com.rabbit.springbootsecurity.pojo #别名
    
  9. 编写 UserController.java

    @RestController
    @RequestMapping("/user")
    public class UserController {
        @Resource
        private UserService userService;
    
        @GetMapping("/login")
        public Result login() {
            return Result.ok(userService.findAll());
        }
    
    }
    

测试结果:

在这里插入图片描述

三、存在问题
以上的入门案例存在一定问题:

我们每次访问接口都需要使用到 SpringSecurity 为我们提供的默认 用户名(user)和密码(控制台有),这显然不是我们想要的,所以我们需要创建一个类,实现 UserDeatailsService接口,重写其中的方法 loadUserByUsername() ,表示用户名密码从数据库中查询。

问题解决:

  1. 编写一个类,UserDetailsServiceImpl.java ,实现 UserDetailsService 接口,重写里边的方法,表示我们需要从数据库中获取用户信息

    @Component
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Resource
        private UserMapper userMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 根据用户名查询用户信息
            User user = userMapper.findUserByUsername(username);
    
            /**
             * 若查询不到用户信息,则抛异常
             * SpringSecurity可以处理我们在查询中遇到的异常
             */
            if (Objects.isNull(user)) {
                throw new RuntimeException("用户名或密码错误");
            }
    
            // TODO 根据用户查询权限信息,添加到LoginUser中
    
            // 因为返回值是UserDetails,所有需要定义一个类,实现UserDetails,把用户信息封装在其中
            return new LoginUser(user);
        }
    }
    
  2. 可以看到,实现的类中重写的方法返回值是 UserDetails,所以我们需要编写一个类,实现 UserDetails 接口并重写里边的方法

    @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;
        }
    }
    

注意:

如果要测试,若数据库中的密码是明文存储,需要在数据库的密码列的值前边加上 `{noop}`,表示是明文存储
四、密码加密解密
实际项目中,我们不会把密码明文的存储到数据库中;
我们一般会使用Spring Security为我们提供的 BCryptPasswordEncoder;
我们只需要把 BCryptPasswordEncoder对象注入到Spring容器中,Spring Security就会使用该PasswordEncoder来进行密码校验,所以我们可以定义一个Spring Security配置类,该配置类需要继承 WebSecurityConfigurerAdapter。

使用步骤:

  1. 编写一个配置类,继承 WebSecurityConfigurerAdapter

    该类就是一个配置类,该配置类提供一个方法,需要返回一个 BCryptPasswordEncoder 对象
    
  2. 编写一个返回 BCryptPasswordEncoder Bean对象的方法

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

测试加密方法:

@Test
void testBCryptPasswordEncoder() {
   BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
   /**
    * encode():加密方法,传入一个明文,加密后返回一个密文
    * 同一明文,每次调用encode()方法生成出来的密文都是不一样的,
    * 因为内部进行加密的时候,会生成一个【随机的加密盐】,
    * 底层是通过【加密盐】和原文进行一系列处理之后再进行加密
    * 这样的话,虽然明文一样,但是每一次的密文都是不一样的
    */
   String encode_pwd_1 = passwordEncoder.encode("rabbit");
   String encode_pwd_2 = passwordEncoder.encode("rabbit");
   log.info("encode_pwd_1:{}", encode_pwd_1);
   log.info("encode_pwd_2:{}", encode_pwd_2);
}

在这里插入图片描述

测试校验方法:

@Test
void testBCryptPasswordEncoder() {
   BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
   boolean flag_true = passwordEncoder.
         matches("rabbit", "$2a$10$MBcThIW7Tqm9liaBAXooPepAeovbD8XbM1tV3xvHOA6FHaI6vD4hO");

   boolean flag_false = passwordEncoder.
         matches("root", "$2a$10$MBcThIW7Tqm9liaBAXooPepAeovbD8XbM1tV3xvHOA6FHaI6vD4hO");

   log.info("flag_true:{}", flag_true);
   log.info("flag_false:{}", flag_false);

}

在这里插入图片描述

五、Jwt的使用和使用Jwt进行登录
5.1 什么是Jwt
	Jwt 是 JSON WEB TOKEN 英文的缩写,它是一个开放标准,它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全传输信息。该信息可以被验证和信任,因为它是数字签名用的。
	使用jwt最多的场景就是登陆,一旦用户登陆,那么后续的每个请求都应该包含jwt。
5.2 Jwt 组成
Jwt由三部分组成,每一部分之间用符号"."进行分割,整体可以看做是一个长字符串。
一个经典的jwt的样子:
	xxx.xxx.xxx

Jwt的三部分是:

  1. Header 头部:

    1. 头部由两部分组成,第一部分是声明类型,在 Jwt 中声明类型就 Jwt,第二部门就是声明加密算法,一般是话是采用 Hash256

    2. 一个经典的头部:

      {
        'typ': 'JWT',      //  'typ':'声明类型'
        'alg': 'HS256'	//	'alg':'声明的加密算法'
      }
      
  2. Payload 载荷、载体

    1. 这一部分是 Jwt 的主体部分,这一部分也是 Json 对象,可以包含需要传递的数据,其中 Jw t指定了七个默认的字段选择,这七个字段是推荐但是不强制使用的:

      iss:发行人
      exp:到期时间
      sub:主题
      aud:用户
      nbf:在此之前不可用
      iat:发布时间
      jti:JWT ID用于识别该JWt
      
    2. 除了上述的七个默认字段之外,还可以自定义字段,通常我们说 Jwt 用于用户登陆,就可以在这个地方放置 userId、username

    3. 下面这个Json对象是一个 Jwt 的 Payload 部分:

      {
          "sub": "1234567890",
          "nickname": "rabbit",
          "id": "9527"
      }
      

      注意:这里不能存放敏感信息,否则可能会被截取

  3. signature 签证:

    1. 这部分是 对前两部分进行 base64 编码 再进行加密,这个加密的方式使用的是 Jwt的头部声明中的加密方式,在加上一个加密盐(secret)组成的,secret通常是一个随机的字符串,这个secret是服务器特有的,不能够让其他人知道。这部分的组成公式是:

      HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
      
5.3 为什么选择 Jwt
  • session 的缺点:

    	首先在我的认知里 Jwt 用处最多的就是作为用户登陆的凭证,以往这个凭证是使用session和cookie进行存储的,session技术的存储在服务器端的一种技术,构造一个类似于哈希表存储用户id和用户的一些信息,将这个用户id放在cookie里返回给用户,用户每次登陆的时候带上这个cookie,在哈希表中如果可以查到信息,那么说明用户登陆并且得到对应用户的信息;
    	但是session存放在服务器端,当用户量很大时,占用了服务器过多的宝贵的内存资源。同时因为如果有多台服务器,那么当用户登陆时访问了服务器A,那么就只有服务器A上会存储这个用户的信息,当用户访问其他页面时,也许请求会发给服务器B,这时服务器B中是没有用户的信息的,会判定用户处于非登录的状态。也就是说session无法很好的在微服务的架构之中使用;
    	因为session是和cookie结合使用的,如果cookie被截获,那么就会存在安全危机。
    
  • Jwt 的优点:

    Json形式,而Json非常通用性可以让它在很多地方使用;
    Jwt所占字节很小,便于传输信息;
    需要服务器保存信息,易于扩展;
    
5.4 一个 Jwt 的工具类
package com.rabbit.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

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

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

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

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

    /**
     * 生成jtw
     * @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();
    }

    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);
    }

    /**
     * 创建token
     * @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();
    }

    public static void main(String[] args) throws Exception {
//        String jwt = createJWT("2123");
        Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0");
        String subject = claims.getSubject();
        System.out.println(subject);
//        System.out.println(claims);
    }

    /**
     * 生成加密后的秘钥 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;
    }
    
    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }


}
六、登录接口
	在实际开发中,我们通常有的接口,不需要登录也能访问,比如:登录页面、注册、忘记密码等,所以我们需要在 Spring Security配置一些信息以及编写一些放行的方法:
	在接口中我们通过 AuthenticationManager 的 authenticate() 方法来进行用户认证,所以需要在 SecurityConfig 中配置把 AuthenticationManager 注入Spring IoC容器
	认证成功的话,需要生成一个 JWT,放入响应中返回。并且为了让用户下回请求时能通过 JWT 识别处具体是哪个用户,我们需要把用户信息存入Redis
6.1 准备工作
  1. 创建一个用于存放 Redis 的 Key 名称的类

    public class RedisConstants {
        public static final String LOGIN_USER_KEY = "jwt:login:token:";
        public static final Long LOGIN_USER_TTL = 36000L;
    
    }
    
  2. 创建一个用于存放登录信息的实体类 LoginDto.java

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class LoginDto implements Serializable {
        private String username;
        private String password;
        private String telephone;
    }
    
  3. 创建一个实体类,用于保存部分信息至Redis的 UserDto.java

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class UserDto implements Serializable {
        private Long id;
        private String username;
    }
    
6.2 开发步骤
  1. 在开发中,我们不可能对任何请求都进行拦截,肯定是部分请求不要拦截,所以我们需要在 SecurityConfig.java 中重写 configure(http) 方法,配置对哪些请求不拦截

    /**
     * 对一些接口放行 比如:登录
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭 csrf
                .csrf().disable()
                // 不通过session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/loginByPassword").anonymous()
                .antMatchers("/user/loginByPhone").anonymous()
    
                // 除了上面的请求,任何请求都需要鉴权认证
                .anyRequest().authenticated();
    }
    
  2. 使用 AuthenticationManager 进行用户认证,将用户登录的 username、password 封装成 Authentication 对象,随后使用 authenticationManager 帮助我们完成认证操作,而 authentcationManager 最终调用 authentcate() 方法完成认证

    1. SecurityConfig.java 中,重写方法 authenticationManagerBean()

      /**
       * 重写该方法,暴露 authenticationManagerBean
       * 具体使用只需保留原本的代码即可
       * @return
       * @throws Exception
       */
      @Bean
      @Override
      public AuthenticationManager authenticationManagerBean() throws Exception {
          return super.authenticationManagerBean();
      }
      
    2. UserServiceImpl.java 中实现登录接口

      /**
       * 根据用户名密码进行登录
       * @param loginDto
       * @return
       */
      @Override
      public Result loginByPassword(LoginDto loginDto) {
          // AuthenticationManager 进行用户认证
          // 将用户登录的username和password封装成 Authentication 对象
          // 随后使用 authenticationManager 帮助我们完成认证操作
          // 而 authenticationManager 最终调用 authenticate() 方法完成认证
          UsernamePasswordAuthenticationToken authenticationToken =
                  new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());
          Authentication authenticate = authenticationManager.authenticate(authenticationToken);
          // 如果认证没通过 则给出相应提示
          if (ObjectUtil.isEmpty(authenticate)) {
              log.info("message:{}",ResultMessage.LOGIN_FAIL);
              throw new RuntimeException("用户名或密码错误!");
          }
      
          // 认证通过,使用 userId 生成一个 Jwt,Jwt存入Result返回
          LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
          String user_id = loginUser.getUser().getId().toString();
          User user = loginUser.getUser();
          String jwtToken = JwtUtils.getJwtToken(user_id);
      
          // TODO 使用 StringRedisTemplate存储实体类型,用hash 不过需将实体转为Map
          UserDto userDto_redis = BeanUtil.copyProperties(user, UserDto.class);
          // TODO 由于 StringRedisTemplate要求所有字段都是String类型,而UserDto的id是long类型
          // TODO 所以需要把所有字段都修改为String类型
          Map<String, Object> map = BeanUtil.beanToMap(userDto_redis, new HashMap<>(),
                  CopyOptions.create().setIgnoreNullValue(true)
                          .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
      
          // 把用户信息存储到Redis中 userid作为Key的前缀
          String login_key = RedisConstants.LOGIN_USER_KEY + user_id;
          stringRedisTemplate.opsForHash().putAll(login_key, map);
      
          // TODO 5.4 设置有效期
          stringRedisTemplate.expire(login_key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
      
          Map<String, String > hashMap = new HashMap<>();
          hashMap.put("authorization", jwtToken);
          return Result.ok(hashMap);
      }
      
6.3 总结
1. 自定义登录接口,让 Spring Security 对这个接口放行,即让用户不需要登录也能进行访问,对应的也就是在 SecurityConfig.java 中通过继承 WebSecurityConfigurerAdapter 类重写 configure(http) 方法然后进行一些必要的配置;
2. 通过在 SecurityConfig.java 中重写 authenticationManagerBean() 方法,暴露 authenticationManagerBean,随后在 UserServiceImpl中国注入该Bean,表示我们可以通过 authenticate()方法进行用户认证;
3. 认证成功的话就可以生成一个Jwt,放入响应中返回,并且为了让用户下回请求时能通过Jwt识别出具体是哪个用户,我们需要把用户信息存储到Redis中
七、校验Token
7.1 简介

前面我们已经生成了一个Token,所以接下来我们需要进行一些必要的Jwt解析认证,流程如下:

1. 定义一个Token解析过滤器,我们使用Spring为我们提供的过滤器接口,原生的Filter过滤器接口在不同的Tomcat版本中存在一点问题;
2. 获取Token;
3. 解析Token获取其中的UserId;
4. 根据UserID获取Redis中的用户信息;
5. 讲用户信息存入SecurityContextHolder,我们前面了解到,Spring Security其实就是一整个过滤器链,所以我们需要把用户认证信息存储到其中一个过滤器,然后后面的过滤器会去该过滤器中查找有无用户信息,有的话才会放行
7.2 编写 Token 解析过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 定义 Token 解析过滤器
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. 获取Token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // 没有获取到Token 放行,让其它过滤器去拦截
            filterChain.doFilter(request, response);
            return;
        }
        // 2. 解析Token 获取userID
        String userId = null;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Token异常!");
        }
        // 3. 从Redis中取出用户数据
        String redis_key = RedisConstants.LOGIN_USER_KEY + userId;
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(redis_key);
        if (map.isEmpty()) {
            throw new RuntimeException("Token异常!");
        }
        // TODO 从Redis中获取的是hash结构 需要转为实体
        UserDto userDto = BeanUtil.fillBeanWithMap(map, new UserDto(), false);

        //  TODO 4. 将用户信息存入SecurityContextHolder
        /**
         * setAuthentication()方法需要一个authentication对象,
         * 所以我们需要把用户信息封装到authentication中
         */
        // TODO 获取权限信息封装到 authentication 中(第三个参数)
        UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(userDto,null,null);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 5. 放行
        filterChain.doFilter(request, response);
    }
}
7.3 配置过滤器链

简介:

	前面我们已经编写好了 Spring Security 的Token解析过滤器,但我们还需去配置过滤器,并且我们把过滤器配置好后还需指定其在Spring容器中的顺序,所以需将我们前面写好的解析Token的过滤器配置到用户认证授权的过滤器UsernamePasswordAuthenticationFilter前面。
	在 SecurityConfig.java 中的 configure() 方法中配置 http.addFilterBefore()

步骤:

  1. SecurityConfig.java 中注入自定义的解析Token过滤器

    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
  2. 把该过滤器放到用户认证拦截器之前

    /**
     * 对一些接口放行 比如:登录
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭 csrf
                .csrf().disable()
                // 不通过session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/loginByPassword").anonymous()
                .antMatchers("/user/loginByPhone").anonymous()
    
                // 除了上面的请求,任何请求都需要鉴权认证
                .anyRequest().authenticated();
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
    
八、退出登录

流程分析

我们只需要从 SecurityContextHolder 中获取用户ID,然后根据用户ID删除Redis中的内容即可

代码实现

/**
 * 退出登录
 * @return
 */
@Override
public Result logout() {
    /**
     * 1. 从 SecurityContextHolder 中获取用户信息(ID)
     * 2. 从Redis中删除对应的Key
     */
    UsernamePasswordAuthenticationToken authenticationToken
        = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    UserDto userDto = (UserDto) authenticationToken.getPrincipal();
    Long userId = userDto.getId();
    // 根据Key删除Redis的值
    stringRedisTemplate.delete(RedisConstants.LOGIN_USER_KEY + userId);
    return Result.ok("退出成功~");
}
九、总结
Authentication在我看来就是一个载体,在未得到认证之前它用来携带登录的关键参数,比如用户名和密码、验证码;在认证成功后它携带用户的信息和角色集。

交给AuthenticationManager认证。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐