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

项目介绍

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;

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

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

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Nepxion Discovery【探索】使用指南,基于Spring Cloud Greenwich版、Finchley版和Hoxton版而 制作,对于Edgware版,使用者需要自行修改。使用指南主要涉及的功能包括: 基于Header传递的全链路灰度路由,网关为路由触发点。采用配置中心配置路由规则映射在网 关过滤器中植入Header信息而实现,路由规则传递到全链路服务中。路由方式主要包括版本和 区域的匹配路由、版本和区域的权重路由、基于机器IP地址和端口的路由 基于规则订阅的全链路灰度发布。采用配置中心配置灰度规则映射在全链路服务而实现,所有 服务都订阅某个共享配置。发布方式主要包括版本和区域的匹配发布、版本和区域的权重发布 全链路服务隔离。包括注册隔离、消费端隔离和提供端服务隔离,示例仅提供基于Group隔 离。除此之外,不在本文介绍内的,还包括: 注册隔离:黑/白名单的IP地址的注册隔离、最大注册数限制的注册隔离 消费端隔离:黑/白名单的IP地址的消费端隔离 全链路服务限流熔断降级权限,集成阿里巴巴Sentinel,有机整合灰度路由,扩展LimitApp的 机制,通过动态的Http Header方式实现组合式防护机制,包括基于服务名、基于灰度组、基于 灰度版本、基于灰度区域、基于机器地址和端口等防护机制,支持自定义任意的业务参数组合 实现该功能。支持原生的流控规则、降级规则、授权规则、系统规则、热点参数流控规则 全链路灰度调用链。包括Header方式和日志方式,Header方式框架内部集成,日志方式通过 MDC输出(需使用者自行集成) 同城双活多机房切换支持。它包含在“基于Header传递的全链路灰度路由”里 数据库灰度发布。内置简单的数据库灰度发布策略,它不在本文的介绍范围内 灰度路由和发布的自动化测试 license Apache 2.0 maven central v5.4.0 javadoc 5.4.0 build passing Docker容器化和Kubernetes平台的无缝支持部署
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值