基於各服務註解方式,在網關zuul中對所有下游服務權限做控制,覆蓋到所有接口,權限控制到角色、菜單、按鈕、方法

開源地址:https://gitee.com/tianyalei/zuulauth

在單體應用架構下,常見的用戶-角色-菜單權限控制模式,譬如shiro,就是在每個接口方法上加RequireRole,RequirePermission,當調用到該方法時,可以從配置的數據庫、緩存中來進行匹配,通過這種方式來進行的權限控制。

而在微服務架構下,我們會使用網關來作爲所有服務的入口,由網關來完成鑑權、分發、限流等功能。

也就是從前由各個單體服務完成的各自的權限驗證,現在全部交給zuul來統一管理,這樣能夠將權限控制到單點裏,便於統一管理,也能避免大量的非法請求、權限不足的請求落到後面的微服務裏,從而減少對網關後面的服務造成衝擊。

針對這種情況,很多方案是採用上圖的方式。具體的我也思考過,首先問題比較明顯:

1:zuul作爲集羣的入口,要承擔大量的請求,還要保證性能,如果每個請求都去和另一個服務做交互,必然會有性能損失,至少在網絡開銷上會不小。

2:AuthServer是否能夠完成精確的權限控制?大部分情況下,都是用戶-角色-菜單這種模型,關鍵在於菜單這塊,現實情況是很多接口並不是菜單,也不是按鈕,在界面上沒有任何體現,就是個接口而已。我想對接口的權限進行控制,譬如只允許某個角色的用戶才能訪問。倘若將全部接口都寫入菜單管理裏,明顯是不合適的,也很容易遺漏,工作量也很大。

比較理想的狀態還是shiro的那種寫法,譬如直接在controller或接口方法上加role、permission的註解,標註該接口的所需權限,然後在菜單管理裏添加一些重要的接口Permission權限,而不是全部的接口。然後呢,每個微服務都完成好自己的權限標註後,當有用戶請求時,就在網關層進行鑑別,由網關來控制是否放行。這樣,在每個微服務裏,就不需要做權限控制了。

這種該怎麼實現呢,單個微服務的權限信息如何告知網關,並且如何保持權限信息的同步?

我的實現方式如圖,首先各個微服務在啓動後,就上傳自己的所有權限信息到redis,zuul監聽redis的變化,及時將各微服務的接口權限變更信息更新到內存。然後auth這個微服務就是用戶、角色、菜單的控制檯,也將相應的信息更新到redis中,zuul也監聽用戶、角色、菜單的變更信息,存入內存。

當有用戶請求時,zuul就根據自己緩存的信息,對請求的接口地址進行匹配,判斷用戶角色、權限是否和各微服務裏映射的權限信息相符,然後決定是否放行。

這一套結構我已封裝爲一個框架,可以直接在pom裏添加依賴並使用,源碼在地址

<repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>
<dependency>
            <groupId>com.github.tianyaleixiaowu</groupId>
            <artifactId>zuulauth</artifactId>
            <version>13a6001c25</version>
        </dependency>

微服務端

使用方法很簡單,添加好依賴,配置好redis的連接地址,然後在代碼裏啓用權限控制,加上@EnableClientAuth註解即可。當應用啓動後就會自動上傳所有的權限信息到redis裏。

authServer端

該端是負責用戶、角色、菜單的增刪改查的,並且要負責把這些信息放到redis裏。

第一步:添加依賴,配置redis地址

第二步:通過AuthCache類來完成信息的存儲和刪除

譬如當添加了role-menu的映射後,就用authCache來save一下。當刪除了role時,就remove掉即可。

 

zuul端

第一步:添加好依賴在pom.xml,配置redis連接地址

第二步:創建好一個zuulFilter,在裏面做權限控制。

在zuul裏,用戶發起請求後,譬如使用的是jwt或其他,我們需要先取到userId或者roleId。

然後調用AuthInfoHolder.findxxx方法,來獲取用戶的角色roleSet,codeSet(某個角色的權限集合),

之後調用AuthCheck的check方法,來確定用戶權限是否匹配。

check方法需要幾個參數,分別是微服務的名字,該請求的方法(get、post、put、delete),請求的地址(/menu/add),該用戶的角色(或角色集合,Set<String>),該用戶的權限集合(Set<String>).

由於獲取用戶角色和角色權限,都是基於內存獲取,倘若用戶在authServer端修改了某個role的權限,那麼在二次查詢前,事實上redis裏是沒有這個role的權限的,只有當調用了authServer的查詢該role的權限接口後,從redis獲取失敗,那麼就會走數據庫查詢獲取,並緩存到redis,然後zuul的內存才能知道。所以在89行,判斷讀取不到時,就調用authServer的接口來獲取。那麼之後,就已經緩存了。

實例代碼:

package com.mm.dmp.zuulnacos.filter;

import com.mm.dmp.zuulnacos.exception.NoLoginException;
import com.mm.dmp.zuulnacos.filter.feign.AuthFeignClient;
import com.mm.dmp.zuulnacos.tool.JwtUtils;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.tianyalei.zuul.zuulauth.tool.FastJsonUtils;
import com.tianyalei.zuul.zuulauth.zuul.AuthChecker;
import com.tianyalei.zuul.zuulauth.zuul.AuthInfoHolder;
import io.jsonwebtoken.Claims;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Set;

import static com.mm.dmp.zuulnacos.Constant.*;
import static com.tianyalei.zuul.zuulauth.zuul.AuthChecker.*;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;

/**
 * @author wuweifeng wrote on 2019/8/12.
 */
@Component
public class PermissionFilter extends ZuulFilter {
    @Resource
    private JwtUtils jwtUtils;

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Resource
    private RouteLocator routeLocator;
    @Resource
    private AuthChecker authChecker;
    @Resource
    private AuthFeignClient authFeignClient;

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest serverHttpRequest = ctx.getRequest();

        String jwtToken = serverHttpRequest.getHeader(AUTHORIZATION);
        if (jwtToken == null) {
            //沒有Authorization
            throw new NoLoginException();
        }
        Claims claims = jwtUtils.getClaimByToken(jwtToken);
        if (claims == null) {
            throw new NoLoginException();
        }
        logger.info("token的過期時間是:" + (claims.getExpiration()));
        if (jwtUtils.isTokenExpired(claims.getExpiration())) {
            throw new NoLoginException();
        }

        //校驗role
        String userId = claims.get(USER_ID) + "";
        String roleId = claims.get(ROLE_ID) + "";
        String userType = (String) claims.get(USER_TYPE);

        //從自己內存讀取,可能爲空,說明redis裏沒有,就需要從auth服務讀取
        Set<String> userCodes = AuthInfoHolder.findByRole(roleId);
        if (CollectionUtils.isEmpty(userCodes)) {
            String codes = authFeignClient.findCodesByRole(Long.valueOf(roleId));
            userCodes = FastJsonUtils.toBean(codes, Set.class);
        }

        //類似於  /zuuldmp/core/test
        String requestPath = serverHttpRequest.getRequestURI();
        //獲取請求的method
        String method = serverHttpRequest.getMethod().toUpperCase();
        //獲取所有路由信息,找到該請求對應的appName
        List<Route> routeList = routeLocator.getRoutes();
        //Route{id='one', fullPath='/zuuldmp/auth/**', path='/**', location='auth', prefix='/zuuldmp/auth',
        String appName = null;
        String path = null;
        for (Route route : routeList) {
            if (requestPath.startsWith(route.getPrefix())) {
                //取到該請求對應的微服務名字
                appName = route.getLocation();
                path = requestPath.replace(route.getPrefix(), "");
            }
        }
        if (appName == null) {
            throw new NoLoginException(404, "不存在的服務");
        }

        //取到該用戶的role、permission
        //訪問  auth 服務的 GET  /project/my 接口
        int code = authChecker.check(appName,
                method,
                path,
                userType,
                userCodes);
        switch (code) {
            case CODE_NO_APP:
                throw new NoLoginException(code, "不存在的服務");
            case CODE_404:
                throw new NoLoginException(code, "無此接口或GET POST方法不對");
            case CODE_NO_ROLE:
                throw new NoLoginException(code, "用戶無該接口所需role");
            case CODE_NO_CODE:
                throw new NoLoginException(code, "用戶無該接口所需權限");
            case CODE_OK:
                ctx.addZuulRequestHeader(USER_ID, userId);
                ctx.addZuulRequestHeader(USER_TYPE, userType);
                ctx.addZuulRequestHeader(ROLE_ID, roleId);
            default:
                break;
        }
        return null;
    }
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章