项目介绍
Nepxion Permission是一款基于Spring Cloud的微服务API权限框架,并通过Redis分布式缓存进行权限缓存。它采用Nepxion Matrix AOP框架进行切面实现,支持注解调用方式,也支持Rest调用方式
项目地址
https://toscode.gitee.com/nepxion/Permission
原理解析
permission-aop-starter
自动配置
permission-aop-starter
项目下spring.factories
中
com.nepxion.permission.annotation.EnablePermission=\
com.nepxion.permission.configuration.PermissionAopConfiguration
PermissionAopConfiguration
注入了PermissionAutoScanProxy
,PermissionInterceptor
,PermissionAuthorization
,PermissionPersister
和PermissionFeignBeanFactoryPostProcessor
。
@Configuration
public class PermissionAopConfiguration {
//...
@Value("${" + PermissionConstant.PERMISSION_SCAN_PACKAGES + ":}")
private String scanPackages;
@Bean
public PermissionAutoScanProxy permissionAutoScanProxy() {
return new PermissionAutoScanProxy(scanPackages);
}
@Bean
public PermissionInterceptor permissionInterceptor() {
return new PermissionInterceptor();
}
@Bean
public PermissionAuthorization permissionAuthorization() {
return new PermissionAuthorization();
}
@Bean
public PermissionPersister permissionPersister() {
return new PermissionPersister();
}
@Bean
public PermissionFeignBeanFactoryPostProcessor permissionFeignBeanFactoryPostProcessor() {
return new PermissionFeignBeanFactoryPostProcessor();
}
}
权限拦截器
PermissionAutoScanProxy
核心功能就是给带有注解Permission
的方法生成代理类,收集所有的PermissionEntity
。
public class PermissionAutoScanProxy extends DefaultAutoScanProxy {
private static final long serialVersionUID = 3188054573736878865L;
@Value("${" + PermissionConstant.PERMISSION_AUTOMATIC_PERSIST_ENABLED + ":true}")
private Boolean automaticPersistEnabled;
@Value("${" + PermissionConstant.SERVICE_NAME + "}")
private String serviceName;
@Value("${" + PermissionConstant.SERVICE_OWNER + ":Unknown}")
private String owner;
private String[] commonInterceptorNames;
@SuppressWarnings("rawtypes")
private Class[] methodAnnotations;
private List<PermissionEntity> permissions = new ArrayList<PermissionEntity>();
public PermissionAutoScanProxy(String scanPackages) {
super(scanPackages, ProxyMode.BY_METHOD_ANNOTATION_ONLY, ScanMode.FOR_METHOD_ANNOTATION_ONLY);
}
@Override
protected String[] getCommonInterceptorNames() {
if (commonInterceptorNames == null) {
commonInterceptorNames = new String[] { "permissionInterceptor" };
}
return commonInterceptorNames;
}
@SuppressWarnings("unchecked")
@Override
protected Class<? extends Annotation>[] getMethodAnnotations() {
if (methodAnnotations == null) {
methodAnnotations = new Class[] { Permission.class };
}
return methodAnnotations;
}
@Override
protected void methodAnnotationScanned(Class<?> targetClass, Method method, Class<? extends Annotation> methodAnnotation) {
if (automaticPersistEnabled) {
if (methodAnnotation == Permission.class) {
Permission permissionAnnotation = method.getAnnotation(Permission.class);
String name = permissionAnnotation.name();
if (StringUtils.isEmpty(name)) {
throw new PermissionAopException("Annotation [Permission]'s name is null or empty");
}
String label = permissionAnnotation.label();
String description = permissionAnnotation.description();
// 取类名、方法名和参数类型组合赋值
String className = targetClass.getName();
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
String parameterTypesValue = ProxyUtil.toString(parameterTypes);
String resource = className + "." + methodName + "(" + parameterTypesValue + ")";
PermissionEntity permission = new PermissionEntity();
permission.setName(name);
permission.setLabel(label);
permission.setType(PermissionType.API.getValue());
permission.setDescription(description);
permission.setServiceName(serviceName);
permission.setResource(resource);
permission.setCreateOwner(owner);
permission.setUpdateOwner(owner);
permissions.add(permission);
}
}
}
public List<PermissionEntity> getPermissions() {
return permissions;
}
}
PermissionInterceptor
,根据方法上的UserId
或者UserType
,获取用户;或者根据方法上的Token
获取token,再根据token信息获取用户数据。
public class PermissionInterceptor extends AbstractInterceptor {
//...
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (interceptionEnabled) {
Permission permissionAnnotation = getPermissionAnnotation(invocation);
if (permissionAnnotation != null) {
String name = permissionAnnotation.name();
String label = permissionAnnotation.label();
String description = permissionAnnotation.description();
return invokePermission(invocation, name, label, description);
}
}
return invocation.proceed();
}
private Object invokePermission(MethodInvocation invocation, String name, String label, String description) throws Throwable {
if (StringUtils.isEmpty(serviceName)) {
throw new PermissionAopException("Service name is null or empty");
}
if (StringUtils.isEmpty(name)) {
throw new PermissionAopException("Annotation [Permission]'s name is null or empty");
}
String proxyType = getProxyType(invocation);
String proxiedClassName = getProxiedClassName(invocation);
String methodName = getMethodName(invocation);
if (frequentLogPrint) {
LOG.info("Intercepted for annotation - Permission [name={}, label={}, description={}, proxyType={}, proxiedClass={}, method={}]", name, label, description, proxyType, proxiedClassName, methodName);
}
UserEntity user = getUserEntityByIdAndType(invocation);
if (user == null) {
user = getUserEntityByToken(invocation);
}
if (user == null) {
throw new PermissionAopException("No user context found");
}
String userId = user.getUserId();
String userType = user.getUserType();
// 检查用户类型白名单,决定某个类型的用户是否要执行权限验证拦截
boolean checkUserTypeFilters = checkUserTypeFilters(userType);
if (checkUserTypeFilters) {
boolean authorized = permissionAuthorization.authorize(userId, userType, name, PermissionType.API.getValue(), serviceName);
if (authorized) {
return invocation.proceed();
} else {
String parameterTypesValue = getMethodParameterTypesValue(invocation);
throw new PermissionAopException("No permision to proceed method [name=" + methodName + ", parameterTypes=" + parameterTypesValue + "], permissionName=" + name + ", permissionLabel=" + label);
}
}
return invocation.proceed();
}
private UserEntity getUserEntityByIdAndType(MethodInvocation invocation) {
// 获取方法参数上的注解值
String userId = getValueByParameterAnnotation(invocation, UserId.class, String.class);
String userType = getValueByParameterAnnotation(invocation, UserType.class, String.class);
if (StringUtils.isEmpty(userId) && StringUtils.isNotEmpty(userType)) {
throw new PermissionAopException("Annotation [UserId]'s value is null or empty");
}
if (StringUtils.isNotEmpty(userId) && StringUtils.isEmpty(userType)) {
throw new PermissionAopException("Annotation [UserType]'s value is null or empty");
}
if (StringUtils.isEmpty(userId) && StringUtils.isEmpty(userType)) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
userId = attributes.getRequest().getHeader(PermissionConstant.USER_ID);
userType = attributes.getRequest().getHeader(PermissionConstant.USER_TYPE);
}
}
if (StringUtils.isEmpty(userId) || StringUtils.isEmpty(userType)) {
return null;
}
UserEntity user = new UserEntity();
user.setUserId(userId);
user.setUserType(userType);
return user;
}
private UserEntity getUserEntityByToken(MethodInvocation invocation) {
// 获取方法参数上的注解值
String token = getValueByParameterAnnotation(invocation, Token.class, String.class);
if (StringUtils.isEmpty(token)) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
token = attributes.getRequest().getHeader(PermissionConstant.TOKEN);
}
}
if (StringUtils.isEmpty(token)) {
return null;
}
// 根据token获取userId和userType
UserEntity user = userResource.getUser(token);
if (user == null) {
throw new PermissionAopException("No user found for token=" + token);
}
return user;
}
}
UserResource
接口定义了Open Feign
的接口格式,提供了根据token获取用户的接口。
@FeignClient(value = "${permission.service.name}")
public interface UserResource {
@RequestMapping(path = "/user/getUser/{token}", method = RequestMethod.GET)
UserEntity getUser(@PathVariable(value = "token") String token);
}
在permission-service
服务中,有具体的RestController
实现了UserResource
,提供了获取用户信息的真正接口。
@RestController
public class UserResourceImpl implements UserResource {
private static final Logger LOG = LoggerFactory.getLogger(UserResourceImpl.class);
// 根据Token获取User实体
@Override
public UserEntity getUser(@PathVariable(value = "token") String token) {
// 当前端登录后,它希望送token到后端,查询出用户信息(并以此调用authorize接口做权限验证,permission-aop已经实现,使用者并不需要关心)
// 需要和单点登录系统,例如OAuth或者JWT等系统做对接
// 示例描述token为abcd1234对应的用户为lisi
LOG.info("Token:{}", token);
if (StringUtils.equals(token, "abcd1234")) {
UserEntity user = new UserEntity();
user.setUserId("lisi");
user.setUserType("LDAP");
return user;
}
return null;
}
}
PermissionInterceptor#checkUserTypeFilters
,检查用户类型白名单。
private boolean checkUserTypeFilters(String userType) {
if (StringUtils.isEmpty(whitelist)) {
return true;
}
if (whitelist.toLowerCase().indexOf(userType.toLowerCase()) > -1) {
return true;
}
return false;
}
用户认证
PermissionAuthorization#authorize
,调用远程服务,判断是否授权。会判断缓存中是否存在。
// 通过自动装配的方式,自身调用自身的注解方法
@Autowired
private PermissionAuthorization permissionAuthorization;
public boolean authorize(String userId, String userType, String permissionName, String permissionType, String serviceName) {
return permissionAuthorization.authorizeCache(userId, userType, permissionName, permissionType, serviceName);
}
@Cacheable(name = "cache", key = "#userId + \"_\" + #userType + \"_\" + #permissionName + \"_\" + #permissionType + \"_\" + #serviceName", expire = -1L)
public boolean authorizeCache(String userId, String userType, String permissionName, String permissionType, String serviceName) {
boolean authorized = permissionResource.authorize(userId, userType, permissionName, permissionType, serviceName);
LOG.info("Authorized={} for userId={}, userType={}, permissionName={}, permissionType={}, serviceName={}", authorized, userId, userType, permissionName, permissionType, serviceName);
return authorized;
}
PermissionResource
提供了授权方法。
@FeignClient(value = "${permission.service.name}")
public interface PermissionResource {
@RequestMapping(path = "/permission/persist", method = RequestMethod.POST)
void persist(@RequestBody List<PermissionEntity> permissions);
@RequestMapping(path = "/authorization/authorize/{userId}/{userType}/{permissionName}/{permissionType}/{serviceName}", method = RequestMethod.GET)
boolean authorize(@PathVariable(value = "userId") String userId, @PathVariable(value = "userType") String userType, @PathVariable(value = "permissionName") String permissionName, @PathVariable(value = "permissionType") String permissionType, @PathVariable(value = "serviceName") String serviceName);
}
PermissionResourceImpl#authorize
,提供具体的实现。
// 权限验证
@Override
public boolean authorize(@PathVariable(value = "userId") String userId, @PathVariable(value = "userType") String userType, @PathVariable(value = "permissionName") String permissionName, @PathVariable(value = "permissionType") String permissionType, @PathVariable(value = "serviceName") String serviceName) {
LOG.info("权限获取: userId={}, userType={}, permissionName={}, permissionType={}, serviceName={}", userId, userType, permissionName, permissionType, serviceName);
// 验证用户是否有权限
// 需要和用户系统做对接,userId一般为登录名,userType为用户系统类型。目前支持多用户类型,所以通过userType来区分同名登录用户,例如财务系统有用户叫zhangsan,支付系统也有用户叫zhangsan
// permissionName即在@Permission注解上定义的name,permissionType为权限类型,目前支持接口权限(API),网关权限(GATEWAY),界面权限(UI)三种类型的权限(参考PermissionType.java类的定义)
// serviceName即服务名,在application.properties里定义的spring.application.name
// 对于验证结果,在后端实现分布式缓存,可以避免频繁调用数据库而出现性能问题
// 示例描述用户zhangsan有权限,用户lisi没权限
if (StringUtils.equals(userId, "zhangsan")) {
return true;
} else if (StringUtils.equals(userId, "lisi")) {
return false;
}
return true;
}
权限数据持久化
PermissionPersister#onApplicationEvent
,失败进行重试。
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (automaticPersistEnabled) {
if (event.getApplicationContext().getParent() instanceof AnnotationConfigApplicationContext) {
LOG.info("Start to persist with following permission list...");
LOG.info("------------------------------------------------------------");
List<PermissionEntity> permissions = permissionAutoScanProxy.getPermissions();
if (CollectionUtils.isNotEmpty(permissions)) {
for (PermissionEntity permission : permissions) {
LOG.info("Permission={}", permission);
}
persist(permissions, automaticPersistRetryTimes + 1);
} else {
LOG.warn("Permission list is empty");
}
LOG.info("------------------------------------------------------------");
}
}
}
PermissionFeignBeanFactoryPostProcessor
后置处理器。
public class PermissionFeignBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinition definition = beanFactory.getBeanDefinition("feignContext");
definition.setDependsOn("eurekaServiceRegistry", "inetUtils");
}
}
permission-service-starter
自动配置
permission-service-starter
的项目下自动配置spring.factories
。
com.nepxion.permission.service.annotation.EnablePermissionSerivce=\
com.nepxion.permission.service.configuration.PermissionServiceConfiguration
PermissionServiceConfiguration
注入了PermissionResource
和UserResource
。
@Configuration
public class PermissionServiceConfiguration {
@Bean
public PermissionResource permissionResource() {
return new PermissionResourceImpl();
}
@Bean
public UserResource userResource() {
return new UserResourceImpl();
}
}
permission-feign-starter
自动配置
com.nepxion.permission.feign.annotation.EnablePermissionFeign=\
com.nepxion.permission.configuration.PermissionFeignConfiguration
PermissionFeignConfiguration
注入了PermissionFeignInterceptor
@Configuration
public class PermissionFeignConfiguration {
@Bean
public PermissionFeignInterceptor permissionFeignInterceptor() {
return new PermissionFeignInterceptor();
}
}
PermissionFeignInterceptor
,如果请求头上有user-id
,user-type
,token
,调用feign的时候复制一份。
public class PermissionFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return;
}
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames == null) {
return;
}
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String header = request.getHeader(headerName);
if (PermissionFeignConstant.PERMISSION_FEIGN_HEADERS.contains(headerName.toLowerCase())) {
requestTemplate.header(headerName, header);
}
}
}
}
PermissionFeignConstant
中定义了PERMISSION_FEIGN_HEADERS
。
public class PermissionFeignConstant {
public static final String PERMISSION_FEIGN_ENABLED = "permission.feign.enabled";
public static final String TOKEN = "token";
public static final String USER_ID = "user-id";
public static final String USER_TYPE = "user-type";
public static final String PERMISSION_FEIGN_HEADERS = TOKEN + ";" + USER_ID + ";" + USER_TYPE;
}
服务调用流程解析
ermission-springcloud-my-service-example
服务启动会执行MyController
的方法。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = { "com.nepxion.permission.api" })
@EnablePermission
@EnableCache
public class MyApplication {
private static final Logger LOG = LoggerFactory.getLogger(MyApplication.class);
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(MyApplication.class, args);
MyController myController = applicationContext.getBean(MyController.class);
try {
LOG.info("Result : {}", myController.doA("zhangsan", "LDAP", "valueA"));
} catch (Exception e) {
LOG.error("Error", e);
}
try {
LOG.info("Result : {}", myController.doB("abcd1234", "valueB"));
} catch (Exception e) {
LOG.error("Error", e);
}
}
}
MyController
提供了三种demo,作为参考。
@RestController
public class MyController {
private static final Logger LOG = LoggerFactory.getLogger(MyController.class);
// 显式基于UserId和UserType注解的权限验证,参数通过注解传递
@RequestMapping(path = "/doA/{userId}/{userType}/{value}", method = RequestMethod.GET)
@Permission(name = "A-Permission", label = "A权限", description = "A权限的描述")
public int doA(@PathVariable(value = "userId") @UserId String userId, @PathVariable(value = "userType") @UserType String userType, @PathVariable(value = "value") String value) {
LOG.info("===== doA被调用");
return 123;
}
// 显式基于Token注解的权限验证,参数通过注解传递
@RequestMapping(path = "/doB/{token}/{value}", method = RequestMethod.GET)
@Permission(name = "B-Permission", label = "B权限", description = "B权限的描述")
public String doB(@PathVariable(value = "token") @Token String token, @PathVariable(value = "value") String value) {
LOG.info("----- doB被调用");
return "abc";
}
// 隐式基于Rest请求的权限验证,参数通过Header传递
@RequestMapping(path = "/doC/{value}", method = RequestMethod.GET)
@Permission(name = "C-Permission", label = "C权限", description = "C权限的描述")
public boolean doC(@PathVariable(value = "value") String value) {
LOG.info("----- doC被调用");
return true;
}
}
第一个接口是用户zhangsan
,认证结果是可以的。
第二个接口是token,需要根据token获取用户,token等于abcd1234
的用户是lisi
,lisi是认证不通过的。
Redis日志打印
RedisCacheDelegateImpl#invokeCacheable
,判断配置文件的属性决定日志打印。
@Value("${frequent.log.print:false}")
private Boolean frequentLogPrint;
public Object invokeCacheable(MethodInvocation invocation, List<String> keys, long expire) throws Throwable {
Object object = null;
try {
object = this.valueOperations.get(keys.get(0));
if (this.frequentLogPrint) {
LOG.info("Before invocation, Cacheable key={}, cache={} in Redis", keys, object);
}
} catch (Exception var9) {
if (!this.cacheAopExceptionIgnore) {
throw var9;
}
LOG.error("Redis exception occurs while Cacheable", var9);
}
}
总结一下
-
如果自己使用这套框架,首先
permission-service-starter
是需要自己去实现的,需要自己定义根据token获取用户信息,需要自己定义根据用户判断权限认证。 -
核心架构只有Feign的使用,可以适配任意的注册中心。