【开源项目】权限框架Nepxion Permission原理解析

news/2025/2/21 8:22:12/

项目介绍

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注入了PermissionAutoScanProxyPermissionInterceptorPermissionAuthorizationPermissionPersisterPermissionFeignBeanFactoryPostProcessor

@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注入了PermissionResourceUserResource

@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-iduser-typetoken,调用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的使用,可以适配任意的注册中心。

在这里插入图片描述


http://www.ppmy.cn/news/77220.html

相关文章

Web安全常见攻击

前言 本篇主要简单介绍在 Web 领域几种常见的攻击手段。 1. Cross Site Script&#xff08;XSS跨站脚本攻击) 首先插播一句&#xff0c;为毛叫 XSS&#xff0c;缩写明显是 CSS 啊&#xff1f;没错&#xff0c;为了防止与我们熟悉的 CSS&#xff08;Cascading Style Sheets&am…

vue+element纯手工完美模拟实现小米有品网站

一、预览 小米有品官网&#xff1a;小米有品 本作品demo预览地址&#xff1a;点击预览 二、效果图对比 1.官方效果截图&#xff1a; 2.作者实现的demo效果图&#xff1a; 首页&#xff1a; 上新精选&#xff1a; 商品详情&#xff1a; 购物车&#xff1a; 登录&#xff1a; …

Linux——IO之系统接口+文件描述符详解

IO 文件再次理解系统接口文件操作理解文件描述符 fd 文件再次理解 文件 文件内容 文件属性 其中文件属性也是数据–>即便你创建一个空文件&#xff0c;其也是要占据磁盘攻坚的。 文件操作 文件内容的操作 文件属性的操作 有可能在操作文件的过程中即改变文件的内容&…

【华为OD机试真题2023B卷 JAVA】阿里巴巴找黄金宝箱(II)

华为OD2023(B卷)机试题库全覆盖,刷题指南点这里 阿里巴巴找黄金宝箱(II) 知识点数组哈希表优先级队列 时间限制:1s 空间限制:256MB 限定语言:不限 题目描述: 一贫如洗的樵夫阿里巴巴在去砍柴的路上,无意中发现了强盗集团的藏宝地,藏宝地有编号从0~N的箱子,每个箱子上…

ZeLinAI是什么?国产ChatGPT快速搭建自己的AI应用

ChatGPT使用门槛高&#xff0c;需要科学上网短信接码等&#xff0c;不如直接选择国产ZelinAI&#xff0c;使用超简单轻轻松松从0到1零代码创建自己的AI应用。目前模型仅支持GPT-3.5-turbo&#xff0c;后续应该会接入文心一言、GPT-4、GPT-4.5和Bard&#xff0c;新手站长分享国产…

RAM Sequential

前段时间&#xff0c;在微信公众号上偶然看到一篇很不错的技术分享文章&#xff1a;《南湖处理器DFT设计范例》。文中详细介绍了中科院计算所的RISC-V处理器实施的DFT设计。 去年&#xff0c;也基于一款处理器应用过Share Test Bus技术&#xff0c;但在memory界面fault测试的问…

绝世内功秘籍《调试技巧》

本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 内容专栏&#xff1a;这里是《C知识系统分享》专栏&#xff0c;笔者用重金(时间和精力)打造&#xff0c;基础知识一网打尽&#xff0c;希望可以帮到读者们哦。 内…

vulnhub靶场之bassamctf

1.信息收集 探测存活主机&#xff0c;输入&#xff1a;netdiscover -r 192.168.239.0/24 &#xff0c;发现192.168.239.177存活。 对目标主机192.168.239.176进行端口扫描&#xff0c;发现存活22(SSH)、80端口。 在浏览器上输入&#xff1a;http://192.168.239.177&#xff…