前言
最近花了两天复习了一下spring security,在网上找了很多帖子和视频,感觉这些帖子感觉讲解的很零散,毕竟spring security里面默认内置了很多场景的解决方案。这里我们只关注spring security + jwt + mybaitsPlus 的解决方案。此方案是我找了很多视频帖子最后总结出来的,最后我参考了ruoyi项目和spring-security-plus,发现这种方式和我自己总结的大同小异,就决定吧这种方式记录下来,这里我们只把和spring security认证和授权的部分拿出来
说明
若想直接用一个权限管理的脚手架,建议直接用rouyi,自己搭太累了(别问我怎么知道的)
- 首先我们要明确安全框架主要有两个部分: 认证 和 授权
- 认证是什么 :我们可以理解为登录功能
- 授权是什么 :授权就是spring security会将你规定的权限和用户带有的权限进行匹配,若校验成功则授权成功
登录
- 接着我们思考一下登录需要什么
- 输入用户名密码
- 将输入的密码进行校验
- 未登录获取资源怎么办
- 怎么验证token,验证token后怎么让spring security知道认证通过了
- 我们再来想一下code时候需要做什么
- 密码暗文加密
- 如何将用户信息封装起来
接下来我们了解一下spring security 都有哪些组件(注意仅针对使用jwt的方式)
我们从一个完整的从登录到认证的完整流程中了解spring security组件
- 首先输入用户名密码登录,登录会调用/login(注意这里是自定义的方法,不是默认的/login,在强调一次仅针对使用jwt的方式,为什么要这么强调呢,因为我是先搭了一个jwt,然后看的spring security视频,蒙了很久)
这个方法调用service的login,获得令牌,将令牌返回
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody) {
//我当时也是起名叫AjaxResult的,好巧
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return ajax;
}
public String login(String username, String password, String code, String uuid) {
//查看缓存中有没有已经保存过的信息
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String captcha = redisCache.getCacheObject(verifyKey);
redisCache.deleteObject(verifyKey);
if (captcha == null) {
throw new CaptchaExpireException();
}
//是否禁用
if (!code.equalsIgnoreCase(captcha)) {
throw new CaptchaException();
}
// 用户验证
Authentication authentication = null;
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
}
- 这里我们看到了第一个spring security 组件 UserDetailsService 他提供了一个loadUserByUsername(username) 方法,可以实现这个方法,根据用户名从数据库取出用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名取出user
SysUser user = userService.selectUserByUserName(username);
return createLoginUser(user);
}
- 还是在service里有第二个组件 authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));这个方法就会去调用loadUserByUsername得到数据库数据中的密码与参数(个 就这个)进行匹配,匹配成功后将用户信息取出保存到token里,返回token 登录部分完成
- 认证部分(我们需要解析token来判断是否是登录状态),这个实现很简单,我们知道spring security是通过过滤器链的方式来进行控制的,我们只需要在合适的地方加入一个解析jwt的过滤器即可,我们先不管他加到哪里,先来看一下做了什么
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException{
//从token中获得用户信息(在登录的时候把用户信息放到了token里返回给客户端,客户端发送请求的时候就会带着token)
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
// 获取到用户信息的话就将用户信息封装到UsernamePasswordAuthenticationToken里放入上下文
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
- 我们来看一下UsernamePasswordAuthenticationToken 类,我们只看他两个构造器就行了,其他都是getter和setter
// 可以看到两个参数构造器会让认证失败
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
// 三个参数构造器会认证成功
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
-
所以我们通过创建三个参数构造器的UsernamePasswordAuthenticationToken的参数就可以让spring security 认为认证成功(能取出用户信息就说明jwt已经验证通过了)
-
这样我们认证流程就完成了,我们看一下这个filter方到了那里,找到security config
// 加载了UsernamePasswordAuthenticationFilter.class过滤器前面(后面那个只是个代表个位置和前面没有任何关系)
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
- 现在我们回过来看一下登录失败怎么办,在spring security 登录失败会抛出异常,异常会被AuthenticationEntryPoint处理,我们只需要实现它的方法给他一个处理方式即可,我们来写一个登录失败的处理器
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException
{
int code = HttpStatus.UNAUTHORIZED;
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
ServletUtils.renderString(response,
//写回一个json
JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
将他配置到security config 中
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
...
httpSecurity.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
...
授权
在从数据库拿到用户信息的里面已经包含了权限信息,所以我们从token中取到的用户信息里面已经带了权限信息,那么spring security 是如何授权的呢,spring security 提供了两种方式,一种是在config配置中配置,一种是使用注解方式,由于这里我学的时候没有什么很难搞清楚的地方就略了
最后聊一聊
认证这里很乱是因为认证方式比较灵活
- 可以将用户信息在每次认证的时候再从数据库去,登陆的时候token只存username,这样可以热部署,但是每次都会查询数据库
- token里面已经带了用户所有信息,认证的时候只要jwt验证通过就算认证成功,这样不用每次都查数据库,但是这样会导致token很大
- 还可以token存放一个标识,认证的时候jwt验证通过后取出标识,从redis中根据标识取出用户信息,能取出用户信息就算认证成功 --> ruoyi 用的是这种