若依认证鉴权实现原理

一、什么是认证鉴权

通俗来说,认证就是系统用户通过提供系统颁发给自己的信任凭证(如用户名和密码)登录系统,系统对用户提交的凭证进行验证这个过程。一般情况下,认证成功之后,系统会给用户分发令牌,令牌由用户代理客户端(如浏览器)存储,当用户需要请求系统资源时候,客户端将令牌传递给系统,系统通过检验令牌来核实访问的用户是谁,这样避免了用户每次获取系统资源都需要提供信任凭证。

鉴权,有时候也可以说是授权,是指用户在认证成功之后,系统按照之前的约定授予用户可访问的资源的权限,当用户发起对资源的请求的时候,通过鉴别已授予用户的资源和当前要访问的资源是否一致,来做数据的隔离。

可以看到,无论是认证还是授权,本质都是为了维护系统的安全性。在SpringBoot框架下,常见的安全框架有 SpringSecurity 和 Shiro 。
SpringSecurity官网:https://spring.io/projects/spring-security#overview
Shiro官网:http://shiro.apache.org/

二、ruoyi认证鉴权概述
在ruoyi微服务项目中,既没有用到 SpringBootSecurity 这个安全框架,也没有用到 Shiro 这个安全框架。
其认证鉴权流程大致为:用户输入用户名密码登录;系统校验用户名密码是否正确;生成uuid作为token返回给用户,并存储到redis;查询用户拥有的角色和权限并存储到redis;请求资源的时候将token转化为userId、userName存储到请求头中;根据 token 查询redis缓存中的权限并和目标资源上标注的权限名称做比对,比对成功即鉴权成功。

三、ruoyi认证鉴权实现原理

1:Auth项目的 TokenController 提供 login 方法登录

package com.ruoyi.auth.controller;

@RestController
public class TokenController{
@PostMapping("login")
public R<?> login(@RequestBody LoginBody form)
{
    // 用户登录 
    LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
    // 获取登录token 
    return R.ok(tokenService.createToken(userInfo));
}
}

2:通过 FeignClient 调用 System 根据 userName 获取用户信息(包含基本信息,角色信息,权限信息)



package com.ruoyi.system.controller;




@RestController@RequestMapping("/user")
public class SysUserController extends BaseController{

/** 
* 获取当前用户信息 
*/ 
@InnerAuth @GetMapping("/info/{username}")
public R<LoginUser> info(@PathVariable("username") String username)
{
    SysUser sysUser = userService.selectUserByUserName(username);
    // 角色集合 
    Set<String> roles = permissionService.getRolePermission(sysUser.getUserId());
    // 权限集合 
    Set<String> permissions = permissionService.getMenuPermission(sysUser.getUserId());
    LoginUser sysUserVo = new LoginUser();
    sysUserVo.setSysUser(sysUser);
    sysUserVo.setRoles(roles);
    sysUserVo.setPermissions(permissions);
    return R.ok(sysUserVo);
}

3:将 token 和用户的角色权限信息存储到 redis

package com.ruoyi.common.security.service;


@Componentpublic class TokenService{
/** 
* 创建令牌 
*/ 
public Map<String, Object> createToken(LoginUser loginUser)
{
    // 生成token 
    String token = IdUtils.fastUUID();
    loginUser.setToken(token);
    loginUser.setUserid(loginUser.getSysUser().getUserId());
    loginUser.setUsername(loginUser.getSysUser().getUserName());
    loginUser.setIpaddr(IpUtils.getIpAddr(ServletUtils.getRequest()));
    refreshToken(loginUser);
    // 保存或更新用户token 
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("access_token", token);
    map.put("expires_in", EXPIRE_TIME);
    redisService.setCacheObject(ACCESS_TOKEN + token, loginUser, EXPIRE_TIME, TimeUnit.SECONDS);
    return map;
  }
}

4:请求资源的时候,由网关中的全局过滤器从请求头中获取token,并根据token查询出 userId 和 userName,并把他们存储到请求头中,相当于在请求头中增加了userId 和userName ,然后放行该请求,该请求根据网关转发规则转发到了资源实际的微服务中。

package com.ruoyi.gateway.filter;


@Component
public class AuthFilter implements GlobalFilter, Ordered{
    @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
    {
        ......
        String userStr = sops.get(getTokenKey(token));
        JSONObject cacheObj = JSONObject.parseObject(userStr);
        String userid = cacheObj.getString("userid");
        String username = cacheObj.getString("username");
        // 设置过期时间 
        redisService.expire(getTokenKey(token), EXPIRE_TIME);
        // 设置用户信息到请求 
        addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
        addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
        // 内部请求来源参数清除 
        removeHeader(mutate, SecurityConstants.FROM_SOURCE);
        return chain.filter(exchange.mutate().request(mutate.build()).build());
    }
}

5:当请求到达资源服务器之后,通过 Controller 层的自定义注解 PreAuthorize 判断用户是否有权限访问该资源,注解中注明了此资源所需要的权限。

package com.ruoyi.system.controller;


@RestController@RequestMapping("/user")
public class SysUserController extends BaseController{

/** 
* 获取用户列表 
*/ 
@PreAuthorize(hasPermi = "system:user:list")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
{
    startPage();
    List<SysUser> list = userService.selectUserList(user);
    return getDataTable(list);
}
}

6:自定义注解 PreAuthorize 实现原理为根据 token 从redis 中查询该用户拥有的权限,和注解中 注明的权限名称做比较。

package com.ruoyi.common.security.aspect;


@Aspect
@Component
public class PreAuthorizeAspect{
    ......
    /** 
    * 验证用户是否具备某权限 
    * 
    * @param permission 权限字符串 
    * @return 用户是否具备某权限 
    */ 
    public boolean hasPermi(String permission)
    {
        LoginUser userInfo = tokenService.getLoginUser();

        return hasPermissions(userInfo.getPermissions(), permission);
    }
    ......
    /** 
    * 判断是否包含权限 * 
    * @param authorities 权限列表 从 redis 中获取 
    * @param permission 权限字符串 system:user:list 
    * @return 用户是否具备某权限 
    */ 
    private boolean hasPermissions(Collection<String> authorities, String permission)
    {
        return authorities.stream().filter(StringUtils::hasText)
            .anyMatch(x -> ALL_PERMISSION.contains(x) || PatternMatchUtils.simpleMatch(x, permission));
    }
}

7:全部鉴权方式

hasPermi:是否有某权限
lacksPermi:是否无某权限
hasAnyPermi:是否有以下权限的一种
hasRole:是否有某角色
lacksRole:是否无某角色
hasAnyRoles:是否有以下角色的一种

四、总结

若依提供的认证鉴权方式较为原始,甚至都没有集成到Spring容器中,提供的功能也比较单一,扩展性不强,不建议在中大型企业级项目中运用。

五、引用
https://spring.io/projects/spring-security#overview
http://shiro.apache.org/
https://www.yinxiang.com/everhub/note/b1425f79-3086-4f26-9f6f-430a979f96e2

若依认证鉴权实现原理

上一篇:NLP与深度学习(二)循环神经网络


下一篇:信息安全-第四章