最近在看三更的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生成可以用现有Jwt类,也可以自己随便写一个加密工具类,比如AES-CBC处理,注意要对称加密算法,不然发来token都解析不了,现在我就先用打包好的Jwt工具类了
还是回到服务~
这里将可以标志用户的信息进行token就行,比如id
,userName
都行,甚至是直接将UserDetails实现类处理。
token得到后就保存在Redis,然后返回给用户
注意:Redis要序列化,这里详情就不说了。直接用数据库也行
大体流程:
过滤器的任务是获取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);
至此整个认证流程就算结束了。至于觉得还有注销流程,其实注销只是一个普通服务而已,并没有特殊的流程。当然我这里还是会写一下的
主要的流程就是:
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写好了