项目介绍
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的使用,可以适配任意的注册中心。 

![实现图形算法API[软光栅渲染器,C++]](https://img-blog.csdnimg.cn/aa1dcb9356854634bf796a5436e8f7cc.png)


















