若依框架-Vue实用框架(登录验证)(三)
Vue实用框架-Ruoyi(登录验证)
token的登录验证中有一步没有详细铺开,即对用户的账号密码进行校验:
package com.ruoyi.framework.web.service; @Component public class SysLoginService { 。。。省略 // 用户验证 Authentication authentication = null; try { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); AuthenticationContextHolder.setContext(authenticationToken); // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername authentication = authenticationManager.authenticate(authenticationToken); } catch (Exception e)
然后你会发现不管是点击 authenticationManager.authenticate
方法进去还是 new UsernamePasswordAuthenticationToken
都会发现后面都是源码了,难道账号密码都不需要走数据库进行校验吗?那么针对这个疑问本文简单展开下:
传统登录验证
正常情况下,让你写个登录逻辑是什么样的?我想最最简单的情况是这样:
User user = 从数据库查到该用户的信息by(name); if(user是空){ return "不存在该账号"; }else{ //继续,校验密码是否正确 if(user.getPassword()不等于传参中的password){ return "账号密码错误"; }else{ 校验完毕,继续进行之后的步骤 } }
你要记住:不管一个权限机制的框架有多花里胡哨,最终还是需要回归业务和本质,所以别想得太复杂,security的登录机制也是奔着实现上述逻辑去的,那么我们只需要知晓这个机制它的实现方式是什么样子就可以了(语法)
spring security校验用户逻辑
网上有很多security的配置攻略,会跟你说配置下加密器PasswordEncoder
、业务对象 UserDetailsService
、用户对象 UserDetails
等,最后注入容器,authenticationManager.authenticate
方法自然会调用上述的配置,但为什么会调用以及具体的流程是什么样子,不少人都是一脸懵逼:
要想知道security的认证流程,那么只能进它的源码看:
Authentication组成
三部分: Principal
:用户信息,没有认证时一般是用户名,认证后一般是用户对象 Credentials
:用户凭证,一般是密码 Authorities
:用户权限 我们最终要得到的也是这个对象,可以放到SecurityContextHolder
上下文中进行管理,只不过在这套框架中,Principal
的用户对象放在了redis中,通过令牌进行拿取,虽然最后在上下文中也存了一遍
具体流程
先是生成一个未认证的 UsernamePasswordAuthenticationToken
对象,它继承 Authentication
,这个对象肯定是不能直接拿来用的。
然后用这个未认证的对象,调用 AuthenticationManager
的 authenticate()
方法进行判断和认证:
继续authenticate接口:
找到其中某个实现类:
重点来了:
最后,如果上述操作都能正常执行下去,那么就会用user对象替换掉原先的username,并生成和返回一个带有user对象信息、已认证的 **Authentication**
对象
剩下的事情就好办了,这个Authentication
对象拿到之后该怎么用就怎么用。
关于获取的User信息:
其实上面的流程其实说的不完整,就是关于拿去用户信息的问题,上面的user对象类型都是 UserDetails
,要知道这个类具体有什么内容也只能进源码看一下:
但是在实际开发中只有以上这么几个属性怎么够呢,肯定要结合自己业务创建一种用户信息类呀,所以你会发现这个是一个接口,需要继承并实现,在ruoyi项目里就有这么一个实现类:
ruoyi框架里这个实体类具体有什么属性不一一展开了
权限集合
说权限集合之前先看下之前说的认证流程中有一步,调用下列方法来获取用户对象信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
this.getUserDetailsService()
方法其实就是 UserDetailsService
对象,同样它是一个待实现的接口,毕竟查询数据肯定要走实际的业务流程,毫无疑问:
找到它的自定义实现类: UserDetailsServiceImpl
这个方法就是获取权限信息的方法,总结一下就是通过用户的角色信息来获取对应的menu数据,形成Set集合,后面权限控制的时候会用到,至于UserDetails的**getAuthorities**
方法,重写返回的是null,说明ruoyi并没有对这个做文章
实现的流程图:
上面的流程总结下(网上盗的图,说的差不多一个意思),具体的类名方法名大概有个印象就行,关键是流程要大致能懂,security在登录验证阶段干了些什么。
流程总结
- 通过对源码的解读,我们会发现登录接口的
authenticate()
方法最终会调用UserDetailsService
的loadUserByUsername()
方法走数据库校验账号有效性,如果有效,那么将得到的User对象用实现UserDetails接口的LoginUser
接收,这个user对象还包含了一些来自该用户角色信息来中的menu数据 - 用户对象信息得到了,继续用未认证对象autn中的Credentials(密码)与取出来的用户信息中的密码进行比对,这个密码器也是可以自定义配置的
BCryptPasswordEncoder
- 通过了上述校验,就可以把之前未认证autn对象中的Principal(之前是name)换成
LoginUser
对象,并且设置成已认证,这样那么一个已认证的auth对象已经生成了,而且通过上一篇文章《前端的token获取》可以知道,做这一步不光是为了校验账号密码是否正确,auth对象中的用户信息LoginUser
会被存放到redis中,key是一个关键字+UUID,这个key又会被包含在生成返回的JWT令牌中,前端拿到了这个令牌相当于拿到了这个用户的全部信息
security配置说明
前面几步说到的UserDetailsService
、 BCryptPasswordEncoder
只是实现了接口重写方法内容,没有声明使用,所以肯定会有一个配置类去做这些------------------SecurityConfig.java:
配置类中除了UserDetailsService
、 BCryptPasswordEncoder
,还有
失败处理类
退出处理类
需要声明下哪个接口会自动触发:
token认证过滤器(重点说下这个)
security不使用Jwt行不行?当然可以!如果不使用jwt,那么这个auth对象是存放在哪,答案是session,key是 HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY
,所以可以这样取auth对象:
Authentication auth = (Authentication)session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)
如果是用jwt来配合security呢?
前面讲到登录校验使用了jwt生成令牌token返回给前端,那么后续的接口访问肯定会需要携带此令牌,security拦截器/过滤器对此进行校验,自定义jwt校验需要实现 OncePerRequestFilter
接口:
首先在配置类 SecurityConfig
中声明不使用session:
// 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
接着添加jwt过滤器:
// 添加JWT filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加CORS filter httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
有些人会奇怪为什么JWT过滤器会出现两次,看下添加方法 addFilterBefore
源码就知道了,这种添加方式有顺序性,前一个参数才是真正添加进去的
这个 JwtAuthenticationTokenFilter
就是继承OncePerRequestFilter
类的自定义过滤器:
/** * token过滤器 验证token有效性 * * @author ruoyi */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private TokenService tokenService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { //根据请求头中的token获取存放在redis中的User对象,这一步中已经包含了对token的校验 LoginUser loginUser = tokenService.getLoginUser(request); if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) { //如果上下文对象SecurityContextHolder没有authenticationToken,说明还没有 //刷新令牌有效期 tokenService.verifyToken(loginUser); // 手动组装一个认证对象,注意,这个构造方法中authenticated的set是true值,相当于登录成功返回后的auth对象,只是少了个credentials(密码) UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); //将用户访问时携带的一些额外信息也存放到authenticationToken中 authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 最后将认证对象放到上下文中,方便随时拿取 SecurityContextHolder.getContext().setAuthentication(authenticationToken); } //请求转发给过滤器链下一个filter , 如果没有filter那就是请求的资源 chain.doFilter(request, response); } }
那么结合之前的securityConfig配置,这样就实现了security+jwt联手控制后端访问权限