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地址

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清晨先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值