若依框架-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联手控制后端访问权限

posted @ 2023-03-29 08:09  爵岚  阅读(1932)  评论(0编辑  收藏  举报