项目介绍
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;@Beanpublic PermissionAutoScanProxy permissionAutoScanProxy() {return new PermissionAutoScanProxy(scanPackages);}@Beanpublic PermissionInterceptor permissionInterceptor() {return new PermissionInterceptor();}@Beanpublic PermissionAuthorization permissionAuthorization() {return new PermissionAuthorization();}@Beanpublic PermissionPersister permissionPersister() {return new PermissionPersister();}@Beanpublic 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);}@Overrideprotected String[] getCommonInterceptorNames() {if (commonInterceptorNames == null) {commonInterceptorNames = new String[] { "permissionInterceptor" };}return commonInterceptorNames;}@SuppressWarnings("unchecked")@Overrideprotected Class<? extends Annotation>[] getMethodAnnotations() {if (methodAnnotations == null) {methodAnnotations = new Class[] { Permission.class };}return methodAnnotations;}@Overrideprotected 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 {//...@Overridepublic 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和userTypeUserEntity 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实体@Overridepublic UserEntity getUser(@PathVariable(value = "token") String token) {// 当前端登录后,它希望送token到后端,查询出用户信息(并以此调用authorize接口做权限验证,permission-aop已经实现,使用者并不需要关心)// 需要和单点登录系统,例如OAuth或者JWT等系统做对接// 示例描述token为abcd1234对应的用户为lisiLOG.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
,调用远程服务,判断是否授权。会判断缓存中是否存在。
// 通过自动装配的方式,自身调用自身的注解方法@Autowiredprivate 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
,提供具体的实现。
// 权限验证@Overridepublic 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
,失败进行重试。
@Overridepublic 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 {@Overridepublic 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 {@Beanpublic PermissionResource permissionResource() {return new PermissionResourceImpl();}@Beanpublic 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 {@Beanpublic PermissionFeignInterceptor permissionFeignInterceptor() {return new PermissionFeignInterceptor();}
}
PermissionFeignInterceptor
,如果请求头上有user-id
,user-type
,token
,调用feign的时候复制一份。
public class PermissionFeignInterceptor implements RequestInterceptor {@Overridepublic 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的使用,可以适配任意的注册中心。