SpringSecurity认证流程小白详解

前言


最近在看三更的SpingSecurity的视频,详细理了一下代码流程,就写个blog记录一下好了。

我的理解是三个步骤(小白刚入门,细节优化可能没考虑到):

 1. 登录认证
 2. token处理并保存到缓存,并发回给用户
 3. 过滤解析用户发来的token

准备工作已经提前做好


登录认证

先将登录接口放行(配置类需要继承WebSecurityConfigurerAdapter再重写方法)

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

用户将账号密码从接口丢入后台,然后交给登录服务

@RequestMapping("/user/login")
public Map<String, Object> login(@RequestBody User user) {
    // 登录
	return loginService.login(user);
}

在服务中,我们先准备一个认证(AuthenticationToken),实现类很多,这里直接用UsernamePasswordAuthenticationToken
我的感觉是申请一个令牌

// 注册一个令牌
Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());

此时我们需要一个AuthenticationManager,我的理解是一个认证器
我们会在SecurityConfig中暴露出去

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
	return super.authenticationManagerBean();
}

然后进行认证

// 用户认证 (AuthenticationManager去认证)
Authentication authenticate = authenticationManager.authenticate(authentication);

认证的时候自动调用一个UserDetailsService服务去获得用户信息,这个服务需要实现一下

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, s);
        User user = userMapper.selectOne(queryWrapper);

        if (user == null) {
            throw new RuntimeException("未通过用户名找到用户");
        }
        //Todo 查询用户权限信息

        LoginUser loginUser = new LoginUser();
        loginUser.setUser(user);
        return loginUser;
    }
}

重写的方法需要返回一个UserDetails对象。还得重写这个对象。这个代码就不给出了,放需要的用户相关信息在里面就行


返回的UserDetails会自动进行密码核对,所以又需要一个密码配对器,这里用Spring提供的就行

@Bean
public PasswordEncoder passwordEncoder() {
    // 加密器
    return new BCryptPasswordEncoder();
}

简单原理就是先对传过来的密码进行加密,然后匹配密文,这个容易实现,不过最好使用不可逆的算法进行加密

回到服务~
当返回的令牌Authentication不为null时说明认证通过。我们可以通过getPrincipal()方法去得到UserDetails,自然也能获得保存在里面的信息。


Token的生成和保存

token生成可以用现有Jwt类,也可以自己随便写一个加密工具类,比如AES-CBC处理,注意要对称加密算法,不然发来token都解析不了,现在我就先用打包好的Jwt工具类了


还是回到服务~
这里将可以标志用户的信息进行token就行,比如iduserName都行,甚至是直接将UserDetails实现类处理。

token得到后就保存在Redis,然后返回给用户
注意:Redis要序列化,这里详情就不说了。直接用数据库也行


3. 过滤解析用户发来的token

大体流程:

  • 创建过滤器
  • 在配置里面添加过滤器

过滤器的任务是获取token,制作令牌给后面的过滤器

创建过滤器

这里直接继承spring提供的OncePerRequestFilter
我们可以从请求头中获取token

// 获取token
String token = httpServletRequest.getHeader("token");

注意如果获取不到token要放行这个请求,这里可能有疑惑:为什么放行?

  • 因为我们这个过滤器的任务就是给令牌授权,也就是将一个令牌交给后面的过滤器,至于一个没授权的令牌是否放行应该交给其他过滤器。

拿到token 后我们就通过之前的对称算法解析token,获取用户信息

Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
// 从redis中获取用户信息
String redisKey = "login:" + userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);

将用户信息存入SecurityContextHolder
这个SecurityContextHolder我的理解就是一个全局变量,是一个容器。一个请求会有一个,所有过滤器甚至Service都能使用。

// 存入SecurityContextHolder
//Todo 获取选项信息到 Authentication
Authentication authentication = new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(authentication);

所以我们将用户信息作为Principal保存在令牌,令牌又放在容器里面,这里可以放任何对象,用户id都行,主要是给Service层判断是哪个用户

此时我们的令牌是三个参数,默认进行已授权操作,这个是根据不同的类来决定的,不是所有的令牌类都会默认授权。

最后还是放行,这样我们的过滤器就算完成了


当然,别忘了将我们写的过滤器配置上去
在配置类里面添加:

http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);

至此整个认证流程就算结束了。至于觉得还有注销流程,其实注销只是一个普通服务而已,并没有特殊的流程。当然我这里还是会写一下的

退出登录

主要的流程就是:

  • 获取用户信息
  • 根据token认证流程,更改用户状态,如删除缓存,将hasLogin置为false都行,看自己的认证实现就行,这里推荐redis删除缓存

可以直接从SecurityContextHolder中获得刚刚在过滤器中放的用户信息,找到用户,然后去redis删除缓存

注意:要清除一下SecurityContextHolder的缓存,即SecurityContextHolder.clearContext();

虽然很多人觉得这步可以不用,但我们了解到,Response会根据过滤链再走一遍回去,这时如果前面的过滤器重新使用SecurityContextHolder拿到的仍然是一个已认证的令牌,可能会有额外的副作用。

放上代码

@Override
public Map<String, Object> logout() {
    // 获取SecurityContextHolder中的用户

    SecurityContext context = SecurityContextHolder.getContext();
    Authentication authentication = context.getAuthentication();
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    User user = loginUser.getUser();
    Integer id = user.getId();
    // 删除redis中的值
    String redisKey = "login:" + id.toString();
    redisCache.deleteObject(redisKey);

    // 清空缓存
    SecurityContextHolder.clearContext();

    Map<String, Object> map = new HashMap<>();
    map.put("code", "200");
    map.put("msg", "注销成功");
    return map;
}

认真理一下,其实流程还是很清晰的。写的不好大佬偷偷笑就行。。。
也是第一次了解这个框架,可能还要后面多点看源码,至于授权我在下一篇Blog写好了

你可能感兴趣的:(java,mybatis,spring,boot)