SpringCloud實戰:Eureka+Gateway+Redis 自定義註解實現分佈式 細粒度權限管理

SpringCloud實戰:Eureka+Gateway+Redis 自定義註解實現分佈式 細粒度權限管理

Gitee 項目地址
表結構SQL地址

前言

無論是單體項目還是微服務項目,只要是涉及到用戶登錄的系統,大多都要有相應的權限控制機制來限制用戶的訪問、操作。而我們有時還需要去控制第三方系統接入的賬號權限,這就給我們的權限管理帶來了點麻煩。本文主要介紹如何設計一套基於SpringCloud 微服務框架下的 細粒度(菜單、接口、按鈕)權限管理機制。

鑑權流程設計

由於在微服務下,我們都是通過網關統一入口進行各個服務的接口訪問。所以鑑權這部分放在Gateway的過濾器內,各個服務需要把自己的接口及權限信息暴露給Gateway 鑑權流程如下
在這裏插入圖片描述

自定義Starter實現:服務自動推送接口及自定義註解權限信息到Redis的功能

自定義註解:VerifyPermission

package com.yxh.www.apiscan.annotation;

import java.lang.annotation.*;

/**
 *     權限標識校驗註解
 * </p>
 *
 * @author yangxiaohui
 * @since 2020/5/9
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface VerifyPermission {
    /**
     * 接口名稱
     */
    String name() default "";
    /**
     * 權限標識集合
     */
    String[] keys() default {};
}

定義ServerApiServerApiKey 存放接口信息

package com.yxh.www.apiscan.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
 * <p>
 *  接口信息
 * </p>
 *
 * @author yangxiaohui
 * @since 2020/5/9
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class ServerApi implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * PK
     */
    private String id;

    /**
     * 接口名稱
     */
    private String apiName;

    /**
     * 接口地址
     */
    private String apiPath;

    /**
     * 接口地址Hash
     */
    private String apiHash;

    /**
     * 接口-服務ID
     */
    private String serverId;

    /**
     * 接口-服務名
     */
    private String serverName;

    /**
     * 權限標識識別
     */
    private String permissionKeys;

    /**
     * 接口類型
     */
    private String apiType;

}

package com.yxh.www.apiscan.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import sun.plugin2.message.Serializer;

import java.io.Serializable;

/**
 * <p>
 *  存放Api版本及Key
 * </p>
 *
 * @author yangxiaohui
 * @since 2020/5/12
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class ServerApiKey implements Serializable {
    private static final long serialVersionUID = 1L;

    private String apiHash;
    private String apiKey;

    public ServerApiKey(String apiHash, String apiKey) {
        this.apiHash = apiHash;
        this.apiKey = apiKey;
    }
}

接口掃描配置類

ServerApiScanProperties實現自定義Starter配置信息的獲取
ServerApiScanConfig 實現項目接口掃描,根據自定義掃描路徑進行接口、權限的推送保存

package com.yxh.www.apiscan.properties;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * <p>
 *    服務接口掃描 - 配置類
 * </p>
 *
 * @author yangxiaohui
 * @since 2020/5/14
 */
@SuppressWarnings("ConfigurationProperties")
@ConfigurationProperties(prefix = "server.api")
public class ServerApiScanProperties {
    /**
     * 掃描路徑
     */
    private String scanPackage = "com.yxh.www.**.controller";
    /**
     * 服務名
     */
    @Value("${spring.application.name}")
    private String serverId;
    /**
     * 是否開啓接口掃描推送功能
     */
    private Boolean enable=false;

    public String getScanPackage() {
        return scanPackage;
    }

    public void setScanPackage(String scanPackage) {
        this.scanPackage = scanPackage;
    }

    public String getServerId() {
        return serverId;
    }

    public void setServerId(String serverId) {
        this.serverId = serverId;
    }
}

package com.yxh.www.apiscan.conf;

import com.alibaba.fastjson.JSONObject;
import com.yxh.www.apiscan.annotation.VerifyPermission;
import com.yxh.www.apiscan.entity.ServerApi;
import com.yxh.www.apiscan.entity.ServerApiKey;
import com.yxh.www.apiscan.properties.ServerApiScanProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.regex.Pattern;

/**
 * <p>
 *
 * </p>
 *
 * @author yangxiaohui
 * @since 2020/5/9
 */
@SuppressWarnings({"all"})
@Slf4j
@Configuration
@EnableConfigurationProperties(ServerApiScanProperties.class)
@ConditionalOnProperty(
        prefix = "server.api",
        name = "enable",
        havingValue = "true"
)
public class ServerApiScanConfig {

    private final ServerApiScanProperties serverApiScanProperties;

    public ServerApiScanConfig(ServerApiScanProperties serverApiScanProperties) {
        this.serverApiScanProperties = serverApiScanProperties;
    }

    @Bean
    public List<ServerApi> getServerApis(WebApplicationContext applicationContext, RedisTemplate<String, Object> redisTemplate) {
        List<ServerApi> serverApis = new ArrayList<>();
        String apisKey="APIS_"+serverApiScanProperties.getServerId();
        RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        // 獲取url與類和方法的對應信息
        Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
        // API版本
        TreeSet<String> apiVersionSet=new TreeSet<>();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
            // 包路徑
            String classPackage = entry.getValue().getBeanType().getName();
            // 判斷路徑是否需要加載該接口
            if (Pattern.matches(this.buildRegexPackage(), classPackage)) {
                // 構建實體
                ServerApi serverApi = new ServerApi();
                // 方法唯一標識,用來生成Hash判斷是否被更新
                StringBuffer methodKey=new StringBuffer();
                // 方法
                Method method = entry.getValue().getMethod();
                // 獲取方法請求類型
                Object[] methodTypes = entry.getKey().getMethodsCondition().getMethods().toArray();
                // 獲取方法路徑
                Object[] methodPaths = entry.getKey().getPatternsCondition().getPatterns().toArray();
                methodKey.append(serverApiScanProperties.getServerId() + methodPaths[0].toString()+methodTypes[0].toString());
                // 判斷是否包含權限標識註解
                if (method.isAnnotationPresent(VerifyPermission.class)) {
                    VerifyPermission verifyPermission = method.getAnnotation(VerifyPermission.class);
                    if (null != verifyPermission && verifyPermission.keys().length > 0) {
                        serverApi.setPermissionKeys(StringUtils.join(verifyPermission.keys(), ","));
                        methodKey.append(StringUtils.join(verifyPermission.keys()));

                    }
                    if (StringUtils.isNotBlank(verifyPermission.name())){
                        methodKey.append(verifyPermission.name());
                    }
                }
                serverApi.setApiName(methodPaths[0].toString());
                serverApi.setApiPath("/" + serverApiScanProperties.getServerId() + methodPaths[0].toString());
                serverApi.setApiType(methodTypes[0].toString());
                serverApi.setServerId(serverApiScanProperties.getServerId());
                serverApi.setServerName(serverApiScanProperties.getServerId());
                serverApis.add(serverApi);
                apiVersionSet.add(methodKey.toString());
                log.info("服務API:{} ", JSONObject.toJSONString(serverApi));
            }
        }
        // 放入Redis數據
        if (serverApis.size()>0) {
            // 刪除舊數據
            redisTemplate.delete(apisKey);
            // 放入新數據
            redisTemplate.opsForList().leftPushAll(apisKey, serverApis.toArray());
            // 把Key放入集合,表示Api發生了更改
            ServerApiKey serverApiKey=new ServerApiKey(DigestUtils.md5Hex(apiVersionSet.toString()),apisKey);
            redisTemplate.opsForList().rightPush("ServerApiKeys",serverApiKey);
        }
        return serverApis;
    }

    private String buildRegexPackage() {
        return serverApiScanProperties.getScanPackage().replace("**", "[\\w]*") + ".[\\w]*";
    }
}

授權服務獲取所有服務接口並更新接口信息

package com.yxh.www.author.conf;

import com.yxh.www.apiscan.entity.ServerApi;
import com.yxh.www.apiscan.entity.ServerApiKey;
import com.yxh.www.author.domain.SmServerApi;
import com.yxh.www.author.service.SmServerApiService;
import com.yxh.www.redis.client.RedisListService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;


/**
 * <p>
 *  API 接口信息更新
 * </p>
 *
 * @author yangxiaohui
 * @since 2020/5/11
 */
@Slf4j
@Component
@EnableScheduling
@SuppressWarnings("all")
public class ServerUpdateConfig {
    private final RedisListService redisListService;
    private final RedisTemplate<String,Object> redisTemplate;
    private final SmServerApiService smServerApiService;
    private Set<String> apiHashSet=new HashSet<>();

    public ServerUpdateConfig(List<ServerApi> serverApis,RedisListService redisListService, RedisTemplate<String, Object> redisTemplate, SmServerApiService smServerApiService) {
        this.redisListService = redisListService;
        this.redisTemplate = redisTemplate;
        this.smServerApiService = smServerApiService;
        this.initApiHashSet();
        this.updateServerApis();
        smServerApiService.updateServerApiToRedis(redisTemplate);
    }

    @Scheduled(cron = "0 */1 * * * ?")
    public void updateServerApis(){
        // 是否有待加載的API集合
        long serverApiKeyCount=redisListService.count("ServerApiKeys");
        if (serverApiKeyCount>0){
            log.info("監聽到新的Api集合,開始加載...");
            for (long i=0;i<serverApiKeyCount;i++){
                ServerApiKey serverApiKey=redisListService.popListRight("ServerApiKeys");
                log.info("加載服務API:{}",serverApiKey);
                if (!apiHashSet.contains(serverApiKey.getApiHash())){
                    // 持久化接口數據
                    List<ServerApi> serverApiList =redisListService.list(serverApiKey.getApiKey());
                    if (serverApiList.size()>0){
                        // 獲取ServerID
                        String serverId=serverApiKey.getApiKey().split("_")[1];
                        smServerApiService.updateSmServerApiByServerId(serverId,this.buildSmServerApi(serverApiList,serverApiKey.getApiHash()));
                        // 更新Redis數據
                        smServerApiService.updateServerApiToRedis(redisTemplate);
                    }
                    this.apiHashSet.add(serverApiKey.getApiHash());
                }else {
                    log.info("服務API無變更:{}",serverApiKey.getApiKey());
                }
            }
            log.info("監聽到新的Api集合,加載完畢...");
        }

    }

    /**
     * 初始化 ApiHash集合
     */
    private void initApiHashSet(){
        Set<String> dbApiHashs=smServerApiService.listApiHash();
        if (dbApiHashs!=null && dbApiHashs.size()>0){
            apiHashSet.addAll(dbApiHashs);
        }
    }

    private List<SmServerApi> buildSmServerApi(List<ServerApi> serverApiList,String apiHash){
        LocalDateTime nowTime=LocalDateTime.now();
        return new ArrayList<SmServerApi>(){{
            for (ServerApi serverApi:serverApiList){
                add(new SmServerApi(
                        DigestUtils.md5Hex(serverApi.getApiPath()),
                        serverApi.getApiName(),
                        serverApi.getApiPath(),
                        apiHash,
                        serverApi.getServerId(),
                        serverApi.getServerName(),
                        nowTime,
                        serverApi.getPermissionKeys(),
                        serverApi.getApiType()
                ));
            }
        }};
    }
}

網關自定義過濾器攔截請求並校驗權限

package com.yxh.www.gateway.filter;

import com.alibaba.fastjson.JSONObject;
import com.yxh.www.author.dto.SmAccountToken;
import com.yxh.www.author.dto.SmUserToken;
import com.yxh.www.common.constant.Constant;
import com.yxh.www.common.result.ResultBuilder;
import com.yxh.www.common.result.ResultEnum;
import com.yxh.www.common.util.JwtTokenUtil;
import com.yxh.www.redis.util.RedisResObjectUtil;
import com.sun.org.apache.regexp.internal.RE;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.regex.Pattern;


/**
 * <p>
 * 校驗請求Token過濾器
 * </p>
 *
 * @author yangxiaohui
 * @since 2020/5/8
 */
@Slf4j
@Component
@RefreshScope
public class VerifyRequestTokenFilter implements GlobalFilter, Ordered {
    @Value("${system.request.ignore-token.urls}")
    private List<String> ignoreTokenUrls;
    private final RedisTemplate<String, Object> redisTemplate;
    private Map<String, List<String>> pathValueServerApi = new ConcurrentHashMap<>();
    private Map<String, List<String>> serverApi = new ConcurrentHashMap<>();

    public VerifyRequestTokenFilter(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.reloadPathValueServerApis();
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();
        // 跳過swagger相關路徑
        if (url.contains("api-docs") || url.contains("swagger") || url.contains("doc.html")) {
            return chain.filter(exchange);
        }
        // 跳過不需要驗證的路徑
        if (null != ignoreTokenUrls && ignoreTokenUrls.contains(url)) {
            return chain.filter(exchange);
        }
        return this.verifyRequest(exchange, chain);
    }

    private Mono<Void> verifyRequest(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 響應體
        ServerHttpResponse resp = exchange.getResponse();
        // 獲取客戶端類型
        String clientType = exchange.getRequest().getHeaders().getFirst(Constant.DEFAULT_REQUEST_CLIENT_TYPE_KEY);
        // 獲取Token
        String token = exchange.getRequest().getHeaders().getFirst(Constant.DEFAULT_REQUEST_AUTHORIZATION_KEY);
        if (StringUtils.isBlank(token)) {
            // Token爲空
            return authErro(resp, ResultEnum.AUTHORIZATION_INVALID, "請求無效!");
        }
        // 解析Token
        try {
            // 根據Client解析Token
            return this.parseTokenByClientType(token, clientType, exchange, chain);
        } catch (Exception e) {
            if (e instanceof MalformedJwtException) {
                log.error("非法Token:", e);
                return authErro(resp, ResultEnum.AUTHORIZATION_INVALID, "Token非法!");
            } else if (e instanceof ExpiredJwtException) {
                log.error("Token已過期:", e);
                return authErro(resp, ResultEnum.AUTHORIZATION_INVALID, "Token已過期!");
            }
            log.error("JWT解析失敗:", e);
            return authErro(resp, ResultEnum.AUTHORIZATION_INVALID, "Token解析失敗!");
        }
    }

    private Mono<Void> parseTokenByClientType(String token, String clientType, ServerWebExchange exchange, GatewayFilterChain chain) throws Exception {
        // 響應體
        ServerHttpResponse resp = exchange.getResponse();
        String url = exchange.getRequest().getURI().getPath();
        // 解析Token
        Claims claims = JwtTokenUtil.parseJWT(token);
        log.info("Jwt解析成功:{}", claims);
        Consumer<HttpHeaders> httpHeaders;
        // Header放入必要參數
        if (StringUtils.isBlank(clientType)) {
            // 匹配內存內的 Url 權限標識
            List<String> permissionKeys = this.getPermissionKeysByUrl(url);
            // 如果內存內不存在,重新加載Redis匹配一次
            if (permissionKeys == null || permissionKeys.isEmpty()) {
                permissionKeys = this.reloadGetPermissionKeysByUrl(url);
            }
            // 判斷是否存在權限標識 如果該接口不存在權限標識就直接放行
            if (permissionKeys != null && !permissionKeys.isEmpty()) {
                // 獲取並 校驗用戶權限
                Object userObject = redisTemplate.opsForValue().get(Constant.REDIS_USER_TOKEN_KEY_PREFIX + token);
                if (userObject == null) {
                    return authErro(resp, ResultEnum.AUTHORIZATION_INVALID, "Token已過期!");
                }
                SmUserToken smUserToken = (SmUserToken) userObject;
                // 校驗權限標識
                if (smUserToken.getPermissions() != null && !smUserToken.getPermissions().isEmpty()) {
                    if (!smUserToken.getPermissions().containsAll(permissionKeys)) {
                        return authErro(resp, ResultEnum.PERMISSION_DENIED, "無權訪問!");
                    }
                }
            }
            httpHeaders = this.buildSmUserHeader(claims);
        } else if ("SDK".equals(clientType)) {
            // 校驗請求域
            String requestSourceHostIp=exchange.getRequest().getRemoteAddress().getHostString();
            String requestSourceHostName=exchange.getRequest().getRemoteAddress().getHostName();
            // 校驗請求接口權限
            Object accountTokenObject = redisTemplate.opsForValue().get(Constant.REDIS_ACCOUNT_TOKEN_KEY + token);
            if (accountTokenObject == null) {
                return authErro(resp, ResultEnum.AUTHORIZATION_INVALID, "Token已過期!");
            }
            SmAccountToken smAccountToken = (SmAccountToken) accountTokenObject;
            // 請求域
            if(!smAccountToken.getAccountScopeIps().contains(requestSourceHostIp)&&!smAccountToken.getAccountScopeIps().contains(requestSourceHostName)){
                return authErro(resp, ResultEnum.PERMISSION_DENIED, "CORS 請求被拒絕: 無效的域!");
            }
            if (!this.verifyAccountPermission(smAccountToken, url)) {
                return authErro(resp, ResultEnum.PERMISSION_DENIED, "無權訪問!");
            }
            httpHeaders = this.buildSmAccountHeader(claims);
        } else {
            return authErro(resp, ResultEnum.AUTHORIZATION_INVALID, "無效的ClientType!");
        }
        // 修改RequestHeader 放入請求標識
        ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().headers(httpHeaders).build();
        exchange.mutate().request(serverHttpRequest).build();
        return chain.filter(exchange);
    }

    /**
     * 校驗第三方賬戶接口權限
     */
    private boolean verifyAccountPermission(SmAccountToken smAccountToken, String requestUrl) {
        // 校驗常規接口
        if (smAccountToken.getApiPath().contains(requestUrl)) {
            return true;
        }
        // 校驗PathValue接口
        for (String pathValueUrl : smAccountToken.getPathValueApiPath()) {
            if (Pattern.matches(this.buildPathValueApiRegx(pathValueUrl), requestUrl)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 匹配Url獲取該Url需要的權限標識
     */
    private List<String> getPermissionKeysByUrl(String url) {
        // 常規Url校驗
        if (serverApi.containsKey(url)) {
            return serverApi.get(url);
        }
        return this.getPermissionKeysForPathValueApiByUrl(url);
    }

    /**
     * 重新加載Api資源,並匹配Url需要的權限標識
     */
    private List<String> reloadGetPermissionKeysByUrl(String url) {
        this.reloadPathValueServerApis();
        return this.getPermissionKeysByUrl(url);
    }

    /**
     * 從PathValueServerApi裏查找匹配Url
     */
    private List<String> getPermissionKeysForPathValueApiByUrl(String url) {
        if (this.pathValueServerApi != null && !this.pathValueServerApi.isEmpty()) {
            for (Map.Entry<String, List<String>> entry : this.pathValueServerApi.entrySet()) {
                if (Pattern.matches(this.buildPathValueApiRegx(entry.getKey()), url)) {
                    return entry.getValue();
                }
            }
        }
        return new ArrayList<>();
    }

    private String buildPathValueApiRegx(String pathValueApiPath) {
        return Pattern.compile("\\{[\\w]*}").matcher(pathValueApiPath).replaceAll("[\\w]*");
    }

    /**
     * 生成用戶的TokenHeader
     */
    private Consumer<HttpHeaders> buildSmUserHeader(Claims claims) {
        return httpHeader -> {
            httpHeader.set(Constant.DEFAULT_REQUEST_TOKEN_USER_ID_KEY, claims.get("id").toString());
            httpHeader.set(Constant.DEFAULT_REQUEST_TOKEN_USER_NAME_KEY, claims.get("userName").toString());
            httpHeader.set(Constant.DEFAULT_REQUEST_TOKEN_USER_PHONE_KEY, claims.get("userPhone").toString());
        };
    }

    /**
     * 生成第三方賬戶的TokenHeader
     */
    private Consumer<HttpHeaders> buildSmAccountHeader(Claims claims) {
        return httpHeader -> {
            httpHeader.set(Constant.DEFAULT_REQUEST_TOKEN_ACCOUNT_ID_KEY, claims.get("id").toString());
            httpHeader.set(Constant.DEFAULT_REQUEST_TOKEN_ACCOUNT_NAME_KEY, claims.get("accountName").toString());
            httpHeader.set(Constant.DEFAULT_REQUEST_TOKEN_ACCOUNT_SECRET_KEY_KEY, claims.get("secretKey").toString());
        };
    }

    /**
     * 構造授權失敗信息
     */
    private Mono<Void> authErro(ServerHttpResponse resp, ResultEnum resultEnum, String mess) {
        resp.setStatusCode(HttpStatus.OK);
        resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        // 構造錯誤信息
        String returnStr = JSONObject.toJSONString(ResultBuilder.error(resultEnum, mess));
        DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
        return resp.writeWith(Flux.just(buffer));
    }

    private void reloadPathValueServerApis() {
        BoundHashOperations<String, String, List<String>> boundHashPathValueServerApiOperations = redisTemplate.boundHashOps(Constant.REDIS_SM_SERVER_PATH_VALUE_API_KEY);
        BoundHashOperations<String, String, List<String>> boundHashServerApiOperations = redisTemplate.boundHashOps(Constant.REDIS_SM_SERVER_API_KEY);
        Map<String, List<String>> pathValueApiMap = boundHashPathValueServerApiOperations.entries();
        Map<String, List<String>> serverApiMap = boundHashServerApiOperations.entries();
        if (pathValueApiMap != null && !pathValueApiMap.isEmpty()) {
            this.pathValueServerApi.putAll(pathValueApiMap);
        }
        if (serverApiMap != null && !serverApiMap.isEmpty()) {
            this.serverApi.putAll(serverApiMap);
        }
    }

    @Override
    public int getOrder() {
        // 第二個執行這個過濾器
        return -2147483647;
    }
}

分析權限控制對象

權限控制模型,針對業務劃分成:用戶、第三方賬戶、角色、菜單(包括目錄、按鈕)、接口、組織幾個實體,相互關聯關係如下:
在這裏插入圖片描述

用戶和第三方賬戶、組織、角色關聯

此處第三方賬戶爲用戶的其他項目接入的賬號,類似SDK接入的概念、故用戶可能有多個第三方應用,所以用戶對應第三方賬號是一對多關係。

組織有且只有一個負責人,所以在組織表有一個負責人用戶ID進行部門主管(組織負責人)的綁定,組織成員和角色用戶關係在同一張關係表進行綁定,以objectType字段進行區分。如圖

在這裏插入圖片描述

第三方賬號接口權限關聯

因爲第三方賬戶不會有菜單權限,也不會有角色信息。所以第三方賬戶和接口直接通過一個多對多關係表進行綁定管理 如圖:
在這裏插入圖片描述

角色與菜單、按鈕關聯

用戶通過角色可獲取自己擁有的菜單、按鈕、權限標識資源。而菜單、按鈕資源可以定義自己的權限標識,然後通過權限標識和接口進行關聯(在項目接口內進行自定義權限的註解,聲明可訪問的權限標,如果沒有自定義註解,默認不參與權限驗證)。所以也可以通過獲取用戶權限標識進而篩選控制用戶的接口權限 如圖:
在這裏插入圖片描述

項目源碼

Gitee 項目地址
表結構SQL地址

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