# qhy **Repository Path**: DaHuYuXiXi/qhy ## Basic Information - **Project Name**: qhy - **Description**: 奇幻鱼--小方象2.0版本 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: https://7fish.fun/qhy/ - **GVP Project**: No ## Statistics - **Stars**: 5 - **Forks**: 0 - **Created**: 2023-02-09 - **Last Updated**: 2024-05-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 奇幻鱼项目 ![img.png](项目log.png) 本项目为作者个人所接的外包项目,项目目前已经写完百分之80%,后续会持续完善查询模块和定时任务模块,现将后台源码进行开源。 项目主页访问链接如下: https://7fish.fun/qhy/ **项目后台界面涉及隐私不公开地址** **** # 项目架构 ![](系统架构图.png) **** # 业务核心需求 ![](需求图.jpg) **** # 项目核心难点 ## 认证中心 认证中心采用RABC权限模型进行设计,并没有使用OAuth2协议进行认证处理(后期考虑重构为通用的OAuth2权限认证服务器)。 ![img_1.png](RABC模型图.png) 认证中心本身采用spring security来进行权限访问控制,登录认证主要利用下图中提供的两个Filter完成: ![img_2.png](认证中心工作原理图.png) 至于授权如何完成,我们需要先来了解一下Spring Security的两种粒度的授权管理策略。 **** ## spring security权限管理策略介绍 从技术角度来看,Spring Security中提供的权限管理功能主要分为两类: - 基于过滤器的权限管理(FilterSecurityInterceptor) - 基于AOP的权限管理(MethodSecurityInterceptor) 基于过滤器的权限管理主要用来拦截HTTP请求,拦截下来之后,根据HTTP请求地址进行权限校验。 基于AOP的权限管理主要用来处理方法级别的权限问题,当需要调用某一个方法时,通过AOP将操作拦截下来,然后判断当前用户是否具备相关的权限,如果具备则允许当前方法的调用,否则禁止方法调用。 **在本项目中,认证中心模块主要使用FilterSecurityInterceptor拦截HTTP请求,进行权限校验。** **自研的权限插件主要使用MethodSecurityInterceptor在方法粒度进行权限校验。** **** ### 认证中心模块如何利用FilterSecurityInterceptor完成权限管理 SpringSecurity大体工作流程图如下: ![](springSecurity工作流程图.png) FilterSecurityInterceptor工作流程: ![](FilterSecurityInterceptor工作流程图.png) FilterInvocationSecurityMetadataSource有个默认子类实现DefaultFilterInvocationSecurityMetadataSource,其内部保存了我们通过如下形式配置的请求路径以及访问权限之间的映射关系: ```java //不需要管理员身份认证的请求--认证中心暴露给外界微服务调用的接口 http.authorizeRequests().antMatchers("/authentication/**").permitAll(); //管理员请求只能由超级管理员才能操作--普通管理员没有操作认证中心的权限 http.authorizeRequests().antMatchers("/manager/**","/operationLog/**").hasAnyRole(ManagerConstants.SUPER_MANAGER); //其他的操作普通管理员或者超级管理员才可以 http.authorizeRequests().antMatchers("/**").hasAnyRole(ManagerConstants.COMMON_MANAGER,ManagerConstants.SUPER_MANAGER); ``` ```java public class DefaultFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { //具体映射关系保存在下面这个map集合中 private final Map> requestMap; .... ``` 我们通过WebSecurityConfigurerAdapter配置的资源请求路径和权限之间的映射关系,会经过DefaultFilterInvocationSecurityMetadataSource的子类ExpressionBasedFilterInvocationSecurityMetadataSource处理后,将映射关系保存到requestMap集合中。 **我们自己也可以将URL地址以及访问它所需要的权限保存在数据库中,以此来实现动态权限校验,具体可以通过自定义实现FilterInvocationSecurityMetadataSource接口,重写里面的getAttribute方法,方法中根据当前请求URL查询数据库,获取访问当前URL需要的权限,然后将查询结果封装为ConfigAttribute返回即可。** 以上就是认证中心借助FilterSecurityInterceptor在http请求粒度上实现授权校验的大致原理流程了。 **** ## 通用权限插件如何设计 通用权限插件主要负责完成两方面的功能: - 登录认证 - 方法粒度授权管理(请求粒度也可以,参考上一个小节) 我们先来看看如何基于MethodSecurityInterceptor完成方法粒度的授权管理。 ### 如何利用MethodSecurityInterceptor完成方法粒度的权限管理 MethodSecurityInterceptor工作流程: ![](MethodSecurityInterceptor实现权限管理流程图.png) 我们需要使用@EnableGlobalMethodSecurity注解开启基于方法粒度权限管理的功能,该注解主要帮我们向容器中导入如下配置类: ![](@EnableGlobalMethodSecurity导入的配置类.png) **** ### 方法粒度授权管理的开发思路 ![](权限插件设计思路图.png) advisor增强器通常类型都为PointcutAdvisor,即由Pointcut和Advice两部分组成。pointcut的classFilter和MethodMatcher方法决定是否对某个对象进行代理,并在代理对象方法执行时,由MethodMatcher方法决定是否拦截当前方法的执行。 ![](MethodSecurityMetadataSourcePointcut.png) 我们可以通过SecurityMetaDataSource来控制MethodSecurityMetadataSourceAdvisor是否代理某个对象,切入对象的哪个方法。 本项目中通过自定义的CustomMethodSecurityMetadataSource来解析方法获取调用方法需要的权限: ```java /** * 方法上存在@RequestMapping注解,此时将方法名视为权限名
* 注意: 不要把swagger的controller控制器也拦截了
* 该方法在初始化bean作为pointcut进行过滤,在方法执行时,调用该方法获取方法权限 */ @Override public Collection getAttributes(Method method, Class targetClass) { if (AnnotationUtils.findAnnotation(method, RequestMapping.class) != null && AnnotationUtils.findAnnotation(targetClass, Controller.class) != null && AnnotationUtils.findAnnotation(method, ApiOperation.class) != null //控制器类上添加了该注解,则不进行拦截 && AnnotationUtils.findAnnotation(targetClass, IgnoreController.class) == null) { return List.of(new CustomConfigAttribute(method.getName())); } return null; } ``` 通过自定义GlobalMethodSecurityConfiguration来替换默认的自动配置类,主要是将我们的CustomMethodSecurityMetadataSource和CustomVoter添加进去: ```java /** * 跳过对GlobalMethodSecurityConfiguration的默认自动注入,我们这里需要重写customMethodSecurityMetadataSource方法
* 注册一个单独的MethodSecurityMetadataSource,注解加载继承至GlobalMethodSecurityConfiguration的配置上,就会跳过对
* GlobalMethodSecurityConfiguration的自动配置,源码在GlobalMethodSecuritySelector中
* 灵感来源: 深入浅出spring security第13章 * @author 大忽悠 * @create 2023/2/14 18:47 */ @EnableGlobalMethodSecurity @Configuration public class CustomGlobalMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration { @Override protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() { return new CustomMethodSecurityMetadataSource(); } @SneakyThrows @Override protected AccessDecisionManager accessDecisionManager() { AccessDecisionManager accessDecisionManager = super.accessDecisionManager(); Field decisionVoters = AbstractAccessDecisionManager.class.getDeclaredField("decisionVoters"); decisionVoters.setAccessible(true); List> accessDecisionVoters = (List>) decisionVoters.get(accessDecisionManager); accessDecisionVoters.add(new CustomVoter()); return accessDecisionManager; } } ``` **** ### 通用登录认证设计思路 - TokenLoginAuthFilter负责拦截/login/**开头的登录请求,具体采用何种登录认证方式由子类决定,默认提供的UsernamePasswordLoginAuthFilter是基于用户名密码进行登录的。 - AbstractTokenAuthenticationFilter负责拦截剩余所有请求,除了/noTokenAuth/**开头的请求,从请求头中取出token,进行校验。 - 校验过程在verifyToken方法中实现,需要子类覆写,默认为RpcTokenAuthenticationFilter覆写此方法,通过用户注入容器的userDetailsService获取当前登录用户信息,通过AuthClient向认证中心发送请求获取当前用户具备的权限信息。 **** ## 项目中涉及到的分布式事务处理 本项目中主要有两处涉及到分布式事务处理,分别如下图所示: - 需要强一致性或者弱一致性的分布式事务: 主要使用seata的at模式解决。具体主要涉及新增用户信息同步保存到认证中心 ![img.png](seata处理分布式事务.png) - 最终一致性分布式事务: 课程上传接口(具体大家可以参考课程微服务课程上传接口源码实现过程) ![](课程上传接口.png) **如果课程接口已经异步化了,那么缓存也就没啥必要进行异步更新了,课程上传接口的耗时在于课程文件通常比较大,上传比较耗时。** **如果上传任务量很大,上面给出的实现方案是存在压垮线程池的可能的,因此优化思路将文件进行分片,然后采用多线程并行上传,配合countDownLatch使用。** **** ## 如何魔改SpringMVC开发出自己的一套本地文件管理系统 ### 为什么中文资源请求404? 文件服务最初基于S3协议,配合MimIO使用,但是由于甲方上传的是一个课程压缩包,里面包含课程html文件,课程图片(许多个),许多css和js文件,并且html文件中采用相对路径引用相关资源。如果要使用minio,那么需要进行大量路径替换操作,所以最终我选择采用springmvc提供的静态资源能力开发出一个简单的本地静态资源文件管理系统。 springMvc默认使用SimpleUrlHandlerMapping作为处理器静态资源请求映射的HandlerMapping,使用的Handler为ResourceHttpRequestHandler,对应的HandlerAdaptor为HttpRequestHandlerAdapter。 默认提供的这条静态资源请求处理一条龙服务没啥毛病,但是存在对中文资源请求路径先解码后编码的问题,导致最终明明文件存在,但是却报404,你说气不气人! ![img.png](AbtsractUrlHandlerMapping第一次对路径进行解密.png) ![](解码.png) ![img.png](默认解码采用的字符集.png) 第二次路径编码位置: ![img.png](第二次路径解码.png) ![](编码核心源码位置.png) 对已经解码过的请求路径再进行编码,不就又变成乱码了吗? 当然,这里主要针对中文字符存在这个问题,英文字符没有这个问题。 **** ### 如何魔改? 我们只需要禁止PathResourceResolver对资源路径进行编码即可,核心思路就是自定义一个CustomPathResourceResolver,重新getResource方法,替换默认的实现: ```java public class CustomPathResourceResolver extends PathResourceResolver { @Override protected Resource getResource(String resourcePath, Resource location) throws IOException { Resource resource = new FileSystemResource(location.getFile().getAbsolutePath()+ File.separator+resourcePath); if (resource.isReadable()) { return resource; } return null; } } ``` 自定义一个CustomFileResourceHttpRequestHandler替换默认的实现: ```java public class CustomFileResourceHttpRequestHandler extends ResourceHttpRequestHandler { @Override public void afterPropertiesSet() throws Exception { //将我们自己的资源解析器设置进去 setResourceResolvers(List.of(new CustomPathResourceResolver())); super.afterPropertiesSet(); setUrlPathHelper(null); } } ``` 自定义一个CustomFileHandlerMapping替换默认的实现: ```java /** * 由于SimpleUrlHandlerMapping返回的ResourceHttpRequestHandler存在对中文字符重复解码的问题,所以这里自定义 * @author 大忽悠 * @create 2023/2/28 10:15 */ public class CustomFileHandlerMapping extends AbstractUrlHandlerMapping implements ApplicationContextAware{ private final Map urlMap = new LinkedHashMap<>(); private final CustomFileResourceHttpRequestHandler fileResourceHttpRequestHandler; public CustomFileHandlerMapping(CustomFileResourceHttpRequestHandler resourceHttpRequestHandler) { this.fileResourceHttpRequestHandler=resourceHttpRequestHandler; //添加文件映射路径 urlMap.put(CourseFileConstants.STATIC_REQUEST_PREFIX_PATTERN,fileResourceHttpRequestHandler); } @Override public void initApplicationContext() throws BeansException { super.initApplicationContext(); registerHandlers(this.urlMap); } //该方法实现直接copy SimpleURLHandlerMapping即可 protected void registerHandlers(Map urlMap) throws BeansException { ... } @Override public int getOrder() { return 1; } } ``` **** # 注意 本项目使用了自研的Easy-Generator轻量级代码生成器进行各个微服务从建表到模板工程代码文件的生成,包括使用到了轻量级代码生成器中提供的通用基础设施,如: 统一返回值处理,全局异常异常,返回值增强器,swagger配置,缓存模块自动配置等... 因此启动本项目需要拉取Easy-Generator项目并安装到本地才可以运行。 Easy-Generator项目仓库链接如下: https://gitee.com/DaHuYuXiXi/easy-code **** # 待解决需求 - openFeign降级处理 - openFeign内部调用,通过拦截器添加请求头,进行内部调用合法性验证,防止内部sdk接口被外部非法访问 - 日志管理--后台需要收集日志服务,并进行处理后,展示给后台管理员检查 - redis通用缓存模块实现 - 通用es查询模块实现 - 定时任务模块实现 - 认证中心存储权限应该为类名+方法名,防止因为不同类中方法名重复造成权限授权错误。同时CustomConfigAttribute中存储的也应该是类名+方法名 - 课程限时秒杀需求,借助悲观锁(redis分布式锁)或者乐观锁实现