SpringCloud实战:Eureka+Gateway+Redis 自定义注解实现分布式 细粒度权限管理
文章目录
前言
无论是单体项目还是微服务项目,只要是涉及到用户登录的系统,大多都要有相应的权限控制机制来限制用户的访问、操作。而我们有时还需要去控制第三方系统接入的账号权限,这就给我们的权限管理带来了点麻烦。本文主要介绍如何设计一套基于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 {};
}
定义ServerApi
、ServerApiKey
存放接口信息
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
字段进行区分。如图
第三方账号接口权限关联
因为第三方账户不会有菜单权限,也不会有角色信息。所以第三方账户和接口直接通过一个多对多关系表进行绑定管理 如图:
角色与菜单、按钮关联
用户通过角色可获取自己拥有的菜单、按钮、权限标识资源。而菜单、按钮资源可以定义自己的权限标识,然后通过权限标识和接口进行关联(在项目接口内进行自定义权限的注解,声明可访问的权限标,如果没有自定义注解,默认不参与权限验证)。所以也可以通过获取用户权限标识进而筛选控制用户的接口权限 如图: