一、什么是認證鑒權
通俗來說,認證就是系統用戶通過提供系統頒發給自己的信任憑證(如用戶名和密碼)登錄系統,系統對用戶提交的憑證進行驗證這個過程。一般情況下,認證成功之后,系統會給用戶分發令牌,令牌由用戶代理客戶端(如瀏覽器)存儲,當用戶需要請求系統資源時候,客戶端將令牌傳遞給系統,系統通過檢驗令牌來核實訪問的用戶是誰,這樣避免了用戶每次獲取系統資源都需要提供信任憑證。
鑒權,有時候也可以說是授權,是指用戶在認證成功之后,系統按照之前的約定授予用戶可訪問的資源的權限,當用戶發起對資源的請求的時候,通過鑒別已授予用戶的資源和當前要訪問的資源是否一致,來做數據的隔離。
可以看到,無論是認證還是授權,本質都是為了維護系統的安全性。在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