# share **Repository Path**: Linwenfeng/share ## Basic Information - **Project Name**: share - **Description**: 本项目是基于SpringCloud微服务架构实现的微服务开发平台,为开发人员集成了快速构建管理系统的一系列基础组件。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2020-08-14 - **Last Updated**: 2024-05-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: SpringCloud, RBAC, 微服务 ## README # share ## 概述 本项目是采用RBAC模型的用户权限设计方案并使用SpringCloud体系技术实现的通用简易微服务基础开发平台,提供了权限管理、组织架构、用户管理、字典管理等基础服务,让开发者更专注于业务服务的开发。 主要为开发者提供了以下便捷: - 权限管理、组织架构、用户管理、字典管理 - 授权码模式统一的登录(基于oauth2协议) - 第三平台的登录(基于AuthenticationManager通过clientId来选择AuthenticationProvider授权登录) - 可拓展性的数据权限(基于mybait的StatementHandler对象进行动态修改sql) - 微服务间操作用户信息传递(基于feign的RequestInterceptor) - 系统字典服务与前端翻译(基于sessionStorage缓存,一次session只加载一次字典数据) - 网关服务(基于spring cloud的gateway组件,通过GlobalFilter进行路由权限校验) - 可插拔的公共基础组件整合包装(基于spring boot的SPI机制) ## 项目集成框架技术 SpringCloud2+feign+Gateway+sentinel+Oauth2+Mybatis-plus+Redis+RabbitMq+MySql+Nacos+FastDFS | 组件名 | 版本号 | | :-----| ----: | | Spring Cloud | Hoxton.SR11 | | Spring Cloud Alibaba | 2.2.1.RELEASE | | Spring Boot | 2.3.10.RELEASE | | Mybatis Plus | 3.4.2 | | JDK | 1.8 | | MySql | 8.0 | | Nacos | 1.1.4 | | Redis | 6.2.2 | ## 权限设计说明 - 操作权限 : 即可以浏览哪些页面与对数据进行哪些处理,如查看,新增,修改,删除,审核等,用户点击删除按钮时,后台会校验用户角色下的所有权限是否包含该删除权限,如果是,就可以进行下一步操作,反之提示无权限。 - 数据权限 : 数据权限就是用户在同一页面看到的数据是不同的,比如财务部只能看到其部门下的用户数据 ### 示例图 ![avatar](./organization.png) ![avatar](./add_permission.png) ![avatar](./permission.png) ### 操作(路由)权限 - 页面|按钮 - 只读|读写 - 白名单路由|权限路由 ### 数据权限 - 归属数据 : 向所属机构或上级公开 - 公开数据 : 向所有用户公开 - 隐私数据 : 独享数据 ### RBAC权限模型 ![avatar](./power.jpg) 用户-->用户组(职位)-->角色-->权限 ### 网关权限校验设计实现 - 网关启动后通过ScheduledThreadPoolExecutor去加载系统的配置权限 - 白名单路由(即无需登录可随意访问的路由) - 权限路由(即只需登录就可以的路由) - 特定权限路由(即需要登录又需要有权限的路由) - 实现GlobalFilter.filter()对所有请求路由拦截校验 #### 关键代码示例如下 [点击查看详情](https://gitee.com/Linwenfeng/share/blob/master/gateway-service/src/main/java/com/black/gateway/filter/AuthenticationFilter.java "最好的markdown教程")。 ### 数据权限设计实现 - 使用Mybaits的插件机制在beforePrepare前动态添加上权限sql语句 - 在需要有数据权限的Service方法上添加上@DataScope() ####关键代码示例如下 ``` /** * mbatis-plus 3.5.1以后的写法 */ @Slf4j public class DataScopeInterceptor implements InnerInterceptor { @Autowired DataScopeService dataScopeService; @Override public void beforePrepare(StatementHandler statementHandler, Connection connection, Integer transactionTimeout) { MetaObject metaObject = SystemMetaObject.forObject(statementHandler); // 先判断是不是SELECT操作 不是直接过滤 MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) { return; } BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); // 执行的SQL语句 String powerSql = boundSql.getSql(); try { powerSql = dataScopeService.handler(boundSql.getSql(), mappedStatement.getId()); } catch (Exception exception) { log.error("数据权限处理异常",exception); } metaObject.setValue("delegate.boundSql.sql", powerSql); } } @Configuration @AutoConfigureBefore({MybatisPlusConfig.class}) @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class MyDataScopeConfig { private final DataScopeHandlerInterceptorRegistry scopeHandlerInterceptorRegistry; @Bean public MyDataScopeSqlHandler1 myDataScopeSqlHandler1() { return new MyDataScopeSqlHandler1(); } @Bean public MyDataScopeSqlHandler2 myDataScopeSqlHandler2() { return new MyDataScopeSqlHandler2(); } @PostConstruct public void registryDataScopeSqlHandler() { scopeHandlerInterceptorRegistry.addInterceptor(myDataScopeSqlHandler1()); scopeHandlerInterceptorRegistry.addInterceptor(myDataScopeSqlHandler2()); } } /** * 权限sql处理器接口 * 系统会根据类型匹配权限sql处理器,再调用getPowerSql(DataPower dataPower)获取权限sql * @author linwenfeng */ public interface DataScopeSqlHandler { /** * 处理类型 * @return r */ String getType(); /** * 获取权限范围 * @param dataScope 权限注解 * @return r */ List getDataPowerCode(DataScope dataScope); /** * 获取权限sql * @param dataScope 权限注解 * @return 权限sql */ String getScopeSql(DataScope dataScope); } /** * 数据权限注解 * @author linwenfeng */ @Documented @Target(value = {ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DataScope { /** * 创建人表对应字段 **/ String creatorField() default "creator"; /** * 表别名 **/ String tableAlias() default ""; /** * 数据归属字段 **/ String powerField() default "comCode"; /** * 处理器类型 **/ String handlerType() default "default"; String scopeType() default ""; /** * 拦截处理方法 * 该注解定义在类上面时用到 * 如果为空表示对所有方法进行拦截处理 * 不为空只拦截定义了的方法 * @return String [] */ String[] interceptMethods() default {""}; } ``` ### 第三方平台登录的设计与实现 - 自定义AuthenticationManager - 通过clientId来选择其对应的AuthenticationProvider来处理登录请求 - 微信小程序登录示例 #### 关键代码示例如下 ``` public class ShareAuthenticationManager implements AuthenticationManager { private final Map specialProviderMap = Maps.newHashMap(); private final AuthenticationProvider defaultAuthenticationProvider; private final static String CLIENT_ID = "client_id"; public ShareAuthenticationManager(AuthenticationProvider defaultAuthenticationProvider) { this.defaultAuthenticationProvider = defaultAuthenticationProvider; } @Override @SuppressWarnings("unchecked") public Authentication authenticate(Authentication authentication) throws AuthenticationException { Object details = authentication.getDetails(); String clientId = null; if (details != null) { if (details instanceof Map) { Map map = (Map) details; clientId = map.get(CLIENT_ID); } else { clientId = details.toString(); } } AuthenticationProvider authenticationProvider = null; //根据不同的client_id调用不同的处理 if (clientId != null) { authenticationProvider = specialProviderMap.get(clientId); } if (authenticationProvider == null) { authenticationProvider = this.defaultAuthenticationProvider; } return authenticationProvider.authenticate(authentication); } public void putSpecialProvider(String clientId, AuthenticationProvider provider) { this.specialProviderMap.put(clientId, provider); } } /** * 微信小程序登录处理,把js_code作为用户名调用微信接口查询openid,查询到则登录成功 * * @author linwenfeng */ public class WechatAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private final UserDetailsService userDetailsService; public WechatAuthenticationProvider(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // 微信小程序用户不进行密码校验 } /** * 这里的用户名是js_code */ @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { try { return this.userDetailsService.loadUserByUsername(username); } catch (UsernameNotFoundException ex) { throw new BadCredentialsException(ex.getMessage()); } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } } } ``` ### 可插拔的公共基础组件 #### 设计与实现 - 使用了SPI技术实现自动注入(common/main/resources/META-INF/spring.factories) - 使用@ConditionalOnClass判断有依赖到相关jar时才会注入配置Bean - 当用不到某组件时,可在相应的pom.xml中exclusion掉相关依赖