前言

自界说登录流程是整合 SpringSecurity 开发必不可少的一步。上篇文章咱们介绍了整合数据库的登录,本篇文章在此基础上整理了 SpringSecurity + JWT + Redis 的登录流程。

全体流程图

登录及认证的全体流程如下图:

整合SpringSecurity——自定义登录流程(SpringSecurity + JWT + Redis)

依赖

除了 SpringSecurity 的相关依赖外,还需求 Redis 和 hutool (强壮且全面的工具包,本篇文章中 JWT 的相关类也来自该包) 的依赖。

<!-- springboot整合的redis依赖,里边集成了 spring-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.13</version>
</dependency>

装备

在整个流程中,咱们用到了 SpringSecurity + JWT + Redis ,需求装备的是SpringSecurity 和 Redis。

SpringSecurity 装备
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private JwtFilter jwtFilter;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 注入 AuthenticationManager 目标,用于调用认证办法
     *
     * @return
     * @throws Exception
     */
    @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()
                // 把jwt过滤器放到UsernamePasswordAuthenticationFilter前,便于先判别用户是否登录,再决议是否登录
                .authorizeRequests()
                // 对于登录接口答应匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的一切恳求全部需求鉴权认证
                .anyRequest().authenticated();
        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

在装备类中,咱们注入了 AuthenticationManager 目标。这个目标在上篇文章咱们提到过,用于调用认证办法。但是在父类 WebSecurityConfigurerAdapter 中并没有将它注入到容器中,而咱们又需求在自己的登录接口中调用它,因而需求重写 authenticationManagerBean 办法并将回来的目标注入到容器中。 configure(HttpSecurity http) 办法用于装备路由,只敞开登录接口,其他接口都需求认证。

Redis装备
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        // 设置序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
                Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 装备redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        RedisSerializer<?> stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);// key序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化
        redisTemplate.setHashKeySerializer(stringSerializer);// Hash key序列化
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// Hash value序列化
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

在 Redis 装备中大部分是常规的序列化装备,特殊的是 ObjectMapper ,这个装备是为因为存储在 Redis 的实体类中除了成员变量的 get 办法外,其他办法不能有回来值,否则会导致反序列化反常,而 ObjectMapper 就是为了解决这个问题。 反序列化反常的问题可以经过下面的代码验证

@Test
public void test(){
    RedisTestEntity entity = new RedisTestEntity();
    entity.setName("111");
    redisTemplate.opsForValue().set("name", entity);
}
@Test
public void get(){
    Object name = redisTemplate.opsForValue().get("name");
    System.out.println(name);
}
@Data
public class RedisTestEntity {
    private String name;
    public Integer requireAge(){
        return 18;
    }
}

界说登录接口

参数的接纳
@Data
public class UserLogin {
    private String username;
    private String password;
}
controller部分
@PostMapping("/login")
public R<String> login(@RequestBody UserLogin userLogin){
    String jwt = securityService.login(userLogin);
    return R.success().data(jwt);
}

controller 仅仅担任匹配路由和回来数据,业务经过 service 的相关办法完结,因而 controller 中没有太多代码

service部分
@Resource
private AuthenticationManager manager;
@Resource
private RedisTemplate redisTemplate;
@Override
public String login(UserLogin userLogin) {
    Authentication userAuthentication = new UsernamePasswordAuthenticationToken(userLogin.getUsername(), userLogin.getPassword());
    Authentication authenticate = manager.authenticate(userAuthentication);
    // 假如认证成功则进入生成token的逻辑
    if (authenticate.isAuthenticated()) {
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        SysUser user = loginUser.getUser();
        // 将登录成功的目标存入redis
        redisTemplate.opsForValue().set(KeyUtil.getLoginUserKey(user.getUserId()), loginUser);
        // 生成token
        String token = JWT
                .create()
                .setPayload(userLoginId, user.getUserId())
                // (签发时刻)---------(生效时刻)---------(当前时刻)---------(失效时刻)
                .setIssuedAt(new Date())
                // 过期时刻七天
                .setExpiresAt(new Date(System.currentTimeMillis() + DateUnit.WEEK.getMillis()))
                // // 设置HS256为加密算法,以用户的密码为盐(密钥)
                .setSigner("HMD5", salt.getBytes(StandardCharsets.UTF_8))
                .sign();
        return token;
    }
    throw new RuntimeException("用户名或密码过错");
}

service 完结了登录的主要流程,包括:

  1. 调用 AuthenticationManager 实例的 authenticate 办法对用户的账号密码进行验证,该办法会调用到咱们上篇文章自界说的办法,经过查询数据库的数据完结校验
  2. 假如校验成功,则将用户信息存入 Redis 并生成相应 token ,同时将用户 id 存入 token 的荷载中,失利则抛出反常

登录过滤器

完结登录后,在今后的每次恳求都需求在恳求头中带上 token 以便于认证,认证操作经过过滤器完结(关于 jwt 的具体常识本篇文章不做讨论,不熟悉请自行查阅相关资料)。

@Component
public class JwtFilter extends OncePerRequestFilter {
    @Resource
    private RedisTemplate redisTemplate;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        // 没有token,去走登录流程
        if (StrUtil.isBlank(token)) {
            filterChain.doFilter(request, response);
            return;
        }
        // token不能为空
        JWT jwt = JWTUtil.parseToken(token);
        // 验证token是否合法
        HMacJWTSigner singer = new HMacJWTSigner(AlgorithmUtil.getAlgorithm("HMD5"), salt.getBytes(StandardCharsets.UTF_8));
        boolean common = jwt.verify(singer);
        // 验证时刻,失利会抛出反常
        try {
            JWTValidator.of(jwt).validateDate(DateUtil.date());
        } catch (ValidateException exception) {
            throw new TokenInvalidException("token反常");
        }
        if (common){
            NumberWithFormat userIdObj = (NumberWithFormat)jwt.getPayload(userLoginId);
            Integer userId = userIdObj.intValue();
            LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get(KeyUtil.getLoginUserKey(userId));
            // 假如用户不存在,阐明token反常
            if (loginUser == null) {
                throw new TokenInvalidException("token反常");
            }
            // 将用户信息存入 SecurityContextHolder ,以便本次在恳求中运用
            UsernamePasswordAuthenticationToken authenticationLoginUser = new UsernamePasswordAuthenticationToken(loginUser, null, null);
            SecurityContextHolder.getContext().setAuthentication(authenticationLoginUser);
            filterChain.doFilter(request, response);
        }
    }
}

咱们自界说的过滤器类继承了 OncePerRequestFilter 类偏重写了 doFilterInternal 办法,然后在 SpringSecurity 的装备类中将其添加到 UsernamePasswordAuthenticationFilter 前面(校验账号密码之前),对应装备类中的如下代码

http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

在过滤器中,咱们首要校验了 token 是否存在。假如不存在则直接放行,然后在后续校验权限时会被主动阻拦下来。存在则验证 token 的合法性,经往后从 token 拿到用户 id ,从 Redis 中获取到用户信息,假如不存在阐明用户的登录状况反常(可能是退出了登录状况,后文会说)。从 Redis 中取到用户信息后,就可以将用户信息存到 SecurityContextHolder 中,方便后续进行认证、授权以及运用。最后放行,履行后续操作。

退出登录

在做完前面的操作后,退出登录的操作就很简单了。

@PostMapping("/logout")
public R<String> logout(){
    Boolean delete = securityService.logout();
    return R.success().data(delete);
}
@Override
public Boolean logout() {
    LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    Integer userId = loginUser.getUser().getUserId();
    Boolean delete = redisTemplate.delete(KeyUtil.getLoginUserKey(userId));
    return delete;
}

controller 和 service 的代码分别如图。 在 service 中咱们删除了 Redis 中的用户信息,这也就解释了为什么过滤器中会存在 token 合法但是用户信息不存在的状况(退出登录),当然也可能有其它的特殊状况。

结语

本篇文章就到这了,下一篇应该是关于授权的内容,咱们下次再会。