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

news/2024/7/10 21:07:27 标签: 开源, spring, java

项目介绍

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

java">@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

java">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信息获取用户数据。

java">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获取用户的接口。

java">@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,提供了获取用户信息的真正接口。

java">@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,检查用户类型白名单。

java">    private boolean checkUserTypeFilters(String userType) {
        if (StringUtils.isEmpty(whitelist)) {
            return true;
        }

        if (whitelist.toLowerCase().indexOf(userType.toLowerCase()) > -1) {
            return true;
        }

        return false;
    }

用户认证

PermissionAuthorization#authorize,调用远程服务,判断是否授权。会判断缓存中是否存在。

java">    // 通过自动装配的方式,自身调用自身的注解方法
    @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提供了授权方法。

java">@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,提供具体的实现。

java">    // 权限验证
    @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,失败进行重试。

java">    @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后置处理器。

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

java">@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

java">@Configuration
public class PermissionFeignConfiguration {
    @Bean
    public PermissionFeignInterceptor permissionFeignInterceptor() {
        return new PermissionFeignInterceptor();
    }
}

PermissionFeignInterceptor,如果请求头上有user-iduser-typetoken,调用feign的时候复制一份。

java">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

java">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的方法。

java">@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,作为参考。

java">@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,判断配置文件的属性决定日志打印。

java">    @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.niftyadmin.cn/n/344554.html

相关文章

由浅入深Netty源码分析

目录 1 启动剖析2 NioEventLoop 剖析3 accept 剖析4 read 剖析 1 启动剖析 我们就来看看 netty 中对下面的代码是怎样进行处理的 //1 netty 中使用 NioEventLoopGroup &#xff08;简称 nio boss 线程&#xff09;来封装线程和 selector Selector selector Selector.open();…

FPGA System Planner(FSP)使用手册

FSP工具是cadence公司为了FPGA/PCB协同设计而推出的一个解决方案工具包。它的主要工作是由软件来自动生成、优化FPGA芯片的管脚分配,提高FPGA/PCB设计的工作效率和连通性。FSP完成两顷重要工作:一、可以自动生成FPGA芯片的原理图符号(symbol);二、自动生成、优化和更改FPG…

Eclipse将代码收缩if/for/try,支持自定义区域收缩

Hi, I’m Shendi Eclipse将代码收缩if/for/try&#xff0c;支持自定义区域收缩 最近忙于给网站增加功能&#xff0c;在使用 Eclipse 编写 Java 代码时发现一个函数内代码过多&#xff0c;并且 if&#xff0c;for&#xff0c;try这种代码块无法收缩&#xff08;在IDEA&#xff0…

【计算机网络复习】第五章 数据链路层 1

数据链路层的作用 两台主机&#xff08;端到端&#xff09;之间的通信是依靠相邻的主机/路由器之间的逐步数据传送来实现 数据链路层实现相邻主机/路由器间的可靠的数据传输 网络层&#xff1a;主机-主机通信 数据链路层&#xff1a;点到点通信 数据链路层的主要功能 u 链路…

非暴力沟通模型

非暴力沟通模型 非暴力沟通的创始人是马歇尔.卢森堡&#xff0c;师从人本主义心理学之父卡尔.罗杰斯。《非暴力沟通》一书入选香港大学推荐的50本必读书籍之列。 模型介绍 非暴力沟通&#xff08;英文名称&#xff1a;NonviolentCommunication&#xff0c;简称NVC&#xff09;…

ChatGPT得到Kubernetes一些概念

1、什么是kubernetes Kubernetes是一个开源的容器编排平台&#xff0c;它通过跨主机集群的方式来管理容器化应用程序。Kubernetes旨在简化容器的部署、扩展和管理&#xff0c;并提供自动化的操作&#xff08;如负载均衡、自我修复、自动缩放等&#xff09;&#xff0c;以确保应…

【C++】 设计模式(单例模式、工厂模式)

文章目录 设计模式概念单例模式懒汉式方法一方法二总结 饿汉式单例模式的优点 工厂模式概念简单工厂工厂方法抽象工厂三种工厂方法的总结 设计模式 概念 设计模式是由先人总结的一些经验规则&#xff0c;被我们反复使用后、被多数人知晓认可的、然后经过分类编排&#xff0c;…

论文阅读_语音合成_VALLE-X

论文信息 name_en: Speak Foreign Languages with Your Own Voice: Cross-Lingual Neural Codec Language Modeling name_ch: 用你自己的声音说外语&#xff1a;跨语言神经编解码器语言建模 paper_addr: http://arxiv.org/abs/2303.03926 date_read: 2023-04-25 date_publish:…