一、什么是认证鉴权
通俗来说,认证就是系统用户通过提供系统颁发给自己的信任凭证(如用户名和密码)登录系统,系统对用户提交的凭证进行验证这个过程。一般情况下,认证成功之后,系统会给用户分发令牌,令牌由用户代理客户端(如浏览器)存储,当用户需要请求系统资源时候,客户端将令牌传递给系统,系统通过检验令牌来核实访问的用户是谁,这样避免了用户每次获取系统资源都需要提供信任凭证。
鉴权,有时候也可以说是授权,是指用户在认证成功之后,系统按照之前的约定授予用户可访问的资源的权限,当用户发起对资源的请求的时候,通过鉴别已授予用户的资源和当前要访问的资源是否一致,来做数据的隔离。
可以看到,无论是认证还是授权,本质都是为了维护系统的安全性。在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