最近在做分布式细粒度权限控制,业务需要使用各个服务的接口及权限信息进行权限的校验。
这里把各个接口的权限级别分为三种:
- 公开权限:所有的客户端都能直接访问,不参与Token校验(不需要登录)、不参与权限校验
- 内部公开权限:需要登录参与Token校验,不需要进行授权校验,所有登录用户都能访问
- 完整控制权限:需要登录并通过权限校验才能访问
下面介绍使用Nacos作为服务注册中心和配置中心,在服务注册时把服务的接口信息放入服务元数据里,其他需要监听的服务只需要监听服务注册事件并取出服务实例内的接口信息即可。
定义一个枚举进行公开权限分类PublicScopeEnum
package org.feasy.cloud.auth.config;
/**
* API公开级别枚举类
*/
public enum PublicScopeEnum {
/**
* 内部公开-只要登录了系统 都能访问
*/
INTERNAL_PUBLIC,
/**
* 全部公开-无论有没有登录系统,都会公开
*/
PUBLIC;
}
自定义注解PublicApi
用来标识接口是否是开放接口
package org.feasy.cloud.auth.config;
import java.lang.annotation.*;
/**
* API开放权限注解
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PublicApi {
/**
* 公开域,默认内部公开 即:只要登录了系统都能访问
*/
PublicScopeEnum scope() default PublicScopeEnum.INTERNAL_PUBLIC ;
}
自定义ApplicationApiContainer
存储服务接口信息,并根据开放权限控制类型进行分类
package org.feasy.cloud.auth.config;
import lombok.*;
import lombok.experimental.Accessors;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
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.*;
import java.util.regex.Pattern;
/**
* 应用服务 API容器封装类
*/
@Data
public class ApplicationApiContainer {
private final String serverKey;
private final String serverName;
/**
* 公开接口集合
*/
private List<ServerApiBO> publicApis = Collections.synchronizedList(new ArrayList<>());
/**
* 内部公开接口集合
*/
private List<ServerApiBO> internalPublicApis = Collections.synchronizedList(new ArrayList<>());
/**
* 纳入权限控制的接口集合
*
* @param applicationContext
*/
private List<ServerApiBO> accessApis = Collections.synchronizedList(new ArrayList<>());
public ApplicationApiContainer(WebApplicationContext applicationContext, String serverKey, String serverName) {
this.serverKey = serverKey;
this.serverName = serverName;
this.initThisApis(applicationContext);
}
private void initThisApis(WebApplicationContext applicationContext) {
// 取出所有的Mapping
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
// 获取url与类和方法的对应信息
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
// 遍历服务接口信息,筛选符合条件的数据
map.forEach((mappingInfo, handlerMethod) -> {
// 类
Class<?> controllerClass = handlerMethod.getBeanType();
// 包路径
String classPackage = controllerClass.getName();
if (verifyClassPackageHasProperties(classPackage, "org.feasy.cloud.**.controller")) {
// 方法
Method method = handlerMethod.getMethod();
// 获取方法请求类型
String[] methodTypes=this.getMethodTypes(mappingInfo);
// 获取方法路径
String[] methodPaths = mappingInfo.getPatternsCondition().getPatterns().toArray(new String[]{});
// 生成数据
List<ServerApiBO> serverApiBOS = this.builderServerApiBO(
controllerClass.isAnnotationPresent(RequestMapping.class) ? controllerClass.getAnnotation(RequestMapping.class).value()[0] : controllerClass.getSimpleName(),
methodTypes,
methodPaths
);
// 查看类上是否包含@PublicApi注解
if (controllerClass.isAnnotationPresent(PublicApi.class) || method.isAnnotationPresent(PublicApi.class)) {
PublicApi publicApi = controllerClass.isAnnotationPresent(PublicApi.class) ? controllerClass.getAnnotation(PublicApi.class) : method.getAnnotation(PublicApi.class);
if (publicApi.scope() == PublicScopeEnum.PUBLIC) {
this.publicApis.addAll(serverApiBOS);
} else {
this.internalPublicApis.addAll(serverApiBOS);
}
} else {
this.accessApis.addAll(serverApiBOS);
}
}
});
}
/**
* 生成一个ServerApiBO对象
*/
private List<ServerApiBO> builderServerApiBO(String moduleKey, String[] methodTypes, String[] methodPaths) {
List<ServerApiBO> serverApiBOS = new ArrayList<>();
if (methodTypes.length <= 0) {
serverApiBOS.add(new ServerApiBO()
.setModuleKey(moduleKey)
.setModuleName(moduleKey)
.setApiType("POST")
.setApiPath(this.serverKey + methodPaths[0])
);
serverApiBOS.add(new ServerApiBO()
.setModuleKey(moduleKey)
.setModuleName(moduleKey)
.setApiType("PUT")
.setApiPath(this.serverKey + methodPaths[0])
);
serverApiBOS.add(new ServerApiBO()
.setModuleKey(moduleKey)
.setModuleName(moduleKey)
.setApiType("GET")
.setApiPath(this.serverKey + methodPaths[0])
);
serverApiBOS.add(new ServerApiBO()
.setModuleKey(moduleKey)
.setModuleName(moduleKey)
.setApiType("DELETE")
.setApiPath(this.serverKey + methodPaths[0])
);
} else {
serverApiBOS.add(new ServerApiBO()
.setModuleKey(moduleKey)
.setModuleName(moduleKey)
.setApiType(methodTypes[0])
.setApiPath(this.serverKey + methodPaths[0])
);
}
return serverApiBOS;
}
private String[] getMethodTypes(RequestMappingInfo mappingInfo){
List<RequestMethod> requestMethodList = new ArrayList<>(mappingInfo.getMethodsCondition().getMethods());
String[] methodTypes=new String[requestMethodList.size()];
for (int i=0;i<requestMethodList.size();i++){
methodTypes[i]=requestMethodList.get(i).toString();
}
return methodTypes;
}
/**
* 验证包路径
*
* @param classPackage 需要验证的包路径
* @param scanPackages 验证条件的包路径,可以传入多个
* @return 验证结果,只要有一个条件符合,条件就会成立并返回True
*/
private static boolean verifyClassPackageHasProperties(String classPackage, String... scanPackages) {
for (String scanPackage : scanPackages) {
if (Pattern.matches(buildRegexPackage(scanPackage), classPackage)) {
return true;
}
}
return false;
}
/**
* 转换验证条件,使其支持正则验证
*
* @param scanPackage 验证条件包路径
* @return 验证条件正则
*/
private static String buildRegexPackage(String scanPackage) {
return scanPackage.replace("**", "[\\w]*") + ".[\\w]*";
}
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
class ServerApiBO {
private String id;
private String moduleKey;
private String moduleName;
private String apiName;
private String apiPath;
private String apiType;
}
}
自定义NacosDiscoveryClientConfiguration
配置类修改元数据
package org.feasy.cloud.auth.config;
import com.alibaba.cloud.nacos.ConditionalOnNacosDiscoveryEnabled;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.discovery.NacosWatch;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.feasy.cloud.redis.conf.FastJsonRedisSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.CommonsClientAutoConfiguration;
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.WebApplicationContext;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* nacos客户端注册至服务端时,更改服务详情中的元数据
*/
@Slf4j
@Configuration
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class})
public class NacosDiscoveryClientConfiguration {
@Value("${spring.application.name}")
private String applicationName;
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = {"spring.cloud.nacos.discovery.watch.enabled"}, matchIfMissing = true)
public NacosWatch nacosWatch(NacosDiscoveryProperties nacosDiscoveryProperties, WebApplicationContext webApplicationContext) {
//更改服务详情中的元数据,增加服务注册时间
nacosDiscoveryProperties.getMetadata().put("startup.time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
nacosDiscoveryProperties.getMetadata().put("apis", JSONObject.toJSONString(new ApplicationApiContainer(webApplicationContext,applicationName,applicationName)));
return new NacosWatch(nacosDiscoveryProperties);
}
}
启动服务注册到Nacos注册中心查看服务元数据: