若依認證鑒權實現原理


一、什么是認證鑒權

通俗來說,認證就是系統用戶通過提供系統頒發給自己的信任憑證(如用戶名和密碼)登錄系統,系統對用戶提交的憑證進行驗證這個過程。一般情況下,認證成功之后,系統會給用戶分發令牌,令牌由用戶代理客戶端(如瀏覽器)存儲,當用戶需要請求系統資源時候,客戶端將令牌傳遞給系統,系統通過檢驗令牌來核實訪問的用戶是誰,這樣避免了用戶每次獲取系統資源都需要提供信任憑證。

鑒權,有時候也可以說是授權,是指用戶在認證成功之后,系統按照之前的約定授予用戶可訪問的資源的權限,當用戶發起對資源的請求的時候,通過鑒別已授予用戶的資源和當前要訪問的資源是否一致,來做數據的隔離。

可以看到,無論是認證還是授權,本質都是為了維護系統的安全性。在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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM