commit c0c8734e140e916e276b2f5a3f863665c039093a Author: wangxiang <1827945911@qq.com> Date: Sat Sep 16 16:20:12 2023 +0800 chore: withdrawal tool diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e25c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +### gradle ### +.gradle +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.settings/ +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +rebel.xml + +### NetBeans ### +nbproject/private/ +nbbuild/ +nbdist/ +.nb-gradle/ + +### maven ### +target/ +*.war +*.ear +*.zip +*.tar +*.tar.gz +*.versionsBackup + +### vscode ### +.vscode + +### logs ### +/logs/ +*.log + +### temp ignore ### +*.cache +*.diff +*.patch +*.tmp +*.java~ +*.properties~ +*.xml~ + +### system ignore ### +.DS_Store +Thumbs.db +Servers +.metadata + +# ui ignore +kicc-ui/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f611fd --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +

+ +
+
+

+ +

一个快速开发软件的平台

+ +

+ Website • + Documentation +

+ +
+ + + + + + + + + +
+ +## 🐬 介绍 +海豚生态计划-打造一个web端,安卓端,ios端的一个海豚开发平台生态圈,不接收任何商业化,并且完全免费开源(包含高级功能)。 + +## 💪 愿景 +让人人都可以快速高效的开发软件 + +## ✨ 特性 +- 主体框架:采用最新的`Spring Cloud 2021.0.1`, `Spring Boot 2.6.4`, `Spring Cloud Alibaba 2021.1`版本进行系统设计。 +- 统一注册:支持`Nacos`作为注册中心,实现多配置、分群组、分命名空间、多业务模块的注册和发现功能。 +- 统一认证:统一`Oauth2`认证协议,并支持自定义grant_type实现手机号码登录,第三方登录集成JustAuth实现微信、支付宝等多种登录模式。 +- 业务监控:利用`Spring Boot Admin`来监控各个独立微服务运行状态。 +- 内部调用:集成了`Feign`与自定义内部注解,支持内部调用。 +- 业务熔断:采用`Sentinel`实现业务熔断处理,避免服务之间出现雪崩。 +- 在线文档:通过接入`Knife4j`,实现在线API文档的查看与调试。 +- 业务分离:采用前后端分离的框架设计,提高开发效率、降低维护成本、增强系统稳定性和灵活性。 +- 多租户功能:集成`Mybatis Plus`,自定义sql执行拦截器,实现SAAS多租户。 +- 消息中间件:采用`RocketMQ`,实现服务之间消息转发。 +- 分布式事物方案:采用`seata`,实现多个微服务分布式事物一致。 +- 分布式定时器:采用`XXL-JOB`,实现多个微服务分布式任务调度。 +- 微服务网关:采用`Spring Gateway`实现流量配置动态化、API管理和路由、负载均衡和容错、解决跨域问题、鉴权,限流,熔断,防火墙等等。 + +## 黑客节 +加入[Github HackToberFest](https://hacktoberfest.com/) 开始为此项目做出贡献. + +## 🔨 开发目录 + +``` +├─dolphin -- 父项目,各模块分离,方便微服务扩展 +│ ├─doc -- 文档数据-包含项目的一些数据资料 +│ ├─docker-cloud -- docker-compose容器配置 +│ ├─dolphin-auth -- 认证授权中心,基于 spring security oAuth2 +│ ├─dolphin-common -- 公共通用模块,主模块 +│ │ ├─dolphin-common-bom -- 全局jar BOM标准定义 +│ │ ├─dolphin-common-core -- 公共工具类核心包 +│ │ ├─dolphin-common-data -- 数据服务核心包 +│ │ ├─dolphin-common-datasource -- 动态切换数据源组件 +│ │ ├─dolphin-common-feign -- feign-sentinel服务降级熔断、限流组件 +│ │ ├─dolphin-common-job -- 定时任务,基于xxl-job +│ │ ├─dolphin-common-log -- 日志服务 +│ │ ├─dolphin-common-mock -- 单元模拟测试工具类 +│ │ ├─dolphin-common-rocketmq -- 阿里 rocketmq 消息中间件 +│ │ ├─dolphin-common-seata -- 阿里巴巴-seata分布式事务解决方案 +│ │ ├─dolphin-common-security -- 安全工具类 +│ │ ├─dolphin-common-swagger -- 接口文档 +│ │─dolphin-common-demo -- 组件使用案列 +│ │ ├─dolphin-common-demo-mq -- 消息中心间演示 +│ │ ├─dolphin-common-demo-seata -- 分布式事务解决方案演示 +│ │─dolphin-gateway -- 服务网关,基于 spring cloud gateway +│ │─ddolphin-platform -- 微服务平台 +│ │ ├─dolphin-platform-api -- 微服务api调用(添加调用的微服务api依赖库,实现调用) +│ │ │ ├─dolphin-common-api -- 通用业务模块公共api模块 +│ │ │ ├─dolphin-monitor-api -- 运维监控api模块 +│ │ │ ├─dolphin-system-api -- 系统api模块 +│ │ │ ├─dolphin-template-api -- 新建api模块模板,只提供基础依赖 +│ │ ├─dolphin-platform-biz -- 微服务业务模块 +│ │ │ ├─dolphin-common-biz -- 通用业务模块 +│ │ │ ├─dolphin-monitor-biz -- 运维监控业务模块 +│ │ │ ├─dolphin-system-biz -- 系统业务模块 +│ │ │ ├─dolphin-template-biz -- 新建业务模块模板,只提供基础依赖 +│ │─dolphin-register -- 注册配置中心 +│ │─dolphin-visual 可视化图形界面 +│ │ ├─dolphin-rocketmq-dashboard -- RocketMQ可视化监控平台 +│ │ ├─dolphin-sentinel-dashboard -- 哨兵流量控制可视化平台 +│ │ ├─dolphin-spring-dashboard -- SpringBoot可视化监控平台 +│ │ ├─dolphin-xxl-job-admin -- XXL-JOB可视化监控平台 +``` + +## 🤔 一起讨论 +加入我们的 [Discord](https://discord.gg/DREuQWrRYQ) 开始与大家交流。 + +## 🤗 我想成为开发团队的一员! +欢迎😀!我们正在寻找有才华的开发者加入我们,让海豚开发平台变得更好!如果您想加入开发团队,请联系我们,非常欢迎您加入我们!💖 + +## 在线一键设置 +您可以使用 Gitpod,一个在线 IDE(开源免费)来在线贡献或运行示例。 + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/wangxiang4/dolphin) + +## 📄 执照 +[Dolphin Development Platform 是获得MIT许可](https://github.com/wangxiang4/dolphin/blob/master/LICENSE) 的开源软件 。 + + diff --git a/kicc-common-bom/pom.xml b/kicc-common-bom/pom.xml new file mode 100644 index 0000000..ef51902 --- /dev/null +++ b/kicc-common-bom/pom.xml @@ -0,0 +1,276 @@ + + + 4.0.0 + + com.cloud + kicc-common-bom + 1.0.0 + pom + + kicc-common-bom + 全局jar BOM标准定义(可以设置工程内部的jar的标准,也可以设置第三方依赖jar的标准) + + + UTF-8 + 1.8 + 1.8 + ${project.version} + 2.6.3 + 2.1.8.RELEASE + 2.17.1 + 1.2.78 + 1.5.24 + 3.5.1 + 1.2.0 + 4.9.9 + 0.0.29 + 2.2.1 + 1.0.3 + 1.1.1 + 2.3 + 6.4.2 + 4.22.0 + 2.2.9 + + + + + + + com.cloud + kicc-common-core + ${kicc.common.version} + + + com.cloud + kicc-common-data + ${kicc.common.version} + + + com.cloud + kicc-common-datasource + ${kicc.common.version} + + + com.cloud + kicc-common-feign + ${kicc.common.version} + + + com.cloud + kicc-common-rocketmq + ${kicc.common.version} + + + com.cloud + kicc-common-seata + ${kicc.common.version} + + + com.cloud + kicc-common-job + ${kicc.common.version} + + + com.cloud + kicc-common-log + ${kicc.common.version} + + + com.cloud + kicc-common-mock + ${kicc.common.version} + + + com.cloud + kicc-common-security + ${kicc.common.version} + + + com.cloud + kicc-common-swagger + ${kicc.common.version} + + + com.cloud + kicc-common-api + ${kicc.common.version} + + + com.cloud + kicc-system-api + ${kicc.common.version} + + + com.cloud + kicc-monitor-api + ${kicc.common.version} + + + com.cloud + kicc-report-api + ${kicc.common.version} + + + com.cloud + kicc-workflow-api + ${kicc.common.version} + + + com.cloud + kicc-template-api + ${kicc.common.version} + + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + ${security.oauth.version} + + + + io.swagger + swagger-models + ${swagger.core.version} + + + io.swagger + swagger-annotations + ${swagger.core.version} + + + + com.alibaba + fastjson + ${fastjson.version} + + + + cn.javaer.aliyun + aliyun-spring-boot-starter-sms + ${sms.version} + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus.version} + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + + spring-boot-starter-tomcat + org.springframework.boot + + + + + + org.apache.logging.log4j + log4j-bom + ${log4j2.version} + pom + import + + + + com.pig4cloud.plugin + oss-spring-boot-starter + ${oss.version} + + + + com.pig4cloud.plugin + captcha-spring-boot-starter + ${captcha.version} + + + + com.pig4cloud.excel + excel-spring-boot-starter + ${excel.version} + + + + org.flowable + flowable-spring-boot-starter-process-rest + ${flowable.version} + + + + org.flowable + flowable-ui-modeler-rest + ${flowable.version} + + + spring-boot-starter-log4j2 + org.springframework.boot + + + + + + org.liquibase + liquibase-core + ${liquibase.version} + + + com.bstek.ureport + ureport2-console + ${ureport2.version} + + + + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + ${git.commit.plugin} + + + + io.spring.javaformat + spring-javaformat-maven-plugin + ${spring.checkstyle.plugin} + + + + + diff --git a/kicc-common-core/pom.xml b/kicc-common-core/pom.xml new file mode 100644 index 0000000..fd2ecb6 --- /dev/null +++ b/kicc-common-core/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-core + jar + + kicc 公共工具类核心包 + + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + javax.servlet + javax.servlet-api + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-json + + + org.springframework + spring-webmvc + provided + + + + io.swagger + swagger-annotations + + + diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/annotation/EnableKiccJacksonAutoConvert.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/annotation/EnableKiccJacksonAutoConvert.java new file mode 100644 index 0000000..cb009cd --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/annotation/EnableKiccJacksonAutoConvert.java @@ -0,0 +1,22 @@ +package com.cloud.kicc.common.core.annotation; + +import com.cloud.kicc.common.core.config.JacksonAutoConfiguration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + *

+ * 激活 Jackson 自动转换配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import({ JacksonAutoConfiguration.class }) +public @interface EnableKiccJacksonAutoConvert { +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/api/R.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/api/R.java new file mode 100644 index 0000000..a7eb065 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/api/R.java @@ -0,0 +1,114 @@ +package com.cloud.kicc.common.core.api; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + *

+ * 响应信息主体 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class R implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 成功标记 + */ + public static Integer SUCCESS = 200; + /** + * 失败标记 + */ + public static Integer FAIL = 500; + /** + * 未认证 + */ + public static Integer UNAUTH = 401; + + @Getter + @Setter + @ApiModelProperty("状态编码") + private int code; + + @Getter + @Setter + @ApiModelProperty("提示消息") + private String msg; + + @Getter + @Setter + @ApiModelProperty("结果集数量统计") + private long total; + + @Getter + @Setter + @ApiModelProperty("结果集") + private T data; + + public static R ok() { + return restResult(null, SUCCESS, "成功"); + } + + public static R ok(T data) { + return restResult(data, SUCCESS, "成功"); + } + + public static R ok(T data, String msg) { + return restResult(data, SUCCESS, msg); + } + + public static R ok(T data, long total) { + return restData(data, SUCCESS, null, total); + } + + public static R error() { + return restResult(null, FAIL, "失败"); + } + + public static R error(String msg) { + return restResult(null, FAIL, msg); + } + + public static R error(T data) { + return restResult(data, FAIL, null); + } + + public static R error(T data, String msg) { + return restResult(data, FAIL, msg); + } + + public static R unAuth(String msg) { + return restResult(null, UNAUTH, msg); + } + + + private static R restResult(T data, int code, String msg) { + R apiResult = new R<>(); + apiResult.setCode(code); + apiResult.setData(data); + apiResult.setMsg(msg); + return apiResult; + } + + private static R restData(T data, int code, String msg, long total) { + R apiData = new R<>(); + apiData.setCode(code); + apiData.setMsg(msg); + apiData.setTotal(total); + apiData.setData(data); + return apiData; + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/GatewayConfigProperties.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/GatewayConfigProperties.java new file mode 100644 index 0000000..37cf9ce --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/GatewayConfigProperties.java @@ -0,0 +1,33 @@ +package com.cloud.kicc.common.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; + +import java.util.List; + +/** + *

+ * 网关配置文件 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Data +@RefreshScope +@ConfigurationProperties("gateway") +public class GatewayConfigProperties { + + /** + * 网关解密登录前端密码 + */ + private String encodeKey; + + /** + * 网关忽略不需要校验验证码是否合法的客户端 + */ + private List ignoreClients; + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/JacksonAutoConfiguration.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/JacksonAutoConfiguration.java new file mode 100644 index 0000000..3689bc3 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/JacksonAutoConfiguration.java @@ -0,0 +1,46 @@ +package com.cloud.kicc.common.core.config; + +import cn.hutool.core.date.DatePattern; +import com.cloud.kicc.common.core.jackson.KiccJavaTimeModule; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.ZoneId; +import java.util.Locale; +import java.util.TimeZone; + +/** + *

+ * 配置全局JacksonConfig,影响mvc层的对象传输日期格式 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(ObjectMapper.class) +@AutoConfigureBefore(org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class) +public class JacksonAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public Jackson2ObjectMapperBuilderCustomizer customizer() { + return builder -> { + builder.locale(Locale.CHINA); + builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + // 针对于Date类型,文本格式化,已经实现前端返回时间戳 + builder.simpleDateFormat(DatePattern.NORM_DATETIME_PATTERN); + // 解决返回给前端的Long类型数据失去精度,将Long转换为String + builder.serializerByType(Long.class, ToStringSerializer.instance); + // 针对于JDK新时间类,序列化时带有T的问题,自定义格式化字符串 + builder.modules(new KiccJavaTimeModule()); + }; + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/RestTemplateConfiguration.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/RestTemplateConfiguration.java new file mode 100644 index 0000000..bd08383 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/RestTemplateConfiguration.java @@ -0,0 +1,23 @@ +package com.cloud.kicc.common.core.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + *

+ * Rest 配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@Configuration(proxyBeanMethods = false) +public class RestTemplateConfiguration { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/WebMvcConfiguration.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/WebMvcConfiguration.java new file mode 100644 index 0000000..d3658ea --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/config/WebMvcConfiguration.java @@ -0,0 +1,45 @@ +package com.cloud.kicc.common.core.config; + +import cn.hutool.core.date.DatePattern; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.time.format.DateTimeFormatter; + +import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET; + +/** + *

+ * 全局DateTimeFormat + * 针对GET请求传入参数转换 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = SERVLET) +public class WebMvcConfiguration implements WebMvcConfigurer { + + /** + * 增加GET请求参数中时间类型转换 {@link com.cloud.kicc.common.core.jackson.KiccJavaTimeModule} + *
    + *
  • HH:mm:ss -> LocalTime
  • + *
  • yyyy-MM-dd -> LocalDate
  • + *
  • yyyy-MM-dd HH:mm:ss -> LocalDateTime
  • + *
+ * @param registry + */ + @Override + public void addFormatters(FormatterRegistry registry) { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setTimeFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)); + registrar.setDateFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)); + registrar.setDateTimeFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)); + registrar.registerFormatters(registry); + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/AppConstants.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/AppConstants.java new file mode 100644 index 0000000..e4aa082 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/AppConstants.java @@ -0,0 +1,25 @@ +package com.cloud.kicc.common.core.constant; + +/** + *

+ * 应用前缀 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/24 + */ +public interface AppConstants { + + String APP_SYSTEM = "/system"; + + String APP_MONITOR = "/monitor"; + + String APP_COMMON = "/common"; + + String APP_WORKFLOW = "/workflow"; + + String APP_REPORT = "/report"; + + String APP_BIGSCREEN = "/bigscreen"; + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/CacheConstants.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/CacheConstants.java new file mode 100644 index 0000000..7c4b6c0 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/CacheConstants.java @@ -0,0 +1,53 @@ +package com.cloud.kicc.common.core.constant; + +/** + *

+ * 缓存的key 常量 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +public interface CacheConstants { + + /** + * oauth 缓存前缀 + */ + String OAUTH_ACCESS = ServiceNameConstants.AUTH_SERVICE + ":access:"; + + /** + * oauth 缓存令牌前缀 + */ + String OAUTH_TOKEN = ServiceNameConstants.AUTH_SERVICE + ":token:"; + + /** + * 验证码前缀 + */ + String VERIFICATION_CODE = ServiceNameConstants.SYSTEM_SERVICE + ":verification_code:"; + + /** + * oauth 客户端信息缓存 + */ + String OAUTH_CLIENT_DETAILS = ServiceNameConstants.AUTH_SERVICE + ":client_details"; + + /** + * 菜单信息缓存 + */ + String MENU_DETAILS = ServiceNameConstants.AUTH_SERVICE + ":menu_details"; + + /** + * 用户信息缓存 + */ + String USER_DETAILS = ServiceNameConstants.AUTH_SERVICE + ":user_details"; + + /** + * 字典信息缓存 + */ + String DICT_DETAILS = ServiceNameConstants.AUTH_SERVICE + ":dict_details"; + + /** + * 全局配置缓存 + */ + String CONFIG_PARAM = ServiceNameConstants.AUTH_SERVICE + ":config_param"; + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/CommonConstants.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/CommonConstants.java new file mode 100644 index 0000000..a913f03 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/CommonConstants.java @@ -0,0 +1,53 @@ +package com.cloud.kicc.common.core.constant; + +/** + *

+ * 通用常量 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +public interface CommonConstants { + + /** + * 正常 + */ + String STATUS_NORMAL = "0"; + + /** + * 锁定 + */ + String STATUS_LOCK = "9"; + + /** + * 编码 + */ + String UTF8 = "UTF-8"; + + /** + * JSON 资源 + */ + String CONTENT_TYPE = "application/json; charset=utf-8"; + + /** + * 前端工程名 + */ + String FRONT_END_PROJECT = "kicc-ui"; + + /** + * 后端工程名 + */ + String BACK_END_PROJECT = "kicc"; + + /** + * 当前页 + */ + String CURRENT = "current"; + + /** + * size + */ + String SIZE = "size"; + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/RegexConstants.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/RegexConstants.java new file mode 100644 index 0000000..99110cf --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/RegexConstants.java @@ -0,0 +1,18 @@ +package com.cloud.kicc.common.core.constant; + +/** + *

+ * 正则表达式常量 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/4/18 + */ +public interface RegexConstants { + + /** + * 匹配网址正则表达式 + */ + String MATCHER_URL = "(((^https?:(?:\\/\\/)?)(?:[-;:&=\\+\\$,\\w]+@)?[A-Za-z0-9.-]+(?::\\d+)?|(?:www.|[-;:&=\\+\\$,\\w]+@)[A-Za-z0-9.-]+)((?:\\/[\\+~%\\/.\\w-_]*)?\\??(?:[-\\+=&;%@.\\w_{\\s\\S}]*)#?(?:[\\w]*))?)$"; + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/SecurityConstants.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/SecurityConstants.java new file mode 100644 index 0000000..4cf5acb --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/SecurityConstants.java @@ -0,0 +1,125 @@ +package com.cloud.kicc.common.core.constant; + +/** + *

+ * 安全常量 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +public interface SecurityConstants { + + /** + * 角色前缀 + */ + String ROLE = "ROLE_"; + + /** + * 项目的license + */ + String PROJECT_LICENSE = "长沙康来生物有限公司"; + + /** + * 内部接口调用密钥 + */ + String FROM_IN = "kG8qA6qG1aP5aR3g"; + + /** + * 内部接口调用Key标志 + */ + String FROM = "from"; + + /** + * 请求header + */ + String HEADER_FROM_IN = FROM + "=" + FROM_IN; + + /** + * 默认登录URL + */ + String OAUTH_TOKEN_URL = "/oauth/token"; + + /** + * grant_type + */ + String REFRESH_TOKEN = "refresh_token"; + + /** + * 手机号登录 + */ + String APP = "app"; + + /** + * {bcrypt} 加密的特征码 + */ + String BCRYPT = "{bcrypt}"; + + /** + * sys_oauth_client_details 表的字段,不包括client_id、client_secret + */ + String CLIENT_FIELDS = "client_id, CONCAT('{noop}',client_secret) as client_secret, resource_ids, scope, " + + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, " + + "refresh_token_validity, additional_information, autoapprove"; + + /** + * JdbcClientDetailsService 查询语句 + */ + String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS + " from sys_oauth_client_details"; + + /** + * 默认的查询语句 + */ + String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id"; + + /** + * 按条件client_id 查询 + */ + String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?"; + + /*** + * 资源服务器默认bean名称 + */ + String RESOURCE_SERVER_CONFIGURER = "resourceServerConfigurerAdapter"; + + /** + * 用户信息 + */ + String DETAILS_USER = "user_info"; + + /** + * 协议字段 + */ + String DETAILS_LICENSE = "license"; + + /** + * 验证码有效期,默认 60秒 + */ + long CODE_TIME = 60; + + /** + * 手机验证码长度 + */ + String PHONE_CODE_SIZE = "6"; + + /** + * 客户端模式 + */ + String CLIENT_CREDENTIALS = "client_credentials"; + + /** + * 客户端ID + */ + String CLIENT_ID = "clientId"; + + /** + * 模拟测试账户 + */ + String MOCK_USERNAME = "admin"; + + /** + * 模拟测试密码 + */ + String MOCK_PASSWORD = "kanglai123"; + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/ServiceNameConstants.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/ServiceNameConstants.java new file mode 100644 index 0000000..754cfcc --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/ServiceNameConstants.java @@ -0,0 +1,44 @@ +package com.cloud.kicc.common.core.constant; + +/** + *

+ * 服务名称 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +public interface ServiceNameConstants { + + /** + * 认证服务的SERVICE_ID + */ + String AUTH_SERVICE = "kicc-auth"; + + /** + * SYSTEM模块 + */ + String SYSTEM_SERVICE = "kicc-system-biz"; + + /** + * MONITOR模块 + */ + String MONITOR_SERVICE = "kicc-monitor-biz"; + + /** + * COMMON模块 + */ + String COMMON_SERVICE = "kicc-common-biz"; + + /** + * seata分布式事务演示-订单模块 + */ + String SEATA_ORDER_SERVICE = "kicc-seata-order"; + + /** + * seata分布式事务演示-积分模块 + */ + String SEATA_POINT_SERVICE = "kicc-seata-point"; + + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/StringPool.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/StringPool.java new file mode 100644 index 0000000..439b711 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/constant/StringPool.java @@ -0,0 +1,97 @@ +package com.cloud.kicc.common.core.constant; + +/** + * Copy to kicc.common.util + *

+ * Pool of String constants to prevent repeating of + * hard-coded String literals in the code. + * Due to fact that these are public static final + * they will be inlined by java compiler and + * reference to this class will be dropped. + * There is no performance gain of using this pool. + * Read: https://java.sun.com/docs/books/jls/third_edition/html/lexical.html#3.10.5 + *

    + *
  • Literal strings within the same class in the same package represent references to the same String object.
  • + *
  • Literal strings within different classes in the same package represent references to the same String object.
  • + *
  • Literal strings within different classes in different packages likewise represent references to the same String object.
  • + *
  • Strings computed by constant expressions are computed at compile time and then treated as if they were literals.
  • + *
  • Strings computed by concatenation at run time are newly created and therefore distinct.
  • + *
+ * @author wangxiang4 + */ +public interface StringPool { + + String AMPERSAND = "&"; + String AND = "and"; + String AT = "@"; + String ASTERISK = "*"; + String STAR = ASTERISK; + String BACK_SLASH = "\\"; + String COLON = ":"; + String COMMA = ","; + String DASH = "-"; + String DOLLAR = "$"; + String DOT = "."; + String DOTDOT = ".."; + String DOT_CLASS = ".class"; + String DOT_JAVA = ".java"; + String DOT_XML = ".xml"; + String EMPTY = ""; + String EQUALS = "="; + String FALSE = "false"; + String SLASH = "/"; + String HASH = "#"; + String HAT = "^"; + String LEFT_BRACE = "{"; + String LEFT_BRACKET = "("; + String LEFT_CHEV = "<"; + String DOT_NEWLINE = ",\n"; + String NEWLINE = "\n"; + String N = "n"; + String NO = "no"; + String NULL = "null"; + String OFF = "off"; + String ON = "on"; + String PERCENT = "%"; + String PIPE = "|"; + String PLUS = "+"; + String QUESTION_MARK = "?"; + String EXCLAMATION_MARK = "!"; + String QUOTE = "\""; + String RETURN = "\r"; + String TAB = "\t"; + String RIGHT_BRACE = "}"; + String RIGHT_BRACKET = ")"; + String RIGHT_CHEV = ">"; + String SEMICOLON = ";"; + String SINGLE_QUOTE = "'"; + String BACKTICK = "`"; + String SPACE = " "; + String TILDA = "~"; + String LEFT_SQ_BRACKET = "["; + String RIGHT_SQ_BRACKET = "]"; + String TRUE = "true"; + String UNDERSCORE = "_"; + String UTF_8 = "UTF-8"; + String US_ASCII = "US-ASCII"; + String ISO_8859_1 = "ISO-8859-1"; + String Y = "y"; + String YES = "yes"; + String ONE = "1"; + String ZERO = "0"; + String DOLLAR_LEFT_BRACE = "${"; + String HASH_LEFT_BRACE = "#{"; + String CRLF = "\r\n"; + + String HTML_NBSP = " "; + String HTML_AMP = "&"; + String HTML_QUOTE = """; + String HTML_LT = "<"; + String HTML_GT = ">"; + + // ---------------------------------------------------------------- array + + String[] EMPTY_ARRAY = new String[0]; + + byte[] BYTES_NEW_LINE = StringPool.NEWLINE.getBytes(); +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/enums/ExceptionEnum.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/enums/ExceptionEnum.java new file mode 100644 index 0000000..48f31df --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/enums/ExceptionEnum.java @@ -0,0 +1,35 @@ +package com.cloud.kicc.common.core.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + *

+ * API错误页面响应状态枚举 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/8/16 + */ +@Getter +@RequiredArgsConstructor +public enum ExceptionEnum { + + UNAUTHORIZED_ACCESS(401, "禁止访问"), + PAGE_NOT_ACCESS(403, "页面无法访问"), + PAGE_NOT_FOUND(404, "网页未找到"), + ERROR(500, "错误"), + NET_WORK_ERROR(10000, "前端Js错误"), + PAGE_NOT_DATA(10100, "无数据页面"); + + /** + * 状态 + */ + private final int value; + + /** + * 描述 + */ + private final String description; + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/enums/LoginTypeEnum.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/enums/LoginTypeEnum.java new file mode 100644 index 0000000..d78b590 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/enums/LoginTypeEnum.java @@ -0,0 +1,38 @@ +package com.cloud.kicc.common.core.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + *

+ * 社交登录类型 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@Getter +@RequiredArgsConstructor +public enum LoginTypeEnum { + + /** + * 账号密码登录 + */ + PWD("PWD", "账号密码登录"), + + /** + * 验证码登录 + */ + SMS("SMS", "验证码登录"); + + /** + * 类型 + */ + private final String type; + + /** + * 描述 + */ + private final String description; + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/exception/CheckedException.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/exception/CheckedException.java new file mode 100644 index 0000000..ba76ec4 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/exception/CheckedException.java @@ -0,0 +1,34 @@ +package com.cloud.kicc.common.core.exception; + +import lombok.NoArgsConstructor; + +/** + *

+ * 检查异常 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@NoArgsConstructor +public class CheckedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public CheckedException(String message) { + super(message); + } + + public CheckedException(Throwable cause) { + super(cause); + } + + public CheckedException(String message, Throwable cause) { + super(message, cause); + } + + public CheckedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/exception/ValidateCodeException.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/exception/ValidateCodeException.java new file mode 100644 index 0000000..2eeddd8 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/exception/ValidateCodeException.java @@ -0,0 +1,22 @@ +package com.cloud.kicc.common.core.exception; + +/** + *

+ * 验证码异常 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +public class ValidateCodeException extends RuntimeException { + + private static final long serialVersionUID = -7285211528095468156L; + + public ValidateCodeException() { + } + + public ValidateCodeException(String msg) { + super(msg); + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/factory/YamlPropertySourceFactory.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/factory/YamlPropertySourceFactory.java new file mode 100644 index 0000000..7d72189 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/factory/YamlPropertySourceFactory.java @@ -0,0 +1,48 @@ +package com.cloud.kicc.common.core.factory; + +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertySourceFactory; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Properties; + +/** + *

+ * 加载yml格式的自定义配置文件 + * @link https://blog.csdn.net/zxl8899/article/details/106382719/ + *

+ * + * @Author: wangxiang4 + * @Date: 2022/3/7 + */ +@AllArgsConstructor +public class YamlPropertySourceFactory implements PropertySourceFactory { + + @Override + public PropertySource createPropertySource(String name, EncodedResource resource) throws IOException { + Properties propertiesFromYaml = loadYamlIntoProperties(resource); + String sourceName = name != null ? name : resource.getResource().getFilename(); + assert sourceName != null; + return new PropertiesPropertySource(sourceName, propertiesFromYaml); + } + + private Properties loadYamlIntoProperties(EncodedResource resource) throws FileNotFoundException { + try { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(resource.getResource()); + factory.afterPropertiesSet(); + return factory.getObject(); + } catch (IllegalStateException e) { + Throwable cause = e.getCause(); + if (cause instanceof FileNotFoundException) { + throw (FileNotFoundException) e.getCause(); + } + throw e; + } + } +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/jackson/KiccJavaTimeModule.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/jackson/KiccJavaTimeModule.java new file mode 100644 index 0000000..7f9ff61 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/jackson/KiccJavaTimeModule.java @@ -0,0 +1,55 @@ +package com.cloud.kicc.common.core.jackson; + +import cn.hutool.core.date.DatePattern; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.PackageVersion; +import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + *

+ * java 8 时间默认序列化 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/3/10 + */ +public class KiccJavaTimeModule extends SimpleModule { + + public KiccJavaTimeModule() { + super(PackageVersion.VERSION); + + // ======================= 时间序列化规则 =============================== + // LocalDateTime序列化时间戳 + this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))); + // LocalDate序列化时间戳 + this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)); + // LocalTime序列化时间戳 + this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME)); + // Instant 类型序列化 + this.addSerializer(Instant.class, InstantSerializer.INSTANCE); + + // ======================= 时间反序列化规则 ============================== + // yyyy-MM-dd HH:mm:ss + this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))); + // yyyy-MM-dd + this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)); + // HH:mm:ss + this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME)); + // Instant 反序列化 + this.addDeserializer(Instant.class, InstantDeserializer.INSTANT); + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/BaseUtil.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/BaseUtil.java new file mode 100644 index 0000000..e9d8549 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/BaseUtil.java @@ -0,0 +1,27 @@ +package com.cloud.kicc.common.core.util; + +import cn.hutool.core.lang.generator.SnowflakeGenerator; +import lombok.experimental.UtilityClass; + +/** + *

+ * 基础工具类 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/3/21 + */ +@UtilityClass +public class BaseUtil { + + /** + * 雪花算法生成全局ID + * @Param + * @return + */ + public Long snowflakeId () { + SnowflakeGenerator snowflakeGenerator = new SnowflakeGenerator(); + return snowflakeGenerator.next(); + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/ClassUtil.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/ClassUtil.java new file mode 100644 index 0000000..44f586f --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/ClassUtil.java @@ -0,0 +1,97 @@ +package com.cloud.kicc.common.core.util; + +import lombok.experimental.UtilityClass; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.web.method.HandlerMethod; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + *

+ * 扩展类工具类 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@UtilityClass +public class ClassUtil extends org.springframework.util.ClassUtils { + + private final ParameterNameDiscoverer PARAMETERNAMEDISCOVERER = new DefaultParameterNameDiscoverer(); + + /** + * 获取方法参数信息 + * @param constructor 构造器 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public MethodParameter getMethodParameter(Constructor constructor, int parameterIndex) { + MethodParameter methodParameter = new SynthesizingMethodParameter(constructor, parameterIndex); + methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER); + return methodParameter; + } + + /** + * 获取方法参数信息 + * @param method 方法 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public MethodParameter getMethodParameter(Method method, int parameterIndex) { + MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex); + methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER); + return methodParameter; + } + + /** + * 获取Annotation + * @param method Method + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + public A getAnnotation(Method method, Class annotationType) { + Class targetClass = method.getDeclaringClass(); + // The method may be on an interface, but we need attributes from the target + // class. + // If the target class is null, the method will be unchanged. + Method specificMethod = ClassUtil.getMostSpecificMethod(method, targetClass); + // If we are dealing with method with generic parameters, find the original + // method. + specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + // 先找方法,再找方法上的类 + A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationType); + ; + if (null != annotation) { + return annotation; + } + // 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类 + return AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), annotationType); + } + + /** + * 获取Annotation + * @param handlerMethod HandlerMethod + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + public A getAnnotation(HandlerMethod handlerMethod, Class annotationType) { + // 先找方法,再找方法上的类 + A annotation = handlerMethod.getMethodAnnotation(annotationType); + if (null != annotation) { + return annotation; + } + // 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类 + Class beanType = handlerMethod.getBeanType(); + return AnnotatedElementUtils.findMergedAnnotation(beanType, annotationType); + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/DateUtil.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/DateUtil.java new file mode 100644 index 0000000..ad5696a --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/DateUtil.java @@ -0,0 +1,50 @@ +package com.cloud.kicc.common.core.util; + +import java.lang.management.ManagementFactory; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +/** + *

+ * 时间 util + *

+ * + * @Author: wangxiang4 + * @since: 2023/6/13 + */ +public class DateUtil extends cn.hutool.core.date.DateUtil { + + /** + * 获取服务器启动时间 + */ + public static Date getServerStartDate() { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + public static String formatDate(long dateTime, String pattern) { + return format(new Date(dateTime), pattern); + } + + /** + * 日期型字符串转化为日期 格式 + */ + public static Date parseDate(Object str) { + if (str == null) { + return null; + } + return parse(str.toString()); + } + + public static final String parseDateToStr(final String format, final Date date) { + return new SimpleDateFormat(format).format(date); + } + + public static final String formatUTC(final Date date, final String format) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); + simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return simpleDateFormat.format(date); + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/FileUtil.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/FileUtil.java new file mode 100644 index 0000000..4647805 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/FileUtil.java @@ -0,0 +1,113 @@ +package com.cloud.kicc.common.core.util; + +import cn.hutool.core.io.IoUtil; + +import javax.servlet.http.HttpServletRequest; +import java.io.*; +import java.net.URLEncoder; +import java.text.DecimalFormat; + +/** + * @author yong + * @date 2020/3/22 + * @description 文件工具类 + */ +public class FileUtil extends cn.hutool.core.io.FileUtil { + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 转换文件大小 + * + * @param fileS + * @return + */ + public static String fileSize(long fileS) { + DecimalFormat df = new DecimalFormat("#.00"); + String fileSizeString = ""; + String wrongSize = "0B"; + if (fileS == 0) { + return wrongSize; + } + if (fileS < 1024) { + fileSizeString = df.format((double) fileS) + "B"; + } else if (fileS < 1048576) { + fileSizeString = df.format((double) fileS / 1024) + "KB"; + } else if (fileS < 1073741824) { + fileSizeString = df.format((double) fileS / 1048576) + "MB"; + } else { + fileSizeString = df.format((double) fileS / 1073741824) + "GB"; + } + return fileSizeString; + } + + public static void copyInputStreamToFile(InputStream source, File destination) throws IOException { + try { + copyToFile(source, destination); + } finally { + IoUtil.close(source); + } + + } + + public static void copyToFile(InputStream source, File destination) throws IOException { + FileOutputStream output = openOutputStream(destination); + + try { + IoUtil.copy(source, output); + output.close(); + } finally { + IoUtil.close(output); + } + + } + + public static FileOutputStream openOutputStream(File file) throws IOException { + return openOutputStream(file, false); + } + + public static FileOutputStream openOutputStream(File file, boolean append) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + + if (!file.canWrite()) { + throw new IOException("File '" + file + "' cannot be written to"); + } + } else { + File parent = file.getParentFile(); + if (parent != null && !parent.mkdirs() && !parent.isDirectory()) { + throw new IOException("Directory '" + parent + "' could not be created"); + } + } + + return new FileOutputStream(file, append); + } + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) + throws UnsupportedEncodingException { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } else if (agent.contains("Firefox")) { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } else if (agent.contains("Chrome")) { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } else { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/HTMLFilterUtil.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/HTMLFilterUtil.java new file mode 100644 index 0000000..776bef3 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/HTMLFilterUtil.java @@ -0,0 +1,499 @@ +package com.cloud.kicc.common.core.util; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

+ * HTML过滤器,用于去除XSS漏洞隐患。 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +public final class HTMLFilterUtil { + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap<>(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" + * becomes " text "). If set to false, unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilterUtil() { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[]{"img"}; + vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"}; + vDisallowed = new String[]{}; + vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp. + vProtocolAtts = new String[]{"src", "href"}; + vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"}; + vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = true; + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilterUtil(final Map conf) { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() { + vTagCounts.clear(); + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) { + reset(); + String s = input; + + s = escapeComments(s); + + s = balanceHTML(s); + + s = checkTags(s); + + s = processRemoveBlanks(s); + + s = validateEntities(s); + + return s; + } + + public boolean isAlwaysMakeTags() { + return alwaysMakeTags; + } + + public boolean isStripComments() { + return stripComment; + } + + private String escapeComments(final String s) { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) { + if (alwaysMakeTags) { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } else { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + final StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) { + for (int ii = 0; ii < vTagCounts.get(key); ii++) { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) { + String result = s; + for (String tag : vRemoveBlanks) { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) { + if (false == inArray(name, vSelfClosingTags)) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) { + final StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) { + if (inArray(paramName, vProtocolAtts)) { + paramValue = processParamProtocol(paramValue); + } + params.append(' ').append(paramName).append("=\"").append(paramValue).append("\""); + } + } + + if (inArray(name, vSelfClosingTags)) { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) { + ending = ""; + } + + if (ending == null || ending.length() < 1) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } else { + vTagCounts.put(name, 1); + } + } else { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } else { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) { + if (encodeQuotes) { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two) + m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); + } + m.appendTail(buf); + return buf.toString(); + } else { + return s; + } + } + + private String checkEntity(final String preamble, final String term) { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) { + for (String item : array) { + if (item != null && item.equals(s)) { + return true; + } + } + return false; + } + + private boolean allowed(final String name) { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/JasyptUtil.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/JasyptUtil.java new file mode 100644 index 0000000..03cc21c --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/JasyptUtil.java @@ -0,0 +1,38 @@ +package com.cloud.kicc.common.core.util; + +import com.ulisesbocchio.jasyptspringboot.encryptor.DefaultLazyEncryptor; +import org.jasypt.encryption.StringEncryptor; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + *

+ * Jasypt加解密单元测试 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/24 + */ +public class JasyptUtil { + + public static void main(String[] args) { + testEnvironmentProperties(); + } + + public static void testEnvironmentProperties() { + System.setProperty("jasypt.encryptor.password", "kicc"); + + PasswordEncoder ENCODER = new BCryptPasswordEncoder(); + + System.out.println(ENCODER.encode("123456")); + + StringEncryptor stringEncryptor = new DefaultLazyEncryptor(new StandardEnvironment()); + //加密方法 + System.out.println(stringEncryptor.encrypt("kicc")); + + //解密方法 + System.out.println(stringEncryptor.decrypt("6GBMom2U/XAHuMG3OSkOMw==")); + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/PinyinUtil.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/PinyinUtil.java new file mode 100644 index 0000000..e4b8e33 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/PinyinUtil.java @@ -0,0 +1,203 @@ +package com.cloud.kicc.common.core.util; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + *

+ * 拼音工具类 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/24 + */ +public class PinyinUtil { + /** 汉字对应ascii范围 */ + private static int[] pinyinValue = new int[] { -20319, -20317, -20304, -20295, -20292, -20283, -20265, -20257, -20242, -20230, -20051, -20036, -20032, -20026, -20002, -19990, -19986, -19982, + -19976, -19805, -19784, -19775, -19774, -19763, -19756, -19751, -19746, -19741, -19739, -19728, -19725, -19715, -19540, -19531, -19525, -19515, -19500, -19484, -19479, -19467, -19289, + -19288, -19281, -19275, -19270, -19263, -19261, -19249, -19243, -19242, -19238, -19235, -19227, -19224, -19218, -19212, -19038, -19023, -19018, -19006, -19003, -18996, -18977, -18961, + -18952, -18783, -18774, -18773, -18763, -18756, -18741, -18735, -18731, -18722, -18710, -18697, -18696, -18526, -18518, -18501, -18490, -18478, -18463, -18448, -18447, -18446, -18239, + -18237, -18231, -18220, -18211, -18201, -18184, -18183, -18181, -18012, -17997, -17988, -17970, -17964, -17961, -17950, -17947, -17931, -17928, -17922, -17759, -17752, -17733, -17730, + -17721, -17703, -17701, -17697, -17692, -17683, -17676, -17496, -17487, -17482, -17468, -17454, -17433, -17427, -17417, -17202, -17185, -16983, -16970, -16942, -16915, -16733, -16708, + -16706, -16689, -16664, -16657, -16647, -16474, -16470, -16465, -16459, -16452, -16448, -16433, -16429, -16427, -16423, -16419, -16412, -16407, -16403, -16401, -16393, -16220, -16216, + -16212, -16205, -16202, -16187, -16180, -16171, -16169, -16158, -16155, -15959, -15958, -15944, -15933, -15920, -15915, -15903, -15889, -15878, -15707, -15701, -15681, -15667, -15661, + -15659, -15652, -15640, -15631, -15625, -15454, -15448, -15436, -15435, -15419, -15416, -15408, -15394, -15385, -15377, -15375, -15369, -15363, -15362, -15183, -15180, -15165, -15158, + -15153, -15150, -15149, -15144, -15143, -15141, -15140, -15139, -15128, -15121, -15119, -15117, -15110, -15109, -14941, -14937, -14933, -14930, -14929, -14928, -14926, -14922, -14921, + -14914, -14908, -14902, -14894, -14889, -14882, -14873, -14871, -14857, -14678, -14674, -14670, -14668, -14663, -14654, -14645, -14630, -14594, -14429, -14407, -14399, -14384, -14379, + -14368, -14355, -14353, -14345, -14170, -14159, -14151, -14149, -14145, -14140, -14137, -14135, -14125, -14123, -14122, -14112, -14109, -14099, -14097, -14094, -14092, -14090, -14087, + -14083, -13917, -13914, -13910, -13907, -13906, -13905, -13896, -13894, -13878, -13870, -13859, -13847, -13831, -13658, -13611, -13601, -13406, -13404, -13400, -13398, -13395, -13391, + -13387, -13383, -13367, -13359, -13356, -13343, -13340, -13329, -13326, -13318, -13147, -13138, -13120, -13107, -13096, -13095, -13091, -13076, -13068, -13063, -13060, -12888, -12875, + -12871, -12860, -12858, -12852, -12849, -12838, -12831, -12829, -12812, -12802, -12607, -12597, -12594, -12585, -12556, -12359, -12346, -12320, -12300, -12120, -12099, -12089, -12074, + -12067, -12058, -12039, -11867, -11861, -11847, -11831, -11798, -11781, -11604, -11589, -11536, -11358, -11340, -11339, -11324, -11303, -11097, -11077, -11067, -11055, -11052, -11045, + -11041, -11038, -11024, -11020, -11019, -11018, -11014, -10838, -10832, -10815, -10800, -10790, -10780, -10764, -10587, -10544, -10533, -10519, -10331, -10329, -10328, -10322, -10315, + -10309, -10307, -10296, -10281, -10274, -10270, -10262, -10260, -10256, -10254 }; + + private static String[] pinyinStr = new String[] { "a", "ai", "an", "ang", "ao", "ba", "bai", "ban", "bang", "bao", "bei", "ben", "beng", "bi", "bian", "biao", "bie", "bin", "bing", "bo", "bu", + "ca", "cai", "can", "cang", "cao", "ce", "ceng", "cha", "chai", "chan", "chang", "chao", "che", "chen", "cheng", "chi", "chong", "chou", "chu", "chuai", "chuan", "chuang", "chui", "chun", + "chuo", "ci", "cong", "cou", "cu", "cuan", "cui", "cun", "cuo", "da", "dai", "dan", "dang", "dao", "de", "deng", "di", "dian", "diao", "die", "ding", "diu", "dong", "dou", "du", "duan", + "dui", "dun", "duo", "e", "en", "er", "fa", "fan", "fang", "fei", "fen", "feng", "fo", "fou", "fu", "ga", "gai", "gan", "gang", "gao", "ge", "gei", "gen", "geng", "gong", "gou", "gu", + "gua", "guai", "guan", "guang", "gui", "gun", "guo", "ha", "hai", "han", "hang", "hao", "he", "hei", "hen", "heng", "hong", "hou", "hu", "hua", "huai", "huan", "huang", "hui", "hun", + "huo", "ji", "jia", "jian", "jiang", "jiao", "jie", "jin", "jing", "jiong", "jiu", "ju", "juan", "jue", "jun", "ka", "kai", "kan", "kang", "kao", "ke", "ken", "keng", "kong", "kou", "ku", + "kua", "kuai", "kuan", "kuang", "kui", "kun", "kuo", "la", "lai", "lan", "lang", "lao", "le", "lei", "leng", "li", "lia", "lian", "liang", "liao", "lie", "lin", "ling", "liu", "long", + "lou", "lu", "lv", "luan", "lue", "lun", "luo", "ma", "mai", "man", "mang", "mao", "me", "mei", "men", "meng", "mi", "mian", "miao", "mie", "min", "ming", "miu", "mo", "mou", "mu", "na", + "nai", "nan", "nang", "nao", "ne", "nei", "nen", "neng", "ni", "nian", "niang", "niao", "nie", "nin", "ning", "niu", "nong", "nu", "nv", "nuan", "nue", "nuo", "o", "ou", "pa", "pai", + "pan", "pang", "pao", "pei", "pen", "peng", "pi", "pian", "piao", "pie", "pin", "ping", "po", "pu", "qi", "qia", "qian", "qiang", "qiao", "qie", "qin", "qing", "qiong", "qiu", "qu", + "quan", "que", "qun", "ran", "rang", "rao", "re", "ren", "reng", "ri", "rong", "rou", "ru", "ruan", "rui", "run", "ruo", "sa", "sai", "san", "sang", "sao", "se", "sen", "seng", "sha", + "shai", "shan", "shang", "shao", "she", "shen", "sheng", "shi", "shou", "shu", "shua", "shuai", "shuan", "shuang", "shui", "shun", "shuo", "si", "song", "sou", "su", "suan", "sui", "sun", + "suo", "ta", "tai", "tan", "tang", "tao", "te", "teng", "ti", "tian", "tiao", "tie", "ting", "tong", "tou", "tu", "tuan", "tui", "tun", "tuo", "wa", "wai", "wan", "wang", "wei", "wen", + "weng", "wo", "wu", "xi", "xia", "xian", "xiang", "xiao", "xie", "xin", "xing", "xiong", "xiu", "xu", "xuan", "xue", "xun", "ya", "yan", "yang", "yao", "ye", "yi", "yin", "ying", "yo", + "yong", "you", "yu", "yuan", "yue", "yun", "za", "zai", "zan", "zang", "zao", "ze", "zei", "zen", "zeng", "zha", "zhai", "zhan", "zhang", "zhao", "zhe", "zhen", "zheng", "zhi", "zhong", + "zhou", "zhu", "zhua", "zhuai", "zhuan", "zhuang", "zhui", "zhun", "zhuo", "zi", "zong", "zou", "zu", "zuan", "zui", "zun", "zuo" }; + + /** + * 获取所给中文的每个汉字首字母组成首字母字符串 + * + * @param chinese 汉字字符串 + * @return 首字母字符串 + */ + public static String getAllFirstLetter(String chinese) { + if (StrUtil.isBlank(chinese)) { + return StrUtil.EMPTY; + } + + int len = chinese.length(); + final StrBuilder strBuilder = new StrBuilder(len); + for (int i = 0; i < len; i++) { + strBuilder.append(getFirstLetter(chinese.charAt(i))); + } + + return strBuilder.toString(); + } + + /** + * 获取拼音首字母
+ * 传入汉字,返回拼音首字母
+ * 如果传入为字母,返回其小写形式
+ * 感谢【帝都】宁静 提供方法 + * + * @param ch 汉字 + * @return 首字母,小写 + */ + public static char getFirstLetter(char ch) { + if (ch >= 'a' && ch <= 'z') { + return ch; + } + if (ch >= 'A' && ch <= 'Z') { + return Character.toLowerCase(ch); + } + final byte[] bys = String.valueOf(ch).getBytes(CharsetUtil.CHARSET_GBK); + if (bys.length == 1) { + return ch; + } + int count = (bys[0] + 256) * 256 + bys[1] + 256; + if (count < 45217) { + return ch; + } else if (count < 45253) { + return 'a'; + } else if (count < 45761) { + return 'b'; + } else if (count < 46318) { + return 'c'; + } else if (count < 46826) { + return 'd'; + } else if (count < 47010) { + return 'e'; + } else if (count < 47297) { + return 'f'; + } else if (count < 47614) { + return 'g'; + } else if (count < 48119) { + return 'h'; + } else if (count < 49062) { + return 'j'; + } else if (count < 49324) { + return 'k'; + } else if (count < 49896) { + return 'l'; + } else if (count < 50371) { + return 'm'; + } else if (count < 50614) { + return 'n'; + } else if (count < 50622) { + return 'o'; + } else if (count < 50906) { + return 'p'; + } else if (count < 51387) { + return 'q'; + } else if (count < 51446) { + return 'r'; + } else if (count < 52218) { + return 's'; + } else if (count < 52698) { + return 't'; + } else if (count < 52980) { + return 'w'; + } else if (count < 53689) { + return 'x'; + } else if (count < 54481) { + return 'y'; + } else if (count < 55290) { + return 'z'; + } + return ch; + } + + /** + * 汉字转拼音 + *
+ * example : 张三 zhangsan + * + * @param chinese 汉字 + * @return 对应的拼音 + * @since 4.0.11 + */ + public static String getPinYin(String chinese) { + final StrBuilder result = StrUtil.strBuilder(); + String strTemp = null; + int len = chinese.length(); + for (int j = 0; j < len; j++) { + strTemp = chinese.substring(j, j + 1); + int ascii = getChsAscii(strTemp); + if (ascii > 0) { + //非汉字 + result.append((char)ascii); + } else { + for (int i = pinyinValue.length - 1; i >= 0; i--) { + if (pinyinValue[i] <= ascii) { + result.append(pinyinStr[i]); + break; + } + } + } + } + return result.toString(); + } + + //------------------------------------------------------------------------------------------------------- Private method start + /** + * 获取汉字对应的ascii码 + * @param chs 汉字 + * @return ascii码 + */ + private static int getChsAscii(String chs) { + int asc = 0; + byte[] bytes = chs.getBytes(CharsetUtil.CHARSET_GBK); + switch (bytes.length) { + case 1: + // 英文字符 + asc = bytes[0]; + break; + case 2: + // 中文字符 + int hightByte = 256 + bytes[0]; + int lowByte = 256 + bytes[1]; + asc = (256 * hightByte + lowByte) - 256 * 256; + break; + default: + throw new UtilException("Illegal resource string"); + } + return asc; + } +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/SpringContextHolderUtil.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/SpringContextHolderUtil.java new file mode 100644 index 0000000..5b99976 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/SpringContextHolderUtil.java @@ -0,0 +1,87 @@ +package com.cloud.kicc.common.core.util; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +/** + *

+ * Spring工具类 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@Slf4j +@Service +@Lazy(false) +public class SpringContextHolderUtil implements ApplicationContextAware, DisposableBean { + + private static ApplicationContext applicationContext = null; + + /** + * 取得存储在静态变量中的ApplicationContext. + */ + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + /** + * 实现ApplicationContextAware接口, 注入Context到静态变量中. + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + SpringContextHolderUtil.applicationContext = applicationContext; + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) { + return (T) applicationContext.getBean(name); + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + public static T getBean(Class requiredType) { + return applicationContext.getBean(requiredType); + } + + /** + * 清除SpringContextHolder中的ApplicationContext为Null. + */ + public static void clearHolder() { + if (log.isDebugEnabled()) { + log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext); + } + applicationContext = null; + } + + /** + * 发布事件 + * @param event + */ + public static void publishEvent(ApplicationEvent event) { + if (applicationContext == null) { + return; + } + applicationContext.publishEvent(event); + } + + /** + * 实现DisposableBean接口, 在Context关闭时清理静态变量. + */ + @Override + @SneakyThrows + public void destroy() { + SpringContextHolderUtil.clearHolder(); + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/TimeUtils.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/TimeUtils.java new file mode 100644 index 0000000..6e62470 --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/TimeUtils.java @@ -0,0 +1,321 @@ +package com.cloud.kicc.common.core.util; + +import java.util.Arrays; +import java.util.Date; + +/** + *

+ * 时间计算工具类 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/1/15 + */ +public class TimeUtils { + + public static String toTimeString(long time) { + TimeUtils t = new TimeUtils(time); + int day = t.get(TimeUtils.DAY); + int hour = t.get(TimeUtils.HOUR); + int minute = t.get(TimeUtils.MINUTE); + int second = t.get(TimeUtils.SECOND); + StringBuilder sb = new StringBuilder(); + if (day > 0){ + sb.append(day).append("天"); + } + if (hour > 0){ + sb.append(hour).append("时"); + } + if (minute > 0){ + sb.append(minute).append("分"); + } + if (second > 0){ + sb.append(second).append("秒"); + } + return sb.toString(); + } + + /** + * 时间字段常量,表示“秒” + */ + public final static int SECOND = 0; + + /** + * 时间字段常量,表示“分” + */ + public final static int MINUTE = 1; + + /** + * 时间字段常量,表示“时” + */ + public final static int HOUR = 2; + + /** + * 时间字段常量,表示“天” + */ + public final static int DAY = 3; + + /** + * 各常量允许的最大值 + */ + private final int[] maxFields = { 59, 59, 23, Integer.MAX_VALUE - 1 }; + + /** + * 各常量允许的最小值 + */ + private final int[] minFields = { 0, 0, 0, Integer.MIN_VALUE }; + + /** + * 默认的字符串格式时间分隔符 + */ + private String timeSeparator = ":"; + + /** + * 时间数据容器 + */ + private int[] fields = new int[4]; + + /** + * 无参构造,将各字段置为 0 + */ + public TimeUtils() { + this(0, 0, 0, 0); + } + + /** + * 使用时、分构造一个时间 + * @param hour 小时 + * @param minute 分钟 + */ + public TimeUtils(int hour, int minute) { + this(0, hour, minute, 0); + } + + /** + * 使用时、分、秒构造一个时间 + * @param hour 小时 + * @param minute 分钟 + * @param second 秒 + */ + public TimeUtils(int hour, int minute, int second) { + this(0, hour, minute, second); + } + + /** + * 使用一个字符串构造时间
+ * Time time = new Time("14:22:23"); + * @param time 字符串格式的时间,默认采用“:”作为分隔符 + */ + public TimeUtils(String time) { + this(time, null); +// System.out.println(time); + } + + /** + * 使用时间毫秒构建时间 + * @param time + */ + public TimeUtils(long time){ + this(new Date(time)); + } + /** + * 使用日期对象构造时间 + * @param date + */ + public TimeUtils(Date date){ + this(DateUtil.formatUTC(date, "HH:mm:ss")); + } + + /** + * 使用天、时、分、秒构造时间,进行全字符的构造 + * @param day 天 + * @param hour 时 + * @param minute 分 + * @param second 秒 + */ + public TimeUtils(int day, int hour, int minute, int second) { + initialize(day, hour, minute, second); + } + + /** + * 使用一个字符串构造时间,指定分隔符
+ * Time time = new Time("14-22-23", "-"); + * @param time 字符串格式的时间 + */ + public TimeUtils(String time, String timeSeparator) { + if(timeSeparator != null) { + setTimeSeparator(timeSeparator); + } + parseTime(time); + } + + /** + * 设置时间字段的值 + * @param field 时间字段常量 + * @param value 时间字段的值 + */ + public void set(int field, int value) { + if(value < minFields[field]) { + throw new IllegalArgumentException(value + ", time value must be positive."); + } + fields[field] = value % (maxFields[field] + 1); + // 进行进位计算 + int carry = value / (maxFields[field] + 1); + if(carry > 0) { + int upFieldValue = get(field + 1); + set(field + 1, upFieldValue + carry); + } + } + + /** + * 获得时间字段的值 + * @param field 时间字段常量 + * @return 该时间字段的值 + */ + public int get(int field) { + if(field < 0 || field > fields.length - 1) { + throw new IllegalArgumentException(field + ", field value is error."); + } + return fields[field]; + } + + /** + * 将时间进行“加”运算,即加上一个时间 + * @param time 需要加的时间 + * @return 运算后的时间 + */ + public TimeUtils addTime(TimeUtils time) { + TimeUtils result = new TimeUtils(); + int up = 0; // 进位标志 + for (int i = 0; i < fields.length; i++) { + int sum = fields[i] + time.fields[i] + up; + up = sum / (maxFields[i] + 1); + result.fields[i] = sum % (maxFields[i] + 1); + } + return result; + } + + /** + * 将时间进行“减”运算,即减去一个时间 + * @param time 需要减的时间 + * @return 运算后的时间 + */ + public TimeUtils subtractTime(TimeUtils time) { + TimeUtils result = new TimeUtils(); + int down = 0; // 退位标志 + for (int i = 0, k = fields.length - 1; i < k; i++) { + int difference = fields[i] + down; + if (difference >= time.fields[i]) { + difference -= time.fields[i]; + down = 0; + } else { + difference += maxFields[i] + 1 - time.fields[i]; + down = -1; + } + result.fields[i] = difference; + } + result.fields[DAY] = fields[DAY] - time.fields[DAY] + down; + return result; + } + + /** + * 获得时间字段的分隔符 + * @return + */ + public String getTimeSeparator() { + return timeSeparator; + } + + /** + * 设置时间字段的分隔符(用于字符串格式的时间) + * @param timeSeparator 分隔符字符串 + */ + public void setTimeSeparator(String timeSeparator) { + this.timeSeparator = timeSeparator; + } + + private void initialize(int day, int hour, int minute, int second) { + set(DAY, day); + set(HOUR, hour); + set(MINUTE, minute); + set(SECOND, second); + } + + private void parseTime(String time) { + if(time == null) { + initialize(0, 0, 0, 0); + return; + } + String t = time; + int field = DAY; + set(field--, 0); + int p = -1; + while((p = t.indexOf(timeSeparator)) > -1) { + parseTimeField(time, t.substring(0, p), field--); + t = t.substring(p + timeSeparator.length()); + } + parseTimeField(time, t, field--); + } + + private void parseTimeField(String time, String t, int field) { + if(field < SECOND || t.length() < 1) { + parseTimeException(time); + } + char[] chs = t.toCharArray(); + int n = 0; + for(int i = 0; i < chs.length; i++) { + if(chs[i] <= ' ') { + continue; + } + if(chs[i] >= '0' && chs[i] <= '9') { + n = n * 10 + chs[i] - '0'; + continue; + } + parseTimeException(time); + } + set(field, n); + } + + private void parseTimeException(String time) { + throw new IllegalArgumentException(time + ", time format error, HH" + + this.timeSeparator + "mm" + this.timeSeparator + "ss"); + } + + public String toString() { + StringBuilder sb = new StringBuilder(16); + sb.append(fields[DAY]).append(',').append(' '); + buildString(sb, HOUR).append(timeSeparator); + buildString(sb, MINUTE).append(timeSeparator); + buildString(sb, SECOND); + return sb.toString(); + } + + private StringBuilder buildString(StringBuilder sb, int field) { + if(fields[field] < 10) { + sb.append('0'); + } + return sb.append(fields[field]); + } + + public int hashCode() { + final int PRIME = 31; + int result = 1; + result = PRIME * result + Arrays.hashCode(fields); + return result; + } + + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final TimeUtils other = (TimeUtils) obj; + if (!Arrays.equals(fields, other.fields)) { + return false; + } + return true; + } + +} diff --git a/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/WebUtil.java b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/WebUtil.java new file mode 100644 index 0000000..9f7bf5c --- /dev/null +++ b/kicc-common-core/src/main/java/com/cloud/kicc/common/core/util/WebUtil.java @@ -0,0 +1,186 @@ +package com.cloud.kicc.common.core.util; + +import cn.hutool.core.codec.Base64; +import cn.hutool.json.JSONUtil; +import com.cloud.kicc.common.core.constant.CommonConstants; +import com.cloud.kicc.common.core.exception.CheckedException; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.NotNull; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + *

+ * 扩展用于 Web 应用程序的各种实用程序工具类 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@Slf4j +@UtilityClass +public class WebUtil extends org.springframework.web.util.WebUtils { + + private final String BASIC_ = "Basic "; + + private final String UNKNOWN = "unknown"; + + /** + * 判断是否ajax请求 spring ajax 返回含有 ResponseBody 或者 RestController注解 + * @param handlerMethod HandlerMethod + * @return 是否ajax请求 + */ + public boolean isBody(HandlerMethod handlerMethod) { + ResponseBody responseBody = ClassUtil.getAnnotation(handlerMethod, ResponseBody.class); + return responseBody != null; + } + + /** + * 读取cookie + * @param name cookie name + * @return cookie value + */ + public String getCookieVal(String name) { + if (WebUtil.getRequest().isPresent()) { + return getCookieVal(WebUtil.getRequest().get(), name); + } + return null; + } + + /** + * 读取cookie + * @param request HttpServletRequest + * @param name cookie name + * @return cookie value + */ + public String getCookieVal(HttpServletRequest request, String name) { + Cookie cookie = getCookie(request, name); + return cookie != null ? cookie.getValue() : null; + } + + /** + * 清除 某个指定的cookie + * @param response HttpServletResponse + * @param key cookie key + */ + public void removeCookie(HttpServletResponse response, String key) { + setCookie(response, key, null, 0); + } + + /** + * 设置cookie + * @param response HttpServletResponse + * @param name cookie name + * @param value cookie value + * @param maxAgeInSeconds maxage + */ + public void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setMaxAge(maxAgeInSeconds); + cookie.setHttpOnly(true); + response.addCookie(cookie); + } + + /** + * 获取 HttpServletRequest + * @return {HttpServletRequest} + */ + public Optional getRequest() { + return Optional + .ofNullable(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()); + } + + /** + * 获取 HttpServletResponse + * @return {HttpServletResponse} + */ + public HttpServletResponse getResponse() { + return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); + } + + /** + * 返回json + * @param response HttpServletResponse + * @param result 结果对象 + */ + public void renderJson(HttpServletResponse response, Object result) { + renderJson(response, result, MediaType.APPLICATION_JSON_VALUE); + } + + /** + * 返回json + * @param response HttpServletResponse + * @param result 结果对象 + * @param contentType contentType + */ + public void renderJson(HttpServletResponse response, Object result, String contentType) { + response.setCharacterEncoding(CommonConstants.UTF8); + response.setContentType(contentType); + try (PrintWriter out = response.getWriter()) { + out.append(JSONUtil.toJsonStr(result)); + } + catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + /** + * 从request 获取CLIENT_ID + * @return + */ + @SneakyThrows + public String getClientId(ServerHttpRequest request) { + String header = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + return splitClient(header)[0]; + } + + @SneakyThrows + public String getClientId() { + if (WebUtil.getRequest().isPresent()) { + String header = WebUtil.getRequest().get().getHeader(HttpHeaders.AUTHORIZATION); + return splitClient(header)[0]; + } + return null; + } + + @NotNull + private static String[] splitClient(String header) { + if (header == null || !header.startsWith(BASIC_)) { + throw new CheckedException("请求头中client信息为空!"); + } + byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8); + byte[] decoded; + try { + decoded = Base64.decode(base64Token); + } + catch (IllegalArgumentException e) { + throw new CheckedException("Failed to decode basic authentication token"); + } + + String token = new String(decoded, StandardCharsets.UTF_8); + + int delim = token.indexOf(":"); + + if (delim == -1) { + throw new CheckedException("Invalid basic authentication token"); + } + return new String[] { token.substring(0, delim), token.substring(delim + 1) }; + } + +} diff --git a/kicc-common-core/src/main/resources/META-INF/spring.factories b/kicc-common-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..d0a408a --- /dev/null +++ b/kicc-common-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.cloud.kicc.common.core.config.WebMvcConfiguration,\ + com.cloud.kicc.common.core.config.RestTemplateConfiguration,\ + com.cloud.kicc.common.core.config.GatewayConfigProperties,\ + com.cloud.kicc.common.core.util.SpringContextHolderUtil diff --git a/kicc-common-core/src/main/resources/banner.txt b/kicc-common-core/src/main/resources/banner.txt new file mode 100644 index 0000000..7e41f98 --- /dev/null +++ b/kicc-common-core/src/main/resources/banner.txt @@ -0,0 +1,17 @@ +${AnsiColor.BRIGHT_GREEN} + + ___ __ ___ ________ ________ + |\ \|\ \ |\ \|\ ____\|\ ____\ + \ \ \/ /|\ \ \ \ \___|\ \ \___| + \ \ ___ \ \ \ \ \ \ \ \ + \ \ \\ \ \ \ \ \ \____\ \ \____ + \ \__\\ \__\ \__\ \_______\ \_______\ + \|__| \|__|\|__|\|_______|\|_______| + + www.kanglailab.com + + 长沙康来生物有限公司 Microservice Architecture +${AnsiColor.DEFAULT} + + + diff --git a/kicc-common-core/src/main/resources/logback-spring.xml b/kicc-common-core/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..9cd338b --- /dev/null +++ b/kicc-common-core/src/main/resources/logback-spring.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + + + + + + ${log.path}/debug.log + + ${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz + 50MB + 30 + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + + + + ${log.path}/error.log + + ${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz + 50MB + 30 + + + %date [%thread] %-5level [%logger{50}] %file:%line - %msg%n + + + ERROR + + + + + + + + + + diff --git a/kicc-common-data/pom.xml b/kicc-common-data/pom.xml new file mode 100644 index 0000000..7a8b29f --- /dev/null +++ b/kicc-common-data/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-data + jar + + kicc 数据服务核心包 + + + + + + com.cloud + kicc-common-core + + + + com.fasterxml.jackson.core + jackson-annotations + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + com.baomidou + mybatis-plus-boot-starter + + + + mysql + mysql-connector-java + + + + + + + + + org.springframework + spring-webmvc + + + + org.springframework.security + spring-security-core + + + diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/annotation/EnableKiccDataRepository.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/annotation/EnableKiccDataRepository.java new file mode 100644 index 0000000..b2b4ae4 --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/annotation/EnableKiccDataRepository.java @@ -0,0 +1,24 @@ +package com.cloud.kicc.common.data.annotation; + +import com.cloud.kicc.common.data.config.MybatisConfiguration; +import com.cloud.kicc.common.data.config.RedisTemplateConfiguration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + *

+ * 开启数据存储和访问层 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/9/15 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import({ MybatisConfiguration.class, RedisTemplateConfiguration.class }) +public @interface EnableKiccDataRepository { + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/config/MybatisConfiguration.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/config/MybatisConfiguration.java new file mode 100644 index 0000000..b8b7a42 --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/config/MybatisConfiguration.java @@ -0,0 +1,66 @@ +package com.cloud.kicc.common.data.config; + +import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.extension.MybatisMapWrapperFactory; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.cloud.kicc.common.core.factory.YamlPropertySourceFactory; +import com.cloud.kicc.common.data.handler.BaseMetaObjectHandler; +import com.cloud.kicc.common.data.handler.KiccTenantLineHandler; +import com.cloud.kicc.common.data.plugins.KiccPaginationInnerInterceptor; +import com.cloud.kicc.common.data.plugins.KiccTenantLineInnerInterceptor; +import com.cloud.kicc.common.data.properties.TenantProperties; +import lombok.RequiredArgsConstructor; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +/** + *

+ * mybatis plus 统一配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@Configuration(proxyBeanMethods = false) +@MapperScan("com.cloud.kicc.**.mapper") +@EnableConfigurationProperties(TenantProperties.class) +@PropertySource(factory = YamlPropertySourceFactory.class, value = "classpath:kicc-tenant.yml") +@RequiredArgsConstructor +public class MybatisConfiguration { + + private final TenantProperties tenantProperties; + + /** + * 插件配置 + * 多租户插件, 自动拼接多租户id进行增删改查 + * 分页插件, 对于单一数据库类型来说,都建议配置该值,避免每次分页都去抓取数据库类型 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new KiccTenantLineInnerInterceptor(new KiccTenantLineHandler(tenantProperties))); + interceptor.addInnerInterceptor(new KiccPaginationInnerInterceptor()); + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + return interceptor; + } + + /** + * 审计字段自动填充 + * @return {@link MetaObjectHandler} + */ + @Bean + public BaseMetaObjectHandler mybatisPlusMetaObjectHandler() { + return new BaseMetaObjectHandler(); + } + + @Bean + public ConfigurationCustomizer mybatisConfigurationCustomizer(){ + return configuration -> configuration.setObjectWrapperFactory(new MybatisMapWrapperFactory()); + } + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/config/RedisTemplateConfiguration.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/config/RedisTemplateConfiguration.java new file mode 100644 index 0000000..99b312d --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/config/RedisTemplateConfiguration.java @@ -0,0 +1,60 @@ +package com.cloud.kicc.common.data.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.*; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + *

+ * Redis 配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@EnableCaching +@Configuration(proxyBeanMethods = false) +public class RedisTemplateConfiguration { + + @Bean + @ConditionalOnMissingBean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(RedisSerializer.string()); + redisTemplate.setHashKeySerializer(RedisSerializer.string()); + redisTemplate.setValueSerializer(RedisSerializer.java()); + redisTemplate.setHashValueSerializer(RedisSerializer.java()); + redisTemplate.setConnectionFactory(factory); + return redisTemplate; + } + + @Bean + public HashOperations hashOperations(RedisTemplate redisTemplate) { + return redisTemplate.opsForHash(); + } + + @Bean + public ValueOperations valueOperations(RedisTemplate redisTemplate) { + return redisTemplate.opsForValue(); + } + + @Bean + public ListOperations listOperations(RedisTemplate redisTemplate) { + return redisTemplate.opsForList(); + } + + @Bean + public SetOperations setOperations(RedisTemplate redisTemplate) { + return redisTemplate.opsForSet(); + } + + @Bean + public ZSetOperations zSetOperations(RedisTemplate redisTemplate) { + return redisTemplate.opsForZSet(); + } + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/BaseEntity.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/BaseEntity.java new file mode 100644 index 0000000..c055929 --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/BaseEntity.java @@ -0,0 +1,37 @@ +package com.cloud.kicc.common.data.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + *

+ * 基础模型 + *

+ * + * @Author: wangxiang4 + * @Date: 2021/12/28 + */ +@Data +public class BaseEntity implements Serializable { + + protected static final long serialVersionUID = 1L; + + /** 多租户ID */ + @ApiModelProperty("多租户ID") + protected String tenantId; + + /** 当前用户 */ + @ApiModelProperty("当前用户") + @TableField(exist = false) + protected KiccUser currentUser; + + /** 自定义sql过滤 */ + @TableField(exist = false) + @JsonIgnore + private String sqlFilter; + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/CasUser.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/CasUser.java new file mode 100644 index 0000000..27544ec --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/CasUser.java @@ -0,0 +1,185 @@ +package com.cloud.kicc.common.data.entity; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + *

+ * CAS统一认证用户数据 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/8/16 + */ +@Setter +@Getter +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +public class CasUser extends User { + + private static final long serialVersionUID = 1L; + + /** 用户ID */ + private String id; + + /** 昵称 */ + private String nickName; + + /** 邮箱 */ + private String email; + + /** 手机号 */ + private String phone; + + /** 性别 */ + private String sex; + + /** 头像地址 */ + private String avatar; + + /** 最后登陆ip */ + private String loginIp; + + /** 最后登陆时间 */ + private LocalDateTime loginTime; + + /** 状态 */ + private String ssoStatus; + + /** 创建ID */ + private String ssoCreateById; + + /** 创建人 */ + private String ssoCreateByName; + + /** 创建时间 */ + private LocalDateTime ssoCreateTime; + + /** 更新ID */ + private String ssoUpdateById; + + /** 更新人 */ + private String ssoUpdateByName; + + /** 更新时间 */ + private LocalDateTime ssoUpdateTime; + + /** 备注 */ + private String remarks; + + /** 角色ID */ + private String roleId; + + /** 多租户ID */ + private String tenantId; + + /** sso扩展信息 */ + private Map exPrincipals = new ConcurrentHashMap<>(3); + + + public CasUser(String username, String password, Collection authorities) { + super(username, password, authorities); + } + + public CasUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection authorities) { + super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + } + + public CasUser(String username, + String password, + boolean enabled, + boolean accountNonExpired, + boolean credentialsNonExpired, + boolean accountNonLocked, + Collection authorities, + String id, + String nickName, + String email, + String phone, + String sex, + String avatar, + String loginIp, + LocalDateTime loginTime, + String ssoStatus, + String ssoCreateById, + String ssoCreateByName, + LocalDateTime ssoCreateTime, + String ssoUpdateById, + String ssoUpdateByName, + LocalDateTime ssoUpdateTime, + String remarks) { + super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + this.id = id; + this.nickName = nickName; + this.email = email; + this.phone = phone; + this.sex = sex; + this.avatar = avatar; + this.loginIp = loginIp; + this.loginTime = loginTime; + this.ssoStatus = ssoStatus; + this.ssoCreateById = ssoCreateById; + this.ssoCreateByName = ssoCreateByName; + this.ssoCreateTime = ssoCreateTime; + this.ssoUpdateById = ssoUpdateById; + this.ssoUpdateByName = ssoUpdateByName; + this.ssoUpdateTime = ssoUpdateTime; + this.remarks = remarks; + } + + public CasUser(String username, + String password, + boolean enabled, + boolean accountNonExpired, + boolean credentialsNonExpired, + boolean accountNonLocked, + Collection authorities, + String id, + String nickName, + String email, + String phone, + String sex, + String avatar, + String loginIp, + LocalDateTime loginTime, + String ssoStatus, + String ssoCreateById, + String ssoCreateByName, + LocalDateTime ssoCreateTime, + String ssoUpdateById, + String ssoUpdateByName, + LocalDateTime ssoUpdateTime, + String remarks, + String roleId, + String tenantId) { + super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + this.id = id; + this.nickName = nickName; + this.email = email; + this.phone = phone; + this.sex = sex; + this.avatar = avatar; + this.loginIp = loginIp; + this.loginTime = loginTime; + this.ssoStatus = ssoStatus; + this.ssoCreateById = ssoCreateById; + this.ssoCreateByName = ssoCreateByName; + this.ssoCreateTime = ssoCreateTime; + this.ssoUpdateById = ssoUpdateById; + this.ssoUpdateByName = ssoUpdateByName; + this.ssoUpdateTime = ssoUpdateTime; + this.remarks = remarks; + this.roleId = roleId; + this.tenantId = tenantId; + } + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/CommonEntity.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/CommonEntity.java new file mode 100644 index 0000000..0cff393 --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/CommonEntity.java @@ -0,0 +1,79 @@ +package com.cloud.kicc.common.data.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + *

+ * 通用模型 + *

+ * + * @Author: wangxiang4 + * @Date: 2021/12/28 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CommonEntity extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** 创建id */ + @ApiModelProperty("创建id") + @TableField(value = "create_by_id", fill = FieldFill.INSERT) + protected String createById; + + /** 创建者 */ + @ApiModelProperty("创建人") + @TableField(value = "create_by_name", fill = FieldFill.INSERT) + protected String createByName; + + /** 创建时间 */ + @ApiModelProperty("创建时间") + @TableField(value = "create_time", fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + protected LocalDateTime createTime; + + /** 更新id */ + @ApiModelProperty("更新id") + @TableField(value = "update_by_id", fill = FieldFill.UPDATE) + protected String updateById; + + /** 更新者 */ + @ApiModelProperty("更新者") + @TableField(value = "update_by_name", fill = FieldFill.UPDATE) + protected String updateByName; + + /** 更新时间 */ + @ApiModelProperty("更新时间") + @TableField(value = "update_time", fill = FieldFill.UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + protected LocalDateTime updateTime; + + /** 备注 */ + @ApiModelProperty("备注") + protected String remarks; + + /** 删除标志(0代表存在 1代表删除)*/ + @TableLogic + @JsonIgnore + protected String delFlag; + + /** 开始时间 */ + @TableField(exist = false) + @JsonIgnore + private String beginTime; + + /** 结束时间 */ + @TableField(exist = false) + @JsonIgnore + private String endTime; + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/KiccUser.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/KiccUser.java new file mode 100644 index 0000000..064e6a1 --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/KiccUser.java @@ -0,0 +1,109 @@ +package com.cloud.kicc.common.data.entity; + +import cn.hutool.core.util.ObjectUtil; +import com.cloud.kicc.common.core.constant.SecurityConstants; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.time.LocalDateTime; +import java.util.List; + +/** + *

+ * 扩展用户数据 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/8/16 + */ +@Getter +@Setter +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@ApiModel(description = "完整用户信息") +public class KiccUser extends CasUser { + + @ApiModelProperty("扩展用户ID") + private String id; + + @ApiModelProperty("CAS用户ID") + private String casUserId; + + @ApiModelProperty("用户类型 {@link com.cloud.kicc.system.api.enums.UserTypeEnum }") + private String userType; + + @ApiModelProperty("机构ID") + private String deptId; + + @ApiModelProperty("机构名称") + private String deptName; + + @ApiModelProperty("地图标记点位置图片旋转值") + private Double mapOrientation; + + @ApiModelProperty("地图设计器默认中心点位置") + private String mapCenter; + + @ApiModelProperty("帐号状态(0正常 1停用)") + private String status; + + @ApiModelProperty("指定登录后首页跳转") + private String homePath; + + @ApiModelProperty("角色ID集合") + private String[] roleIds; + + @ApiModelProperty("菜单按钮权限") + private String[] permissions; + + @ApiModelProperty("多租户ID集合") + private String[] tenantIds; + + @ApiModelProperty("创建ID") + private String createById; + + @ApiModelProperty("创建人") + private String createByName; + + @ApiModelProperty("创建时间") + private LocalDateTime createTime; + + @ApiModelProperty("更新ID") + private String updateById; + + @ApiModelProperty("更新人") + private String updateByName; + + @ApiModelProperty("更新时间") + private LocalDateTime updateTime; + + public KiccUser() { + super(SecurityConstants.MOCK_USERNAME, SecurityConstants.MOCK_PASSWORD, AuthorityUtils.createAuthorityList()); + } + + @JsonCreator + public KiccUser(@JsonProperty("username") String username, + @JsonProperty("password") String password, + @JsonProperty("enabled") boolean enabled, + @JsonProperty("accountNonExpired") boolean accountNonExpired, + @JsonProperty("credentialsNonExpired") boolean credentialsNonExpired, + @JsonProperty("accountNonLocked") boolean accountNonLocked, + @JsonProperty("authorities") List authorities) { + super(ObjectUtil.defaultIfBlank(username, SecurityConstants.MOCK_USERNAME), + ObjectUtil.defaultIfBlank(password, SecurityConstants.MOCK_PASSWORD), + enabled, + accountNonExpired, + credentialsNonExpired, + accountNonLocked, + ObjectUtil.defaultIfNull(authorities, AuthorityUtils.createAuthorityList())); + } + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/SsoUser.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/SsoUser.java new file mode 100644 index 0000000..4ce7def --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/SsoUser.java @@ -0,0 +1,66 @@ +package com.cloud.kicc.common.data.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.cloud.kicc.common.data.entity.CommonEntity; +import io.swagger.annotations.ApiModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + *

+ * SSO用户统一表 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/8/6 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("cas_sso_user") +@ApiModel(description = "SSO用户表") +public class SsoUser extends CommonEntity { + + private static final long serialVersionUID = 1L; + + /** 用户ID */ + private String id; + + /** 用户名 */ + private String userName; + + /** 昵称 */ + private String nickName; + + /** 密码 */ + private String password; + + /** 用户邮箱 */ + private String email; + + /** 手机号码 */ + private String phone; + + /** 用户性别(0男 1女 2未知)*/ + private String sex; + + /** 头像路径 */ + private String avatar; + + /** 最后登陆IP */ + private String loginIp; + + /** 帐号状态(0正常 1停用)*/ + private String status; + + /** 最后登陆时间 */ + private LocalDateTime loginTime; + + /** 新密码 */ + @TableField(exist = false) + private String newPassword; +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/TreeEntity.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/TreeEntity.java new file mode 100644 index 0000000..fd7c1d9 --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/entity/TreeEntity.java @@ -0,0 +1,46 @@ +package com.cloud.kicc.common.data.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * 树结构模型 + *

+ * + * @Author: wangxiang4 + * @Date: 2021/12/28 + */ +@Data +public class TreeEntity extends CommonEntity { + + private static final long serialVersionUID = 1L; + + /** 编号 **/ + @ApiModelProperty("编号") + private String id; + + /** 父级编号 **/ + @ApiModelProperty("父级编号") + private String parentId; + + /** 名称 */ + @ApiModelProperty("名称") + protected String name; + + /** 排序 **/ + @ApiModelProperty("排序") + private Integer sort; + + @ApiModelProperty("子级集合") + @TableField(exist = false) + @JsonInclude(JsonInclude.Include.NON_NULL) + protected List children; + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/enums/DataTypeEnum.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/enums/DataTypeEnum.java new file mode 100644 index 0000000..4436b57 --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/enums/DataTypeEnum.java @@ -0,0 +1,52 @@ +package com.cloud.kicc.common.data.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + *

+ * 数据类型 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@Getter +@AllArgsConstructor +public enum DataTypeEnum { + /** + * mysql + */ + MYSQL("mysql", "com.mysql.cj.jdbc.Driver"), + + /** + * sqlserver + */ + SQLSERVER("sqlserver", "com.microsoft.sqlserver.jdbc.SQLServerDriver"), + + /** + * oracle + */ + ORACLE("oracle", "oracle.jdbc.driver.OracleDriver"), + + /** + * Postgresql + */ + POSTGRESQL("postgresql", "org.postgresql.Driver"), + + /** + * sqlite + */ + SQLITE("sqlite", "org.sqlite.JDBC"); + + /** + * 类型 + */ + private final String type; + + /** + * 驱动 + */ + private final String driverClassName; + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/handler/BaseMetaObjectHandler.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/handler/BaseMetaObjectHandler.java new file mode 100644 index 0000000..c1b9b5e --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/handler/BaseMetaObjectHandler.java @@ -0,0 +1,75 @@ +package com.cloud.kicc.common.data.handler; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.cloud.kicc.common.data.entity.CasUser; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.time.LocalDateTime; +import java.util.Optional; + +/** + *

+ * 公共字段自动填充 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +public class BaseMetaObjectHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + Object createTime = getFieldValByName("createTime", metaObject); + if (createTime == null) { + setFieldValByName("createTime", LocalDateTime.now(), metaObject); + } + + if (Optional.ofNullable(getUser()).isPresent()) { + Object createById = getFieldValByName("createById", metaObject); + if (createById == null) { + setFieldValByName("createById", getUser().getId(), metaObject); + } + + Object createByName = getFieldValByName("createByName", metaObject); + if (createByName == null) { + setFieldValByName("createByName", getUser().getUsername(), metaObject); + } + } + } + + @Override + public void updateFill(MetaObject metaObject) { + Object fieldValue = getFieldValByName("updateTime", metaObject); + if (fieldValue == null) { + setFieldValByName("updateTime", LocalDateTime.now(), metaObject); + } + + if (Optional.ofNullable(getUser()).isPresent()) { + Object updateById = getFieldValByName("updateById", metaObject); + if (updateById == null) { + setFieldValByName("updateById", getUser().getId(), metaObject); + } + + Object updateByName = getFieldValByName("updateByName", metaObject); + if (updateByName == null) { + setFieldValByName("updateByName", getUser().getUsername(), metaObject); + } + } + } + + /** + * 获取用户 + */ + protected CasUser getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (Optional.ofNullable(authentication).isPresent()) { + Object principal = authentication.getPrincipal(); + if (principal instanceof CasUser) { + return (CasUser) principal; + } + } + return null; + } +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/handler/KiccTenantLineHandler.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/handler/KiccTenantLineHandler.java new file mode 100644 index 0000000..5573d0a --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/handler/KiccTenantLineHandler.java @@ -0,0 +1,77 @@ +package com.cloud.kicc.common.data.handler; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import com.cloud.kicc.common.data.entity.CasUser; +import com.cloud.kicc.common.data.override.TenantLikeExpression; +import com.cloud.kicc.common.data.properties.TenantProperties; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.List; +import java.util.Optional; + +/** + *

+ * 多租户拦截处理 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/4/2 + */ +public class KiccTenantLineHandler implements TenantLineHandler { + + private TenantProperties tenantProperties; + + public KiccTenantLineHandler(TenantProperties tenantProperties) { + this.tenantProperties = tenantProperties; + } + + /** + * 默认为tenant_id字段,尽量不要去改动,因为会牵扯到实体类中的TenantId字段 + * @return String: 表中的多租户字段 + */ + @Override + public String getTenantIdColumn() { + return TenantLineHandler.super.getTenantIdColumn(); + } + + @Override + public boolean ignoreTable(String tableName) { + return tenantProperties.getExclusionTable().contains(tableName); + } + + /** + * 新增数据字段中存在多租户字段是否忽略此多租户字段拼接新增 + * @Param columns: 当前新增表所有字段 + * @Param tenantIdColumn: 多租户字段 + * @return boolean: 是否忽略此多租户字段拼接新增 + */ + @Override + public boolean ignoreInsert(List columns, String tenantIdColumn) { + return TenantLineHandler.super.ignoreInsert(columns, tenantIdColumn); + } + + @Override + public Expression getTenantId() { + // 返回当前用户所属的多租户ID进行条件拼接 + return ObjectUtil.isNotEmpty(getUser()) ? new TenantLikeExpression(getUser().getTenantId()): null; + } + + /** + * 获取用户 + */ + protected CasUser getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (Optional.ofNullable(authentication).isPresent()) { + Object principal = authentication.getPrincipal(); + if (principal instanceof CasUser) { + return (CasUser) principal; + } + } + return null; + } + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/override/TenantLikeExpression.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/override/TenantLikeExpression.java new file mode 100644 index 0000000..53658d6 --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/override/TenantLikeExpression.java @@ -0,0 +1,70 @@ +package com.cloud.kicc.common.data.override; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +import java.util.Objects; + +/** + *

+ * 重写StringValue,支持多租户like拼接查询 + * 由于内部StringExpression会拼接默认会加'' + * 而多租户like条件经过处理不许需要加'',如果加上会导致数据查不出 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/5/11 + */ +public class TenantLikeExpression extends ASTNodeAccessImpl implements Expression { + + private String value = ""; + + public TenantLikeExpression() { + } + + public TenantLikeExpression(String escapedValue) { + this.value = escapedValue; + } + + public String getValue() { + return this.value; + } + + + public void setValue(String string) { + this.value = string; + } + + + @Override + public void accept(ExpressionVisitor expressionVisitor) { + } + + @Override + public String toString() { + return this.value; + } + + public TenantLikeExpression withValue(String value) { + this.setValue(value); + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (o != null && this.getClass() == o.getClass()) { + TenantLikeExpression that = (TenantLikeExpression)o; + return Objects.equals(this.value, that.value); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(new Object[]{this.value}); + } +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/plugins/KiccPaginationInnerInterceptor.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/plugins/KiccPaginationInnerInterceptor.java new file mode 100644 index 0000000..7790214 --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/plugins/KiccPaginationInnerInterceptor.java @@ -0,0 +1,38 @@ +package com.cloud.kicc.common.data.plugins; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.ParameterUtils; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.SQLException; + +/** + *

+ * 分页拦截器 + * 重构分页插件, 当 size 小于 0 时, 直接设置为 0, 防止错误查询全表 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +public class KiccPaginationInnerInterceptor extends PaginationInnerInterceptor { + + @Override + public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, + ResultHandler resultHandler, BoundSql boundSql) throws SQLException { + IPage page = ParameterUtils.findPage(parameter).orElse(null); + // size 小于 0 直接设置为 0 , 即不查询任何数据 + if (null != page && page.getSize() < 0) { + page.setSize(0); + } + super.beforeQuery(executor, ms, page, rowBounds, resultHandler, boundSql); + } + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/plugins/KiccTenantLineInnerInterceptor.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/plugins/KiccTenantLineInnerInterceptor.java new file mode 100644 index 0000000..4136ca6 --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/plugins/KiccTenantLineInnerInterceptor.java @@ -0,0 +1,209 @@ +package com.cloud.kicc.common.data.plugins; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import com.cloud.kicc.common.core.exception.CheckedException; +import com.cloud.kicc.common.data.override.TenantLikeExpression; +import lombok.NoArgsConstructor; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Parenthesis; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.ItemsList; +import net.sf.jsqlparser.expression.operators.relational.LikeExpression; +import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.update.Update; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * 租户线路内部拦截器 + * 重构租户线路内部拦截器,支持查询多个多租户ID,还支持查询多个租户的共享数据,默认只支持一个多租户id查询 + * 支持多租户ID不存在时,查询所有租户ID数据 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@NoArgsConstructor +public class KiccTenantLineInnerInterceptor extends TenantLineInnerInterceptor { + + private TenantLineHandler tenantLineHandler; + + public KiccTenantLineInnerInterceptor(final TenantLineHandler tenantLineHandler) { + super(tenantLineHandler); + this.tenantLineHandler = tenantLineHandler; + } + + @Override + public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { + if (ObjectUtil.isNotEmpty(tenantLineHandler.getTenantId())){ + super.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql); + } + } + + @Override + public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { + if (ObjectUtil.isNotEmpty(tenantLineHandler.getTenantId())) { + super.beforePrepare(sh, connection, transactionTimeout); + } + } + + @Override + protected void processInsert(Insert insert, int index, String sql, Object obj) { + if (!this.tenantLineHandler.ignoreTable(insert.getTable().getName())) { + List columns = insert.getColumns(); + if (!CollectionUtils.isEmpty(columns)) { + String tenantIdColumn = this.tenantLineHandler.getTenantIdColumn(); + if (!this.tenantLineHandler.ignoreInsert(columns, tenantIdColumn)) { + columns.add(new Column(tenantIdColumn)); + + List duplicateUpdateColumns = insert.getDuplicateUpdateExpressionList(); + if (CollectionUtils.isNotEmpty(duplicateUpdateColumns)) { + // 替换likeExpression支持查询多个租户ID的条件,包括查询数据对应多个多租户ID的数据 + List tenantIds = StrUtil.split(this.tenantLineHandler.getTenantId().toString(), ","); + StringBuilder statementBuilder = new StringBuilder(); + tenantIds.forEach(tenantId -> { + LikeExpression likeExpression = new LikeExpression(); + likeExpression.setLeftExpression(new Column(tenantIdColumn)); + likeExpression.setRightExpression(new StringValue("%" + tenantId + "%")); + statementBuilder.append(likeExpression + " OR "); + }); + if (statementBuilder.length() == 0) { + throw new CheckedException("当前用户没有分配租户"); + } + statementBuilder.delete(statementBuilder.length()-4, statementBuilder.length()); + TenantLikeExpression tenantLikeExpression = new TenantLikeExpression(statementBuilder.toString()); + Parenthesis parenthesis = new Parenthesis(tenantLikeExpression); + duplicateUpdateColumns.add(parenthesis); + } + + Select select = insert.getSelect(); + if (select != null) { + this.processInsertSelect(select.getSelectBody()); + } else { + if (insert.getItemsList() == null) { + throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId", new Object[0]); + } + + ItemsList itemsList = insert.getItemsList(); + if (itemsList instanceof MultiExpressionList) { + ((MultiExpressionList)itemsList).getExpressionLists().forEach((el) -> { + el.getExpressions().add(new StringValue(this.tenantLineHandler.getTenantId().toString())); + }); + } else { + ((ExpressionList)itemsList).getExpressions().add(new StringValue(this.tenantLineHandler.getTenantId().toString())); + } + } + + } + } + } + } + + @Override + protected void processUpdate(Update update, int index, String sql, Object obj) { + Table table = update.getTable(); + if (!this.tenantLineHandler.ignoreTable(table.getName())) { + update.setWhere(this.andLikeExpression(table, update.getWhere())); + } + } + + @Override + protected void processDelete(Delete delete, int index, String sql, Object obj) { + if (!this.tenantLineHandler.ignoreTable(delete.getTable().getName())) { + delete.setWhere(this.andLikeExpression(delete.getTable(), delete.getWhere())); + } + } + + /** 重写andExpression表达式,支持like查询多个参数 */ + protected Expression andLikeExpression(Table table, Expression where) { + // 替换likeExpression支持查询多个租户ID的条件,包括查询数据对应多个多租户ID的数据 + List tenantIds = StrUtil.split(this.tenantLineHandler.getTenantId().toString(), ","); + StringBuilder statementBuilder = new StringBuilder(); + tenantIds.forEach(tenantId -> { + LikeExpression likeExpression = new LikeExpression(); + likeExpression.setLeftExpression(this.getAliasColumn(table)); + likeExpression.setRightExpression(new StringValue("%" + tenantId + "%")); + statementBuilder.append(likeExpression + " OR "); + }); + if (statementBuilder.length() == 0) { + throw new CheckedException("当前用户没有分配租户"); + } + statementBuilder.delete(statementBuilder.length()-4, statementBuilder.length()); + TenantLikeExpression tenantLikeExpression = new TenantLikeExpression(statementBuilder.toString()); + Parenthesis parenthesis = new Parenthesis(tenantLikeExpression); + if (null != where) { + return where instanceof OrExpression ? new AndExpression(parenthesis, new Parenthesis(where)) : new AndExpression(parenthesis, where); + } else { + return parenthesis; + } + } + + @Override + protected Expression builderExpression(Expression currentExpression, List tables) { + if (CollectionUtils.isEmpty(tables)) { + return currentExpression; + } else { + // 替换likeExpression支持查询多个租户ID的条件,包括查询数据对应多个多租户ID的数据 + List likeParenthesisExpressions = tables.stream() + .filter(x -> !this.tenantLineHandler.ignoreTable(x.getName())) + .map(item -> { + List tenantIds = StrUtil.split(this.tenantLineHandler.getTenantId().toString(), ","); + StringBuilder statementBuilder = new StringBuilder(); + tenantIds.forEach(tenantId -> { + LikeExpression likeExpression = new LikeExpression(); + likeExpression.setLeftExpression(this.getAliasColumn(item)); + likeExpression.setRightExpression(new StringValue("%" + tenantId + "%")); + statementBuilder.append(likeExpression + " OR "); + }); + if (statementBuilder.length() == 0) { + throw new CheckedException("当前用户没有分配租户"); + } + statementBuilder.delete(statementBuilder.length()-4, statementBuilder.length()); + TenantLikeExpression tenantLikeExpression = new TenantLikeExpression(statementBuilder.toString()); + Parenthesis parenthesis = new Parenthesis(tenantLikeExpression); + return parenthesis; + }).collect(Collectors.toList()); + + if (CollectionUtils.isEmpty(likeParenthesisExpressions)) { + return currentExpression; + } else { + Expression injectExpression = likeParenthesisExpressions.get(0); + if (likeParenthesisExpressions.size() > 1) { + for(int i = 1; i < likeParenthesisExpressions.size(); ++i) { + injectExpression = new AndExpression(injectExpression, likeParenthesisExpressions.get(i)); + } + } + + if (currentExpression == null) { + return injectExpression; + } else { + return currentExpression instanceof OrExpression ? new AndExpression(new Parenthesis(currentExpression), injectExpression) : new AndExpression(currentExpression, injectExpression); + } + } + } + } + +} diff --git a/kicc-common-data/src/main/java/com/cloud/kicc/common/data/properties/TenantProperties.java b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/properties/TenantProperties.java new file mode 100644 index 0000000..8a866be --- /dev/null +++ b/kicc-common-data/src/main/java/com/cloud/kicc/common/data/properties/TenantProperties.java @@ -0,0 +1,24 @@ +package com.cloud.kicc.common.data.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +/** + *

+ * 多租户配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/4/2+ + */ +@Data +@ConfigurationProperties(prefix = "tenant") +public class TenantProperties { + + private Boolean enableInsert; + + private List exclusionTable; + +} diff --git a/kicc-common-data/src/main/resources/META-INF/spring.factories b/kicc-common-data/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ab43764 --- /dev/null +++ b/kicc-common-data/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration= diff --git a/kicc-common-data/src/main/resources/kicc-tenant.yml b/kicc-common-data/src/main/resources/kicc-tenant.yml new file mode 100644 index 0000000..c1d329e --- /dev/null +++ b/kicc-common-data/src/main/resources/kicc-tenant.yml @@ -0,0 +1,4 @@ +tenant: + # 需要排除的多租户的表 + exclusionTable: + - sys_tenant diff --git a/kicc-common-datasource/pom.xml b/kicc-common-datasource/pom.xml new file mode 100644 index 0000000..b1566e0 --- /dev/null +++ b/kicc-common-datasource/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-datasource + jar + + kicc 动态切换数据源组件 + + + + + com.cloud + kicc-common-data + + + + com.baomidou + dynamic-datasource-spring-boot-starter + ${dynamic-ds.version} + + + org.apache.commons + commons-collections4 + ${apache.collections4.version} + + + diff --git a/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/DynamicDataSourceConfiguration.java b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/DynamicDataSourceConfiguration.java new file mode 100644 index 0000000..6e787a7 --- /dev/null +++ b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/DynamicDataSourceConfiguration.java @@ -0,0 +1,53 @@ +package com.cloud.kicc.common.datasource; + +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator; +import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import com.cloud.kicc.common.datasource.dynamic.DynamicDataSourceJdbcProvider; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +/** + *

+ * 动态数据源切换配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +@Configuration(proxyBeanMethods = false) +public class DynamicDataSourceConfiguration { + + @Bean + public DynamicDataSourceProvider dynamicDataSourceProvider(DataSourceProperties dataSourceProperties, DynamicDataSourceProperties dynamicDataSourceProperties) { + String driverClassName = dataSourceProperties.getDriverClassName(); + String url = dataSourceProperties.getUrl(); + String username = dataSourceProperties.getUsername(); + String password = dataSourceProperties.getPassword(); + DataSourceProperty master = dynamicDataSourceProperties.getDatasource().get(dynamicDataSourceProperties.getPrimary()); + if (master != null) { + driverClassName = master.getDriverClassName(); + url = master.getUrl(); + username = master.getUsername(); + password = master.getPassword(); + } + return new DynamicDataSourceJdbcProvider(dynamicDataSourceProperties, driverClassName, url, username, password); + } + + @Bean + public DataSource dataSource(DynamicDataSourceProperties dynamicDataSourceProperties) { + DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(); + dataSource.setPrimary(dynamicDataSourceProperties.getPrimary()); + dataSource.setStrict(dynamicDataSourceProperties.getStrict()); + dataSource.setStrategy(dynamicDataSourceProperties.getStrategy()); + dataSource.setP6spy(dynamicDataSourceProperties.getP6spy()); + dataSource.setSeata(dynamicDataSourceProperties.getSeata()); + return dataSource; + } + +} diff --git a/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/annotation/EnableDynamicDataSource.java b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/annotation/EnableDynamicDataSource.java new file mode 100644 index 0000000..7f92cd7 --- /dev/null +++ b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/annotation/EnableDynamicDataSource.java @@ -0,0 +1,23 @@ +package com.cloud.kicc.common.datasource.annotation; + +import com.cloud.kicc.common.datasource.DynamicDataSourceConfiguration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + *

+ * 开启动态数据源 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(DynamicDataSourceConfiguration.class) +public @interface EnableDynamicDataSource { + +} diff --git a/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/dynamic/DynamicDataSource.java b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/dynamic/DynamicDataSource.java new file mode 100644 index 0000000..9b0153b --- /dev/null +++ b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/dynamic/DynamicDataSource.java @@ -0,0 +1,45 @@ +package com.cloud.kicc.common.datasource.dynamic; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 动态数据源 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/7/3 + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +public class DynamicDataSource { + + /** + * 数据源ID + */ + private String id; + + /** + * 驱动类 + */ + private String driverClass; + + /** + * 数据库链接 + */ + private String url; + + /** + * 数据库账号名 + */ + private String username; + + /** + * 数据库密码 + */ + private String password; + +} diff --git a/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/dynamic/DynamicDataSourceJdbcProvider.java b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/dynamic/DynamicDataSourceJdbcProvider.java new file mode 100644 index 0000000..313dc25 --- /dev/null +++ b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/dynamic/DynamicDataSourceJdbcProvider.java @@ -0,0 +1,91 @@ +package com.cloud.kicc.common.datasource.dynamic; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.dynamic.datasource.provider.AbstractJdbcDataSourceProvider; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import com.cloud.kicc.common.datasource.support.DynamicDataSourceConstant; +import com.cloud.kicc.common.datasource.util.ConnUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Map; + +/** + *

+ * 动态数据源初始加载 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/7/3 + */ +public class DynamicDataSourceJdbcProvider extends AbstractJdbcDataSourceProvider { + + private final String driverClassName; + private final String url; + private final String username; + private final String password; + private final DynamicDataSourceProperties dynamicDataSourceProperties; + + public DynamicDataSourceJdbcProvider(DynamicDataSourceProperties dynamicDataSourceProperties, + String driverClassName, + String url, + String username, + String password) { + super(driverClassName, url, username, password); + this.dynamicDataSourceProperties = dynamicDataSourceProperties; + this.driverClassName = driverClassName; + this.url = url; + this.username = username; + this.password = password; + } + + @Override + protected Map executeStmt(Statement statement) throws SQLException { + // 构建数据源集合 + Map map = new HashMap<>(16); + // 构建主数据源 + DataSourceProperty masterProperty = new DataSourceProperty(); + masterProperty.setDriverClassName(driverClassName); + masterProperty.setUrl(url); + masterProperty.setUsername(username); + masterProperty.setPassword(password); + map.put(dynamicDataSourceProperties.getPrimary(), masterProperty); + // 构建yml数据源 + Map datasource = dynamicDataSourceProperties.getDatasource(); + if (!datasource.isEmpty()) { + datasource.remove(dynamicDataSourceProperties.getPrimary()); + map.putAll(datasource); + } + // 构建动态数据源 + ResultSet rs = statement.executeQuery(DynamicDataSourceConstant.DYNAMIC_DATASOURCE_GROUP_STATEMENT); + while (rs.next()) { + String id = rs.getString("id"); + String name = rs.getString("name"); + String driver = rs.getString("driverClass"); + String url = rs.getString("url"); + String username = rs.getString("username"); + String password = rs.getString("password"); + try { + if (StrUtil.isAllNotBlank(id, driver, url, username, password)) { + // 测试链接是否生效 + Boolean result = ConnUtil.dbTest(driver, url, username, password); + if (result) { + DataSourceProperty jdbcProperty = new DataSourceProperty(); + // 设置SQL链接 + jdbcProperty.setDriverClassName(driver); + jdbcProperty.setUrl(url); + jdbcProperty.setUsername(username); + jdbcProperty.setPassword(password); + map.put(name, jdbcProperty); + } + } + } catch (Exception e) { + System.err.printf("数据源:"+ name + "初始化失败!"); + } + } + return map; + } +} diff --git a/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/support/DynamicDataSourceConstant.java b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/support/DynamicDataSourceConstant.java new file mode 100644 index 0000000..e098539 --- /dev/null +++ b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/support/DynamicDataSourceConstant.java @@ -0,0 +1,48 @@ +package com.cloud.kicc.common.datasource.support; + +/** + *

+ * 数据源常量 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/7/3 + */ +public interface DynamicDataSourceConstant { + + /** + * 数据源查询基础 + */ + String DYNAMIC_DATASOURCE_BASE_STATEMENT = "SELECT id, name, driver_class as driverClass, url, username, password FROM sys_datasource"; + + /** + * 数据源查询SQL + */ + String DYNAMIC_DATASOURCE_SINGLE_STATEMENT = DYNAMIC_DATASOURCE_BASE_STATEMENT + " WHERE del_flag = 0 AND id = ?"; + + /** + * 数据源查询SQL + */ + String DYNAMIC_DATASOURCE_GROUP_STATEMENT = DYNAMIC_DATASOURCE_BASE_STATEMENT + " WHERE del_flag = 0"; + + /** + * 数据源错误提示 + */ + String DYNAMIC_DATASOURCE_NOT_FOUND = "数据源信息有误,数据加载失败"; + + /** + * oracle驱动类 + */ + String ORACLE_DRIVER_CLASS = "oracle.jdbc.OracleDriver"; + + /** + * oracle校验 + */ + String ORACLE_VALIDATE_STATEMENT = "select 1 from dual"; + + /** + * 通用校验 + */ + String COMMON_VALIDATE_STATEMENT = "select 1"; + +} diff --git a/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/util/ConnUtil.java b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/util/ConnUtil.java new file mode 100644 index 0000000..837daf0 --- /dev/null +++ b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/util/ConnUtil.java @@ -0,0 +1,52 @@ +package com.cloud.kicc.common.datasource.util; + +import lombok.SneakyThrows; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +/** + *

+ * 数据库工具类 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/7/3 + */ +public class ConnUtil { + + /** + * 测试数据库链接 + */ + @SneakyThrows + public static Boolean dbTest(String driverClass, String url, String username, String password) { + Connection conn = null; + try { + //测试驱动类 + Class.forName(driverClass); + //创建连接 + conn = DriverManager.getConnection(url, username, password); + conn.setAutoCommit(Boolean.FALSE); + return true; + } finally { + //关闭连接 + dbClose(conn); + } + } + + /** + * 关闭数据库链接 + */ + private static void dbClose(Connection conn) { + try { + //关闭数据源连接 + if (conn != null) { + conn.close(); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + +} diff --git a/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/util/DynamicDataSourceUtil.java b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/util/DynamicDataSourceUtil.java new file mode 100644 index 0000000..1a5ada8 --- /dev/null +++ b/kicc-common-datasource/src/main/java/com/cloud/kicc/common/datasource/util/DynamicDataSourceUtil.java @@ -0,0 +1,112 @@ +package com.cloud.kicc.common.datasource.util; + +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; +import com.cloud.kicc.common.datasource.dynamic.DynamicDataSource; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.map.LRUMap; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + + +/** + *

+ * 动态数据源核心处理工具 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/7/3 + */ +@Setter +@Slf4j +@Component +public class DynamicDataSourceUtil { + + public static DynamicRoutingDataSource dynamicRoutingDataSource; + public static DynamicDataSourceProperties dynamicDataSourceProperties; + private static DefaultDataSourceCreator defaultDataSourceCreator; + private static int MAX_DATASOURCE_COUNT = 300; + // 最多保存三百个数据源,按使用率淘汰 + private static final LRUMap linksProperties = new LRUMap(MAX_DATASOURCE_COUNT); + + public DynamicDataSourceUtil(DataSource dynamicRoutingDataSource, + DynamicDataSourceProperties dynamicDataSourceProperties, + DefaultDataSourceCreator defaultDataSourceCreator) { + DynamicDataSourceUtil.dynamicRoutingDataSource = (DynamicRoutingDataSource) dynamicRoutingDataSource; + DynamicDataSourceUtil.dynamicDataSourceProperties = dynamicDataSourceProperties; + DynamicDataSourceUtil.defaultDataSourceCreator = defaultDataSourceCreator; + } + + /** + * 创建并切换至远程数据源 + * @param dynamicDataSource 切换数据源 + */ + public static void switchToDataSource(DynamicDataSource dynamicDataSource) { + String dbKey = dynamicDataSource.getId(); + String removeKey = null; + boolean insert = true; + if (dynamicRoutingDataSource.getDataSources().containsKey(dynamicDataSource.getId())) { + synchronized (linksProperties) { + if (linksProperties.get(dbKey).equals(dynamicDataSource)) { + insert = false; + } + } + } + if (insert) { + // 创建数据源配置 + DataSourceProperty dataSourceProperty = new DataSourceProperty(); + // 拷贝数据源配置 + BeanUtils.copyProperties(dynamicDataSource, dataSourceProperty); + // 创建动态数据源 + DataSource dataSource = defaultDataSourceCreator.createDataSource(dataSourceProperty); + // 添加最新数据源 + dynamicRoutingDataSource.addDataSource(dbKey, dataSource); + synchronized (linksProperties) { + if (linksProperties.size() == MAX_DATASOURCE_COUNT) { + removeKey = linksProperties.firstKey(); + } + linksProperties.put(dbKey, dynamicDataSource); + } + } + // 切换数据源 + DynamicDataSourceContextHolder.push(dbKey); + if (removeKey != null) { + try { + dynamicRoutingDataSource.removeDataSource(removeKey); + } catch (Exception e) { + log.error("移除数据源失败:{}", e.getMessage()); + } + } + } + + /** + * 移除当前设置的远程数据源,清除上次清除之后切换的所有数据源 + * 需要先调用 switchToDataSource切换数据源 + */ + public static void clearSwitchDataSource() { + DynamicDataSourceContextHolder.poll(); + } + + /** + * 获取当前数据源的数据链接(切库后的) + * 用完之后一定要关闭 + * @return + * @throws SQLException + */ + public static Connection getCurrentConnection() throws SQLException { + return dynamicRoutingDataSource.getConnection(); + } + + public static boolean containsLink(String key) { + return linksProperties.containsKey(key); + } + +} diff --git a/kicc-common-datasource/src/main/resources/META-INF/spring.factories b/kicc-common-datasource/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..2e33d9d --- /dev/null +++ b/kicc-common-datasource/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.cloud.kicc.common.datasource.util.DynamicDataSourceUtil diff --git a/kicc-common-feign/pom.xml b/kicc-common-feign/pom.xml new file mode 100644 index 0000000..0416d77 --- /dev/null +++ b/kicc-common-feign/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-feign + jar + + feign-sentinel服务降级熔断、限流组件 + + + + + com.cloud + kicc-common-core + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + io.github.openfeign + feign-okhttp + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + com.github.ben-manes.caffeine + caffeine + + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + + + diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/KiccFeignAutoConfiguration.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/KiccFeignAutoConfiguration.java new file mode 100644 index 0000000..c6a1061 --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/KiccFeignAutoConfiguration.java @@ -0,0 +1,49 @@ +package com.cloud.kicc.common.feign; + +import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; +import com.cloud.kicc.common.feign.sentinel.ext.KiccSentinelFeign; +import com.cloud.kicc.common.feign.sentinel.handle.KiccUrlBlockHandler; +import com.cloud.kicc.common.feign.sentinel.parser.KiccHeaderRequestOriginParser; +import feign.Feign; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +/** + *

+ * sentinel 配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureBefore(SentinelFeignAutoConfiguration.class) +public class KiccFeignAutoConfiguration { + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "feign.sentinel.enabled") + public Feign.Builder feignSentinelBuilder() { + return KiccSentinelFeign.builder(); + } + + @Bean + @ConditionalOnMissingBean + public BlockExceptionHandler blockExceptionHandler() { + return new KiccUrlBlockHandler(); + } + + @Bean + @ConditionalOnMissingBean + public RequestOriginParser requestOriginParser() { + return new KiccHeaderRequestOriginParser(); + } + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/annotation/EnableKiccFeignClients.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/annotation/EnableKiccFeignClients.java new file mode 100644 index 0000000..b9eb0e2 --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/annotation/EnableKiccFeignClients.java @@ -0,0 +1,72 @@ +package com.cloud.kicc.common.feign.annotation; + +import com.cloud.kicc.common.feign.config.FeignErrorDecoder; +import com.cloud.kicc.common.feign.config.KiccFeignClientConfiguration; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClientsConfiguration; +import org.springframework.cloud.openfeign.KiccFeignClientsRegistrar; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + *

+ * 扩展Feign请求接口支持自动熔断降级 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnableFeignClients +@Import({ KiccFeignClientsRegistrar.class, KiccFeignClientConfiguration.class }) +public @interface EnableKiccFeignClients { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation + * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of + * {@code @ComponentScan(basePackages="org.my.pkg")}. + * @return the array of 'basePackages'. + */ + String[] value() default {}; + + /** + * Base packages to scan for annotated components. + *

+ * {@link #value()} is an alias for (and mutually exclusive with) this attribute. + *

+ * Use {@link #basePackageClasses()} for a type-safe alternative to String-based + * package names. + * @return the array of 'basePackages'. + */ + String[] basePackages() default { "com.cloud.kicc" }; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for annotated components. The package of each class specified will be scanned. + *

+ * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + * @return the array of 'basePackageClasses'. + */ + Class[] basePackageClasses() default {}; + + /** + * A custom @Configuration for all feign clients. Can contain override + * @Bean definition for the pieces that make up the client, for instance + * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. + * + * @see FeignClientsConfiguration for the defaults + */ + Class[] defaultConfiguration() default { FeignErrorDecoder.class }; + + /** + * List of classes annotated with @FeignClient. If not empty, disables classpath + * scanning. + * @return + */ + Class[] clients() default {}; + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/config/FeignErrorDecoder.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/config/FeignErrorDecoder.java new file mode 100644 index 0000000..64acbd7 --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/config/FeignErrorDecoder.java @@ -0,0 +1,50 @@ +package com.cloud.kicc.common.feign.config; + +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; +import com.cloud.kicc.common.core.api.R; +import feign.FeignException; +import feign.Response; +import feign.RetryableException; +import feign.codec.ErrorDecoder; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + *

+ * 自定义feign错误响应数据 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/5/16 + */ +@Slf4j +@Configuration +public class FeignErrorDecoder extends ErrorDecoder.Default { + + @Override + @SneakyThrows + public Exception decode(String methodKey, Response response) { + Exception exception = super.decode(methodKey, response); + // 如果是RetryableException,则返回继续重试 + if (exception instanceof RetryableException) { + return exception; + } + try { + // 如果是FeignException,则对其进行处理,并抛出Exception + if (exception instanceof FeignException && ((FeignException) exception).responseBody().isPresent()) { + ByteBuffer responseBody = ((FeignException) exception).responseBody().get(); + String bodyText = StandardCharsets.UTF_8.newDecoder().decode(responseBody.asReadOnlyBuffer()).toString(); + return new Exception(JSONUtil.isJson(bodyText) ? JSONUtil.toBean(bodyText, R.class).getMsg() : bodyText); + } + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + } + return exception; + } + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/config/KiccFeignClientConfiguration.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/config/KiccFeignClientConfiguration.java new file mode 100644 index 0000000..b4f0199 --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/config/KiccFeignClientConfiguration.java @@ -0,0 +1,26 @@ +package com.cloud.kicc.common.feign.config; + +import feign.RequestInterceptor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.commons.security.AccessTokenContextRelay; +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; + +/** + *

+ * 拦截器传递 header 中oauth token,使用hystrix 的信号量模式 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@ConditionalOnProperty("security.oauth2.client.client-id") +public class KiccFeignClientConfiguration { + + @Bean + public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails resource, AccessTokenContextRelay accessTokenContextRelay) { + return new KiccFeignClientInterceptor(oAuth2ClientContext, resource, accessTokenContextRelay); + } + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/config/KiccFeignClientInterceptor.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/config/KiccFeignClientInterceptor.java new file mode 100644 index 0000000..bdc37df --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/config/KiccFeignClientInterceptor.java @@ -0,0 +1,61 @@ +package com.cloud.kicc.common.feign.config; + +import cn.hutool.core.collection.CollUtil; +import com.cloud.kicc.common.core.constant.SecurityConstants; +import feign.RequestTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.commons.security.AccessTokenContextRelay; +import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; + +import java.util.Collection; + +/** + *

+ * 扩展OAuth2FeignRequestInterceptor + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +public class KiccFeignClientInterceptor extends OAuth2FeignRequestInterceptor { + + private final OAuth2ClientContext oAuth2ClientContext; + + private final AccessTokenContextRelay accessTokenContextRelay; + + /** + * 在授权标头中使用提供的 OAuth2ClientContext 和 Bearer 令牌的默认构造函数 + * @param oAuth2ClientContext provided context + * @param resource type of resource to be accessed + * @param accessTokenContextRelay + */ + public KiccFeignClientInterceptor(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails resource, + AccessTokenContextRelay accessTokenContextRelay) { + super(oAuth2ClientContext, resource); + this.oAuth2ClientContext = oAuth2ClientContext; + this.accessTokenContextRelay = accessTokenContextRelay; + } + + /** + * 使用提供的名称和提取的提取物的标题创建模板 + * 1. 如果使用 非web 请求,header 区别 + * 2. 根据authentication 还原请求token + * @param template + */ + @Override + public void apply(RequestTemplate template) { + Collection fromHeader = template.headers().get(SecurityConstants.FROM); + if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) { + return; + } + // 进行token中转,传递token + accessTokenContextRelay.copyToken(); + if (oAuth2ClientContext != null && oAuth2ClientContext.getAccessToken() != null) { + super.apply(template); + } + } + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/SentinelAutoConfiguration.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/SentinelAutoConfiguration.java new file mode 100644 index 0000000..22ad4a6 --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/SentinelAutoConfiguration.java @@ -0,0 +1,52 @@ +package com.cloud.kicc.common.feign.sentinel; + +import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; +import com.cloud.kicc.common.feign.sentinel.ext.KiccSentinelFeign; +import com.cloud.kicc.common.feign.sentinel.ext.KiccSentinelFilterConfiguration; +import com.cloud.kicc.common.feign.sentinel.handle.KiccUrlBlockHandler; +import com.cloud.kicc.common.feign.sentinel.parser.KiccHeaderRequestOriginParser; +import feign.Feign; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Scope; + +/** + *

+ * sentinel 配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Configuration(proxyBeanMethods = false) +@Import(KiccSentinelFilterConfiguration.class) +@AutoConfigureBefore(SentinelFeignAutoConfiguration.class) +public class SentinelAutoConfiguration { + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "feign.sentinel.enabled") + public Feign.Builder feignSentinelBuilder() { + return KiccSentinelFeign.builder(); + } + + @Bean + @ConditionalOnMissingBean + public BlockExceptionHandler blockExceptionHandler() { + return new KiccUrlBlockHandler(); + } + + @Bean + @ConditionalOnMissingBean + public RequestOriginParser requestOriginParser() { + return new KiccHeaderRequestOriginParser(); + } + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/ext/KiccSentinelFeign.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/ext/KiccSentinelFeign.java new file mode 100644 index 0000000..c4f532f --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/ext/KiccSentinelFeign.java @@ -0,0 +1,132 @@ +package com.cloud.kicc.common.feign.sentinel.ext; + +import com.alibaba.cloud.sentinel.feign.SentinelContractHolder; +import feign.Contract; +import feign.Feign; +import feign.InvocationHandlerFactory; +import feign.Target; +import org.springframework.beans.BeansException; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +/** + *

+ * 支持自动降级注入 重写 {@link com.alibaba.cloud.sentinel.feign.SentinelFeign} + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public final class KiccSentinelFeign { + + private KiccSentinelFeign() { + + } + + public static KiccSentinelFeign.Builder builder() { + return new KiccSentinelFeign.Builder(); + } + + public static final class Builder extends Feign.Builder implements ApplicationContextAware { + + private Contract contract = new Contract.Default(); + + private ApplicationContext applicationContext; + + private FeignContext feignContext; + + @Override + public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) { + throw new UnsupportedOperationException(); + } + + @Override + public KiccSentinelFeign.Builder contract(Contract contract) { + this.contract = contract; + return this; + } + + @Override + public Feign build() { + super.invocationHandlerFactory(new InvocationHandlerFactory() { + @Override + public InvocationHandler create(Target target, Map dispatch) { + + // 查找 FeignClient 上的降级策略 + FeignClient feignClient = AnnotationUtils.findAnnotation(target.type(), FeignClient.class); + Class fallback = feignClient.fallback(); + Class fallbackFactory = feignClient.fallbackFactory(); + + // 查找 FeignClient 上的上下文唯一ID,也就是是注入的bean名称 + String beanName = feignClient.contextId(); + if (!StringUtils.hasText(beanName)) { + beanName = feignClient.name(); + } + + // 开始处理自动降级 + Object fallbackInstance; + FallbackFactory fallbackFactoryInstance; + if (void.class != fallback) { + fallbackInstance = getFromContext(beanName, "fallback", fallback, target.type()); + return new KiccSentinelInvocationHandler(target, dispatch, new FallbackFactory.Default(fallbackInstance)); + } + + if (void.class != fallbackFactory) { + fallbackFactoryInstance = (FallbackFactory) getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class); + return new KiccSentinelInvocationHandler(target, dispatch, fallbackFactoryInstance); + } + return new KiccSentinelInvocationHandler(target, dispatch); + } + + private Object getFromContext(String name, String type, Class fallbackType, Class targetType) { + Object fallbackInstance = feignContext.getInstance(name, fallbackType); + if (fallbackInstance == null) { + throw new IllegalStateException(String.format( + "No %s instance of type %s found for feign client %s", type, fallbackType, name)); + } + + if (!targetType.isAssignableFrom(fallbackType)) { + throw new IllegalStateException(String.format( + "Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", + type, fallbackType, targetType, name)); + } + return fallbackInstance; + } + }); + + super.contract(new SentinelContractHolder(contract)); + return super.build(); + } + + private Object getFieldValue(Object instance, String fieldName) { + Field field = ReflectionUtils.findField(instance.getClass(), fieldName); + field.setAccessible(true); + try { + return field.get(instance); + } + catch (IllegalAccessException e) { + // ignore + } + return null; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + feignContext = this.applicationContext.getBean(FeignContext.class); + } + + } + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/ext/KiccSentinelFilterConfiguration.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/ext/KiccSentinelFilterConfiguration.java new file mode 100644 index 0000000..8f3e690 --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/ext/KiccSentinelFilterConfiguration.java @@ -0,0 +1,59 @@ +package com.cloud.kicc.common.feign.sentinel.ext; + +import com.alibaba.cloud.sentinel.SentinelProperties; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.util.StringUtils; + +import java.util.Optional; + +/** + *

+ * 避免哨兵拦截请求 与 spring cloud 2021 不兼容 的问题 + * 重新注入Sentinel拦截核心Bean + * 会出现:The dependencies of some of the beans in the application context form a cycle 循环依赖问题 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +public class KiccSentinelFilterConfiguration { + + @Bean + public SentinelWebInterceptor sentinelWebInterceptor(SentinelWebMvcConfig sentinelWebMvcConfig) { + return new SentinelWebInterceptor(sentinelWebMvcConfig); + } + + @Bean + public SentinelWebMvcConfig sentinelWebMvcConfig(SentinelProperties properties, + Optional urlCleanerOptional, Optional blockExceptionHandlerOptional, + Optional requestOriginParserOptional) { + SentinelWebMvcConfig sentinelWebMvcConfig = new SentinelWebMvcConfig(); + sentinelWebMvcConfig.setHttpMethodSpecify(properties.getHttpMethodSpecify()); + sentinelWebMvcConfig.setWebContextUnify(properties.getWebContextUnify()); + + if (blockExceptionHandlerOptional.isPresent()) { + blockExceptionHandlerOptional.ifPresent(sentinelWebMvcConfig::setBlockExceptionHandler); + } else { + if (StringUtils.hasText(properties.getBlockPage())) { + sentinelWebMvcConfig.setBlockExceptionHandler( + ((request, response, e) -> response.sendRedirect(properties.getBlockPage()))); + } else { + sentinelWebMvcConfig.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); + } + } + + urlCleanerOptional.ifPresent(sentinelWebMvcConfig::setUrlCleaner); + requestOriginParserOptional.ifPresent(sentinelWebMvcConfig::setOriginParser); + return sentinelWebMvcConfig; + } + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/ext/KiccSentinelInvocationHandler.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/ext/KiccSentinelInvocationHandler.java new file mode 100644 index 0000000..a16970f --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/ext/KiccSentinelInvocationHandler.java @@ -0,0 +1,178 @@ +package com.cloud.kicc.common.feign.sentinel.ext; + +import com.alibaba.cloud.sentinel.feign.SentinelContractHolder; +import com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.cloud.kicc.common.core.api.R; +import feign.Feign; +import feign.InvocationHandlerFactory; +import feign.MethodMetadata; +import feign.Target; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.LinkedHashMap; +import java.util.Map; + +import static feign.Util.checkNotNull; + +/** + *

+ * 支持自动降级注入 重写 {@link com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler} + * 重新写入一些提示降级信息 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +public class KiccSentinelInvocationHandler implements InvocationHandler { + + public static final String EQUALS = "equals"; + + public static final String HASH_CODE = "hashCode"; + + public static final String TO_STRING = "toString"; + + private final Target target; + + private final Map dispatch; + + private FallbackFactory fallbackFactory; + + private Map fallbackMethodMap; + + KiccSentinelInvocationHandler(Target target, Map dispatch, + FallbackFactory fallbackFactory) { + this.target = checkNotNull(target, "target"); + this.dispatch = checkNotNull(dispatch, "dispatch"); + this.fallbackFactory = fallbackFactory; + this.fallbackMethodMap = toFallbackMethod(dispatch); + } + + KiccSentinelInvocationHandler(Target target, Map dispatch) { + this.target = checkNotNull(target, "target"); + this.dispatch = checkNotNull(dispatch, "dispatch"); + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + if (EQUALS.equals(method.getName())) { + try { + Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; + return equals(otherHandler); + } + catch (IllegalArgumentException e) { + return false; + } + } + else if (HASH_CODE.equals(method.getName())) { + return hashCode(); + } + else if (TO_STRING.equals(method.getName())) { + return toString(); + } + + Object result; + InvocationHandlerFactory.MethodHandler methodHandler = this.dispatch.get(method); + // only handle by HardCodedTarget + if (target instanceof Target.HardCodedTarget) { + Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target; + MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP + .get(hardCodedTarget.type().getName() + Feign.configKey(hardCodedTarget.type(), method)); + // resource default is HttpMethod:protocol://url + if (methodMetadata == null) { + result = methodHandler.invoke(args); + } + else { + String resourceName = methodMetadata.template().method().toUpperCase() + ':' + hardCodedTarget.url() + + methodMetadata.template().path(); + Entry entry = null; + try { + ContextUtil.enter(resourceName); + entry = SphU.entry(resourceName, EntryType.OUT, 1, args); + result = methodHandler.invoke(args); + } + catch (Throwable ex) { + // fallback handle + if (!BlockException.isBlockException(ex)) { + Tracer.trace(ex); + } + if (fallbackFactory != null) { + try { + return fallbackMethodMap.get(method).invoke(fallbackFactory.create(ex), args); + } + catch (IllegalAccessException e) { + // shouldn't happen as method is public due to being an + // interface + throw new AssertionError(e); + } + catch (InvocationTargetException e) { + throw new AssertionError(e.getCause()); + } + } + else { + // 若是R类型 执行自动降级返回R + if (R.class == method.getReturnType()) { + log.error("feign 服务间调用异常", ex); + return R.error(ex.getLocalizedMessage()); + } + else { + throw ex; + } + } + } + finally { + if (entry != null) { + entry.exit(1, args); + } + ContextUtil.exit(); + } + } + } + else { + // other target type using default strategy + result = methodHandler.invoke(args); + } + + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SentinelInvocationHandler) { + KiccSentinelInvocationHandler other = (KiccSentinelInvocationHandler) obj; + return target.equals(other.target); + } + return false; + } + + @Override + public int hashCode() { + return target.hashCode(); + } + + @Override + public String toString() { + return target.toString(); + } + + static Map toFallbackMethod(Map dispatch) { + Map result = new LinkedHashMap<>(); + for (Method method : dispatch.keySet()) { + method.setAccessible(true); + result.put(method, method); + } + return result; + } + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/handle/GlobalBizExceptionHandler.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/handle/GlobalBizExceptionHandler.java new file mode 100644 index 0000000..0cdc55b --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/handle/GlobalBizExceptionHandler.java @@ -0,0 +1,122 @@ +package com.cloud.kicc.common.feign.sentinel.handle; + +import com.alibaba.csp.sentinel.Tracer; +import com.cloud.kicc.common.core.api.R; +import com.cloud.kicc.common.core.exception.CheckedException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.SpringSecurityMessageSource; +import org.springframework.util.Assert; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.List; + +/** + *

+ * 全局异常处理器结合sentinel 全局异常处理器不能作用在 oauth server + * 因为授权那边有自己的异常处理不能覆盖授权异常处理 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@Slf4j +@RestControllerAdvice +@ConditionalOnExpression("!'${security.oauth2.client.clientId}'.isEmpty()") +public class GlobalBizExceptionHandler { + + /** + * 全局异常. + * @param e the e + * @return R + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public R handleGlobalException(Exception e) { + log.error("全局异常信息 ex={}", e.getMessage(), e); + + // 业务异常交由 sentinel 记录 + Tracer.trace(e); + return R.error(e.getLocalizedMessage()); + } + + /** + * 通用前端错误提示自定义全局异常 + * @param e the e + * @return R + */ + @ExceptionHandler(CheckedException.class) + @ResponseStatus(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED) + public R handleGlobalCommonException(CheckedException e) { + log.error("自定义异常信息 ex={}", e.getMessage(), e); + + // 业务异常交由 sentinel 记录 + Tracer.trace(e); + return R.error(e.getLocalizedMessage()); + } + + /** + * 处理业务校验过程中碰到的非法参数异常 该异常基本由{@link org.springframework.util.Assert}抛出 + * @see Assert#hasLength(String, String) + * @see Assert#hasText(String, String) + * @see Assert#isTrue(boolean, String) + * @see Assert#isNull(Object, String) + * @see Assert#notNull(Object, String) + * @param exception 参数校验异常 + * @return API返回结果对象包装后的错误输出结果 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.OK) + public R handleIllegalArgumentException(IllegalArgumentException exception) { + log.error("非法参数,ex = {}", exception.getMessage(), exception); + return R.error(exception.getMessage()); + } + + /** + * AccessDeniedException + * @param e the e + * @return R + */ + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public R handleAccessDeniedException(AccessDeniedException e) { + String msg = SpringSecurityMessageSource.getAccessor().getMessage("AbstractAccessDecisionManager.accessDenied", + e.getMessage()); + log.warn("拒绝授权异常信息 ex={}", msg); + return R.error(e.getLocalizedMessage()); + } + + /** + * validation Exception + * @param exception + * @return R + */ + @ExceptionHandler({ MethodArgumentNotValidException.class }) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleBodyValidException(MethodArgumentNotValidException exception) { + List fieldErrors = exception.getBindingResult().getFieldErrors(); + log.warn("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage()); + return R.error(fieldErrors.get(0).getDefaultMessage()); + } + + /** + * validation Exception (以form-data形式传参) + * @param exception + * @return R + */ + @ExceptionHandler({ BindException.class }) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R bindExceptionHandler(BindException exception) { + List fieldErrors = exception.getBindingResult().getFieldErrors(); + log.warn("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage()); + return R.error(fieldErrors.get(0).getDefaultMessage()); + } + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/handle/KiccUrlBlockHandler.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/handle/KiccUrlBlockHandler.java new file mode 100644 index 0000000..63e6d37 --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/handle/KiccUrlBlockHandler.java @@ -0,0 +1,34 @@ +package com.cloud.kicc.common.feign.sentinel.handle; + +import cn.hutool.http.ContentType; +import cn.hutool.json.JSONUtil; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.cloud.kicc.common.core.api.R; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + *

+ * sentinel统一降级限流返回处理 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +public class KiccUrlBlockHandler implements BlockExceptionHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { + log.error("sentinel 降级 资源名称{}", e.getRule().getResource(), e); + + response.setContentType(ContentType.JSON.toString()); + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + response.getWriter().print(JSONUtil.toJsonStr(R.error(e.getMessage()))); + } + +} diff --git a/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/parser/KiccHeaderRequestOriginParser.java b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/parser/KiccHeaderRequestOriginParser.java new file mode 100644 index 0000000..067939e --- /dev/null +++ b/kicc-common-feign/src/main/java/com/cloud/kicc/common/feign/sentinel/parser/KiccHeaderRequestOriginParser.java @@ -0,0 +1,34 @@ +package com.cloud.kicc.common.feign.sentinel.parser; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; + +import javax.servlet.http.HttpServletRequest; + +/** + *

+ * sentinel 请求头解析判断 + * 配置Sentinel授权规则,根据什么条件进行判断 + * 白名单/黑名单 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class KiccHeaderRequestOriginParser implements RequestOriginParser { + + /** + * 请求头获取allow + */ + private static final String ALLOW = "Allow"; + + /** + * Parse the origin from given HTTP request. + * @param request HTTP request + * @return parsed origin + */ + @Override + public String parseOrigin(HttpServletRequest request) { + return request.getHeader(ALLOW); + } + +} diff --git a/kicc-common-feign/src/main/java/org/springframework/cloud/openfeign/KiccFeignClientsRegistrar.java b/kicc-common-feign/src/main/java/org/springframework/cloud/openfeign/KiccFeignClientsRegistrar.java new file mode 100644 index 0000000..8c332a8 --- /dev/null +++ b/kicc-common-feign/src/main/java/org/springframework/cloud/openfeign/KiccFeignClientsRegistrar.java @@ -0,0 +1,223 @@ +package org.springframework.cloud.openfeign; + +import com.cloud.kicc.common.feign.KiccFeignAutoConfiguration; +import lombok.Getter; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Map; + +/** + *

+ * feign 自动配置功能内置基于mica + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class KiccFeignClientsRegistrar implements ImportBeanDefinitionRegistrar, BeanClassLoaderAware, EnvironmentAware { + + @Getter + private ClassLoader beanClassLoader; + + @Getter + private Environment environment; + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + registerFeignClients(registry); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + private void registerFeignClients(BeanDefinitionRegistry registry) { + List feignClients = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); + // 如果 spring.factories 里为空 + if (feignClients.isEmpty()) { + return; + } + for (String className : feignClients) { + try { + Class clazz = beanClassLoader.loadClass(className); + AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(clazz, FeignClient.class); + if (attributes == null) { + continue; + } + // 如果已经存在该 bean,支持原生的 Feign + if (registry.containsBeanDefinition(className)) { + continue; + } + registerClientConfiguration(registry, getClientName(attributes), attributes.get("configuration")); + + validate(attributes); + BeanDefinitionBuilder definition = BeanDefinitionBuilder + .genericBeanDefinition(FeignClientFactoryBean.class); + definition.addPropertyValue("url", getUrl(attributes)); + definition.addPropertyValue("path", getPath(attributes)); + String name = getName(attributes); + definition.addPropertyValue("name", name); + + // 兼容最新版本的 spring-cloud-openfeign,尚未发布 + StringBuilder aliasBuilder = new StringBuilder(18); + if (attributes.containsKey("contextId")) { + String contextId = getContextId(attributes); + aliasBuilder.append(contextId); + definition.addPropertyValue("contextId", contextId); + } + else { + aliasBuilder.append(name); + } + + definition.addPropertyValue("type", className); + definition.addPropertyValue("decode404", attributes.get("decode404")); + definition.addPropertyValue("fallback", attributes.get("fallback")); + definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + + AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); + + // alias + String alias = aliasBuilder.append("FeignClient").toString(); + + // has a default, won't be null + boolean primary = (Boolean) attributes.get("primary"); + + beanDefinition.setPrimary(primary); + + String qualifier = getQualifier(attributes); + if (StringUtils.hasText(qualifier)) { + alias = qualifier; + } + + BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, + new String[] { alias }); + BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + + } + catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + } + + /** + * Return the class used by {@link SpringFactoriesLoader} to load configuration + * candidates. + * @return the factory class + */ + private Class getSpringFactoriesLoaderFactoryClass() { + return KiccFeignAutoConfiguration.class; + } + + private void validate(Map attributes) { + AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes); + // This blows up if an aliased property is overspecified + FeignClientsRegistrar.validateFallback(annotation.getClass("fallback")); + FeignClientsRegistrar.validateFallbackFactory(annotation.getClass("fallbackFactory")); + } + + private String getName(Map attributes) { + String name = (String) attributes.get("serviceId"); + if (!StringUtils.hasText(name)) { + name = (String) attributes.get("name"); + } + if (!StringUtils.hasText(name)) { + name = (String) attributes.get("value"); + } + name = resolve(name); + return FeignClientsRegistrar.getName(name); + } + + private String getContextId(Map attributes) { + String contextId = (String) attributes.get("contextId"); + if (!StringUtils.hasText(contextId)) { + return getName(attributes); + } + + contextId = resolve(contextId); + return FeignClientsRegistrar.getName(contextId); + } + + private String resolve(String value) { + if (StringUtils.hasText(value)) { + return this.environment.resolvePlaceholders(value); + } + return value; + } + + private String getUrl(Map attributes) { + String url = resolve((String) attributes.get("url")); + return FeignClientsRegistrar.getUrl(url); + } + + private String getPath(Map attributes) { + String path = resolve((String) attributes.get("path")); + return FeignClientsRegistrar.getPath(path); + } + + @Nullable + private String getQualifier(@Nullable Map client) { + if (client == null) { + return null; + } + String qualifier = (String) client.get("qualifier"); + if (StringUtils.hasText(qualifier)) { + return qualifier; + } + return null; + } + + @Nullable + private String getClientName(@Nullable Map client) { + if (client == null) { + return null; + } + String value = (String) client.get("contextId"); + if (!StringUtils.hasText(value)) { + value = (String) client.get("value"); + } + if (!StringUtils.hasText(value)) { + value = (String) client.get("name"); + } + if (!StringUtils.hasText(value)) { + value = (String) client.get("serviceId"); + } + if (StringUtils.hasText(value)) { + return value; + } + + throw new IllegalStateException( + "Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName()); + } + + private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class); + builder.addConstructorArgValue(name); + builder.addConstructorArgValue(configuration); + registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), + builder.getBeanDefinition()); + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + +} diff --git a/kicc-common-feign/src/main/resources/META-INF/spring.factories b/kicc-common-feign/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..b503fd8 --- /dev/null +++ b/kicc-common-feign/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.cloud.kicc.common.feign.KiccFeignAutoConfiguration,\ + com.cloud.kicc.common.feign.sentinel.SentinelAutoConfiguration,\ + com.cloud.kicc.common.feign.sentinel.handle.GlobalBizExceptionHandler diff --git a/kicc-common-job/pom.xml b/kicc-common-job/pom.xml new file mode 100644 index 0000000..acde680 --- /dev/null +++ b/kicc-common-job/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-job + jar + + kicc 定时任务,基于xxl-job + + + + + + com.xuxueli + xxl-job-core + ${xxl-job.version} + + + + org.springframework.cloud + spring-cloud-commons + + + diff --git a/kicc-common-job/src/main/java/com/cloud/kicc/common/job/XxlJobAutoConfiguration.java b/kicc-common-job/src/main/java/com/cloud/kicc/common/job/XxlJobAutoConfiguration.java new file mode 100644 index 0000000..e585d5d --- /dev/null +++ b/kicc-common-job/src/main/java/com/cloud/kicc/common/job/XxlJobAutoConfiguration.java @@ -0,0 +1,72 @@ +package com.cloud.kicc.common.job; + +import com.cloud.kicc.common.job.properties.XxlExecutorProperties; +import com.cloud.kicc.common.job.properties.XxlJobProperties; +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +import java.util.stream.Collectors; + +/** + *

+ * xxl-job自动装配 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(XxlJobProperties.class) +public class XxlJobAutoConfiguration { + + /** + * 服务名称 包含 XXL_JOB_ADMIN 则说明是 Admin + */ + private static final String XXL_JOB_ADMIN = "xxl-job-admin"; + + /** + * 配置xxl-job 执行器,提供自动发现 xxl-job-admin 能力 + * @param xxlJobProperties xxl 配置 + * @param environment 环境变量 + * @param discoveryClient 注册发现客户端 + * @return + */ + @Bean + public XxlJobSpringExecutor xxlJobSpringExecutor(XxlJobProperties xxlJobProperties, Environment environment, + DiscoveryClient discoveryClient) { + XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); + XxlExecutorProperties executor = xxlJobProperties.getExecutor(); + // 应用名默认为服务名 + String appName = executor.getAppname(); + if (!StringUtils.hasText(appName)) { + appName = environment.getProperty("spring.application.name"); + } + xxlJobSpringExecutor.setAppname(appName); + xxlJobSpringExecutor.setAddress(executor.getAddress()); + xxlJobSpringExecutor.setIp(executor.getIp()); + xxlJobSpringExecutor.setPort(executor.getPort()); + xxlJobSpringExecutor.setAccessToken(executor.getAccessToken()); + xxlJobSpringExecutor.setLogPath(executor.getLogPath()); + xxlJobSpringExecutor.setLogRetentionDays(executor.getLogRetentionDays()); + + // 如果配置为空则获取注册中心的服务列表 "http://127.0.0.1:8057/xxl-job-admin" + if (!StringUtils.hasText(xxlJobProperties.getAdmin().getAddresses())) { + String serverList = discoveryClient.getServices().stream().filter(s -> s.contains(XXL_JOB_ADMIN)) + .flatMap(s -> discoveryClient.getInstances(s).stream()).map(instance -> String + .format("http://%s:%s/%s", instance.getHost(), instance.getPort(), XXL_JOB_ADMIN)) + .collect(Collectors.joining(",")); + xxlJobSpringExecutor.setAdminAddresses(serverList); + } + else { + xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdmin().getAddresses()); + } + + return xxlJobSpringExecutor; + } + +} diff --git a/kicc-common-job/src/main/java/com/cloud/kicc/common/job/annotation/EnableKiccXxlJob.java b/kicc-common-job/src/main/java/com/cloud/kicc/common/job/annotation/EnableKiccXxlJob.java new file mode 100644 index 0000000..d8e7eef --- /dev/null +++ b/kicc-common-job/src/main/java/com/cloud/kicc/common/job/annotation/EnableKiccXxlJob.java @@ -0,0 +1,23 @@ +package com.cloud.kicc.common.job.annotation; + +import com.cloud.kicc.common.job.XxlJobAutoConfiguration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + *

+ * 激活xxl-job配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import({ XxlJobAutoConfiguration.class }) +public @interface EnableKiccXxlJob { + +} diff --git a/kicc-common-job/src/main/java/com/cloud/kicc/common/job/properties/XxlAdminProperties.java b/kicc-common-job/src/main/java/com/cloud/kicc/common/job/properties/XxlAdminProperties.java new file mode 100644 index 0000000..e61d37d --- /dev/null +++ b/kicc-common-job/src/main/java/com/cloud/kicc/common/job/properties/XxlAdminProperties.java @@ -0,0 +1,21 @@ +package com.cloud.kicc.common.job.properties; + +import lombok.Data; + +/** + *

+ * xxl-job管理平台配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +@Data +public class XxlAdminProperties { + + /** + * 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。 执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; + */ + private String addresses; + +} diff --git a/kicc-common-job/src/main/java/com/cloud/kicc/common/job/properties/XxlExecutorProperties.java b/kicc-common-job/src/main/java/com/cloud/kicc/common/job/properties/XxlExecutorProperties.java new file mode 100644 index 0000000..d27ec9f --- /dev/null +++ b/kicc-common-job/src/main/java/com/cloud/kicc/common/job/properties/XxlExecutorProperties.java @@ -0,0 +1,52 @@ +package com.cloud.kicc.common.job.properties; + +import lombok.Data; + +/** + *

+ * xxl-job执行器配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +@Data +public class XxlExecutorProperties { + + /** + * 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 + */ + private String appname; + + /** + * 服务注册地址,优先使用该配置作为注册地址 为空时使用内嵌服务 ”IP:PORT“ 作为注册地址 从而更灵活的支持容器类型执行器动态IP和动态映射端口问题 + */ + private String address; + + /** + * 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP ,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 + * "调度中心请求并触发任务" + */ + private String ip; + + /** + * 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口; + */ + private Integer port = 0; + + /** + * 执行器通讯TOKEN [选填]:非空时启用; + */ + private String accessToken; + + /** + * 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; + */ + private String logPath = "logs/applogs/xxl-job/jobhandler"; + + /** + * 执行器日志保存天数 [选填] :值大于3天时生效,启用执行器Log文件定期清理功能,否则不生效; + */ + private Integer logRetentionDays = 30; + +} diff --git a/kicc-common-job/src/main/java/com/cloud/kicc/common/job/properties/XxlJobProperties.java b/kicc-common-job/src/main/java/com/cloud/kicc/common/job/properties/XxlJobProperties.java new file mode 100644 index 0000000..b39906c --- /dev/null +++ b/kicc-common-job/src/main/java/com/cloud/kicc/common/job/properties/XxlJobProperties.java @@ -0,0 +1,25 @@ +package com.cloud.kicc.common.job.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + *

+ * xxl-job配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +@Data +@ConfigurationProperties(prefix = "xxl.job") +public class XxlJobProperties { + + @NestedConfigurationProperty + private XxlAdminProperties admin = new XxlAdminProperties(); + + @NestedConfigurationProperty + private XxlExecutorProperties executor = new XxlExecutorProperties(); + +} diff --git a/kicc-common-log/pom.xml b/kicc-common-log/pom.xml new file mode 100644 index 0000000..1e134fa --- /dev/null +++ b/kicc-common-log/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-log + jar + + kicc 日志服务 + + + + + + com.cloud + kicc-monitor-api + + + + org.springframework.security + spring-security-core + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + + + diff --git a/kicc-common-log/src/main/java/com/cloud/kicc/common/log/LogAutoConfiguration.java b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/LogAutoConfiguration.java new file mode 100644 index 0000000..6e22084 --- /dev/null +++ b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/LogAutoConfiguration.java @@ -0,0 +1,36 @@ +package com.cloud.kicc.common.log; + +import com.cloud.kicc.common.log.aspect.SysLogAspect; +import com.cloud.kicc.common.log.event.SysLogListener; +import com.cloud.kicc.monitor.api.feign.RemoteLogService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; + +/** + *

+ * 日志自动配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@EnableAsync +@RequiredArgsConstructor +@ConditionalOnWebApplication +@Configuration(proxyBeanMethods = false) +public class LogAutoConfiguration { + + @Bean + public SysLogListener sysLogListener(RemoteLogService remoteLogService) { + return new SysLogListener(remoteLogService); + } + + @Bean + public SysLogAspect sysLogAspect() { + return new SysLogAspect(); + } + +} diff --git a/kicc-common-log/src/main/java/com/cloud/kicc/common/log/annotation/SysLog.java b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/annotation/SysLog.java new file mode 100644 index 0000000..163de5e --- /dev/null +++ b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/annotation/SysLog.java @@ -0,0 +1,24 @@ +package com.cloud.kicc.common.log.annotation; + +import java.lang.annotation.*; + +/** + *

+ * 操作日志注解 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SysLog { + + /** + * 描述 + * @return {String} + */ + String value(); + +} diff --git a/kicc-common-log/src/main/java/com/cloud/kicc/common/log/aspect/SysLogAspect.java b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/aspect/SysLogAspect.java new file mode 100644 index 0000000..7e07bd2 --- /dev/null +++ b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/aspect/SysLogAspect.java @@ -0,0 +1,52 @@ +package com.cloud.kicc.common.log.aspect; + +import com.cloud.kicc.common.core.util.SpringContextHolderUtil; +import com.cloud.kicc.common.log.annotation.SysLog; +import com.cloud.kicc.common.log.event.SysLogEvent; +import com.cloud.kicc.common.log.menus.LogTypeEnum; +import com.cloud.kicc.common.log.util.SysLogUtils; +import com.cloud.kicc.monitor.api.entity.OperLog; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +/** + *

+ * 操作日志使用spring event异步入库 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Aspect +@Slf4j +public class SysLogAspect { + + @Around("@annotation(sysLog)") + @SneakyThrows + public Object around(ProceedingJoinPoint point, SysLog sysLog) { + Long startTime = System.currentTimeMillis(); + String strClassName = point.getTarget().getClass().getName(); + String strMethodName = point.getSignature().getName(); + log.debug("[类名]:{},[方法]:{}", strClassName, strMethodName); + OperLog operLog = SysLogUtils.getSysLog(); + operLog.setTitle(sysLog.value()); + Object obj; + try { + obj = point.proceed(); + } catch (Exception e) { + operLog.setType(LogTypeEnum.ERROR.getType()); + operLog.setErrorMsg(e.getMessage()); + throw e; + } finally { + Long endTime = System.currentTimeMillis(); + operLog.setExecuteTime((endTime - startTime) + "毫秒"); + // 发送异步日志事件 + SpringContextHolderUtil.publishEvent(new SysLogEvent(operLog)); + } + return obj; + } + +} diff --git a/kicc-common-log/src/main/java/com/cloud/kicc/common/log/event/SysLogEvent.java b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/event/SysLogEvent.java new file mode 100644 index 0000000..023ec84 --- /dev/null +++ b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/event/SysLogEvent.java @@ -0,0 +1,20 @@ +package com.cloud.kicc.common.log.event; + +import com.cloud.kicc.monitor.api.entity.OperLog; +import org.springframework.context.ApplicationEvent; + +/** + *

+ * 系统日志事件 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class SysLogEvent extends ApplicationEvent { + + public SysLogEvent(OperLog source) { + super(source); + } + +} diff --git a/kicc-common-log/src/main/java/com/cloud/kicc/common/log/event/SysLogListener.java b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/event/SysLogListener.java new file mode 100644 index 0000000..21b482b --- /dev/null +++ b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/event/SysLogListener.java @@ -0,0 +1,34 @@ +package com.cloud.kicc.common.log.event; + +import com.cloud.kicc.common.core.constant.SecurityConstants; +import com.cloud.kicc.monitor.api.entity.OperLog; +import com.cloud.kicc.monitor.api.feign.RemoteLogService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; + +/** + *

+ * 异步监听日志事件 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +@RequiredArgsConstructor +public class SysLogListener { + + private final RemoteLogService remoteLogService; + + @Async + @Order + @EventListener(SysLogEvent.class) + public void saveSysLog(SysLogEvent event) { + OperLog operLog = (OperLog) event.getSource(); + remoteLogService.saveLog(operLog, SecurityConstants.FROM_IN); + } + +} diff --git a/kicc-common-log/src/main/java/com/cloud/kicc/common/log/init/ApplicationLoggerInitializer.java b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/init/ApplicationLoggerInitializer.java new file mode 100644 index 0000000..279425a --- /dev/null +++ b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/init/ApplicationLoggerInitializer.java @@ -0,0 +1,33 @@ +package com.cloud.kicc.common.log.init; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + *

+ * 通过环境变量的形式设置springboot内部logback路径 + * 达到每个服务都可以动态配置的目的 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class ApplicationLoggerInitializer implements EnvironmentPostProcessor, Ordered { + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + String appName = environment.getProperty("spring.application.name"); + String logBase = environment.getProperty("LOGGING_PATH", "logs"); + + // spring boot admin 直接加载日志 + System.setProperty("logging.file.name", String.format("%s/%s/debug.log", logBase, appName)); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/kicc-common-log/src/main/java/com/cloud/kicc/common/log/menus/LogTypeEnum.java b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/menus/LogTypeEnum.java new file mode 100644 index 0000000..33e6466 --- /dev/null +++ b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/menus/LogTypeEnum.java @@ -0,0 +1,38 @@ +package com.cloud.kicc.common.log.menus; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + *

+ * 日志类型 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Getter +@RequiredArgsConstructor +public enum LogTypeEnum { + + /** + * 正常日志类型 + */ + NORMAL("0", "正常日志"), + + /** + * 错误日志类型 + */ + ERROR("9", "错误日志"); + + /** + * 类型 + */ + private final String type; + + /** + * 描述 + */ + private final String description; + +} diff --git a/kicc-common-log/src/main/java/com/cloud/kicc/common/log/util/SysLogUtils.java b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/util/SysLogUtils.java new file mode 100644 index 0000000..5b53e36 --- /dev/null +++ b/kicc-common-log/src/main/java/com/cloud/kicc/common/log/util/SysLogUtils.java @@ -0,0 +1,95 @@ +package com.cloud.kicc.common.log.util; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.hutool.http.HttpUtil; +import com.cloud.kicc.common.data.entity.CasUser; +import com.cloud.kicc.common.log.menus.LogTypeEnum; +import com.cloud.kicc.monitor.api.entity.OperLog; +import lombok.experimental.UtilityClass; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.web.authentication.www.BasicAuthenticationConverter; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Optional; + +/** + *

+ * 系统日志工具类 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@UtilityClass +public class SysLogUtils { + + public OperLog getSysLog() { + HttpServletRequest request = ((ServletRequestAttributes) Objects + .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + OperLog sysLog = new OperLog(); + sysLog.setOperIp(ServletUtil.getClientIP(request)); + sysLog.setType(LogTypeEnum.NORMAL.getType()); + sysLog.setOperAddr(ServletUtil.getClientIP(request)); + sysLog.setOperUrl(URLUtil.getPath(request.getRequestURI())); + sysLog.setMethod(request.getMethod()); + sysLog.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT)); + sysLog.setOperParam(HttpUtil.toParams(request.getParameterMap())); + sysLog.setClientId(getClientId(request)); + sysLog.setServiceId(getClientId(request)); + sysLog.setOperTime(LocalDateTime.now()); + if (ObjectUtil.isNotEmpty(getUser())) { + sysLog.setOperName(getUser().getUsername()); + sysLog.setCreateById(getUser().getId()); + sysLog.setCreateByName(getUser().getUsername()); + sysLog.setUpdateById(getUser().getId()); + sysLog.setUpdateByName(getUser().getUsername()); + } + return sysLog; + } + + /** + * 获取客户端 + * @return clientId + */ + private String getClientId(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof OAuth2Authentication) { + OAuth2Authentication auth2Authentication = (OAuth2Authentication) authentication; + return auth2Authentication.getOAuth2Request().getClientId(); + } + if (authentication instanceof UsernamePasswordAuthenticationToken) { + BasicAuthenticationConverter basicAuthenticationConverter = new BasicAuthenticationConverter(); + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = basicAuthenticationConverter + .convert(request); + if (usernamePasswordAuthenticationToken != null) { + return usernamePasswordAuthenticationToken.getName(); + } + } + return null; + } + + /** + * 获取用户 + */ + protected CasUser getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (Optional.ofNullable(authentication).isPresent()) { + Object principal = authentication.getPrincipal(); + if (principal instanceof CasUser) { + return (CasUser) principal; + } + } + return null; + } + +} diff --git a/kicc-common-log/src/main/resources/META-INF/spring.factories b/kicc-common-log/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..16f70e3 --- /dev/null +++ b/kicc-common-log/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.cloud.kicc.common.log.LogAutoConfiguration +org.springframework.boot.env.EnvironmentPostProcessor=\ + com.cloud.kicc.common.log.init.ApplicationLoggerInitializer diff --git a/kicc-common-mock/pom.xml b/kicc-common-mock/pom.xml new file mode 100644 index 0000000..e48c9e8 --- /dev/null +++ b/kicc-common-mock/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-mock + jar + + kicc oauth 2.0 单元模拟测试工具类 + + + + com.cloud + kicc-common-security + + + org.springframework.security + spring-security-test + + + diff --git a/kicc-common-mock/src/main/java/com/cloud/kicc/common/mock/WithMockSecurityContextFactory.java b/kicc-common-mock/src/main/java/com/cloud/kicc/common/mock/WithMockSecurityContextFactory.java new file mode 100644 index 0000000..adf5ffd --- /dev/null +++ b/kicc-common-mock/src/main/java/com/cloud/kicc/common/mock/WithMockSecurityContextFactory.java @@ -0,0 +1,73 @@ +package com.cloud.kicc.common.mock; + +import cn.hutool.http.HttpRequest; +import cn.hutool.json.JSONUtil; +import com.cloud.kicc.common.core.util.SpringContextHolderUtil; +import com.cloud.kicc.common.mock.annotation.WithMockOAuth2User; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.token.RemoteTokenServices; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +/** + *

+ * oauth2 上下文生成处理器 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class WithMockSecurityContextFactory implements WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(WithMockOAuth2User oAuth2User) { + // 1. 请求认证中心获取token + String token = getToken(oAuth2User); + + // 2. 解析认证中心返回用户 + OAuth2Authentication authentication = getUser(token); + + // 3. 构建 oauth2 上下文 + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + + // 4. 上下文保存 token + DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(token); + OAuth2ClientContext clientContext = SpringContextHolderUtil.getBean(OAuth2ClientContext.class); + clientContext.setAccessToken(accessToken); + return context; + } + + /** + * 请求认证中心获取token + * @param oAuth2User 账号、密码 + * @return String token + */ + private String getToken(WithMockOAuth2User oAuth2User) { + OAuth2ProtectedResourceDetails clientProperties = SpringContextHolderUtil.getBean(OAuth2ProtectedResourceDetails.class); + String result = HttpRequest.post(clientProperties.getAccessTokenUri()) + .basicAuth(clientProperties.getClientId(), clientProperties.getClientSecret()) + .form("username", oAuth2User.username()) + .form("password", oAuth2User.password()) + .form("grant_type", "password") + .form("scope", clientProperties.getScope()) + .execute() + .body(); + return JSONUtil.parseObj(result).getStr("access_token"); + } + + /** + * 使用token 获取用户详情 + * @param token token + * @return user详细 + */ + private OAuth2Authentication getUser(String token) { + RemoteTokenServices tokenServices = SpringContextHolderUtil.getBean(RemoteTokenServices.class); + return tokenServices.loadAuthentication(token); + } + +} diff --git a/kicc-common-mock/src/main/java/com/cloud/kicc/common/mock/annotation/WithMockOAuth2User.java b/kicc-common-mock/src/main/java/com/cloud/kicc/common/mock/annotation/WithMockOAuth2User.java new file mode 100644 index 0000000..5684948 --- /dev/null +++ b/kicc-common-mock/src/main/java/com/cloud/kicc/common/mock/annotation/WithMockOAuth2User.java @@ -0,0 +1,33 @@ +package com.cloud.kicc.common.mock.annotation; + +import com.cloud.kicc.common.core.constant.SecurityConstants; +import com.cloud.kicc.common.mock.WithMockSecurityContextFactory; +import org.springframework.security.test.context.support.WithSecurityContext; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + *

+ * WithMockOAuth2User 注解 + * 用于单元测试模拟登录用户测试接口 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockSecurityContextFactory.class) +public @interface WithMockOAuth2User { + + /** + * 用户名 + */ + String username() default SecurityConstants.MOCK_USERNAME; + + /** + * 密码 + */ + String password() default SecurityConstants.MOCK_PASSWORD; + +} diff --git a/kicc-common-mock/src/main/java/com/cloud/kicc/common/mock/kit/OAuthMockKit.java b/kicc-common-mock/src/main/java/com/cloud/kicc/common/mock/kit/OAuthMockKit.java new file mode 100644 index 0000000..39ceffe --- /dev/null +++ b/kicc-common-mock/src/main/java/com/cloud/kicc/common/mock/kit/OAuthMockKit.java @@ -0,0 +1,31 @@ +package com.cloud.kicc.common.mock.kit; + +import com.cloud.kicc.common.core.util.SpringContextHolderUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +/** + *

+ * Mock 工具类 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class OAuthMockKit { + + /** + * mock 请求增加统一请求头 + * @return RequestPostProcessor 类似于拦截器 + */ + public static RequestPostProcessor token() { + return mockRequest -> { + OAuth2ClientContext clientContext = SpringContextHolderUtil.getBean(OAuth2ClientContext.class); + String token = clientContext.getAccessToken().getValue(); + mockRequest.addHeader(HttpHeaders.AUTHORIZATION, String.format("Bearer: %s", token)); + return mockRequest; + }; + } + +} diff --git a/kicc-common-rocketmq/pom.xml b/kicc-common-rocketmq/pom.xml new file mode 100644 index 0000000..278de41 --- /dev/null +++ b/kicc-common-rocketmq/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-rocketmq + jar + + 阿里 rocketmq 消息中间件 + + + + com.cloud + kicc-common-core + + + com.alibaba.cloud + spring-cloud-starter-stream-rocketmq + + + + + diff --git a/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/channel/KiccSink.java b/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/channel/KiccSink.java new file mode 100644 index 0000000..c3c9511 --- /dev/null +++ b/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/channel/KiccSink.java @@ -0,0 +1,33 @@ +package com.cloud.kicc.common.rocketmq.channel; + +import com.cloud.kicc.common.rocketmq.constant.MessageConstant; +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.messaging.SubscribableChannel; + +/** + *

+ * 消费者Channel + *

+ * + * @Author: wangxiang4 + * @Date: 2022/3/9 + */ +public interface KiccSink { + + /** + * 短消息消费者 + * + * @return SubscribableChannel + */ + @Input(MessageConstant.SMS_MESSAGE_INPUT) + SubscribableChannel smsInput(); + + + /** + * 订单消费者 + * + * @return SubscribableChannel + */ + @Input(MessageConstant.ORDER_MESSAGE_INPUT) + SubscribableChannel orderInput(); +} diff --git a/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/channel/KiccSource.java b/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/channel/KiccSource.java new file mode 100644 index 0000000..4f6a4a1 --- /dev/null +++ b/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/channel/KiccSource.java @@ -0,0 +1,32 @@ +package com.cloud.kicc.common.rocketmq.channel; + +import com.cloud.kicc.common.rocketmq.constant.MessageConstant; +import org.springframework.cloud.stream.annotation.Output; +import org.springframework.messaging.MessageChannel; + +/** + *

+ * 生产者Channel + *

+ * + * @Author: wangxiang4 + * @Date: 2022/3/9 + */ +public interface KiccSource { + + /** + * 短消息通道 + * + * @return MessageChannel + */ + @Output(MessageConstant.SMS_MESSAGE_OUTPUT) + MessageChannel smsOutput(); + + /** + * 订单通道 + * + * @return MessageChannel + */ + @Output(MessageConstant.ORDER_MESSAGE_OUTPUT) + MessageChannel orderOutput(); +} diff --git a/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/config/RocketMQConfiguration.java b/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/config/RocketMQConfiguration.java new file mode 100644 index 0000000..dbfa9ea --- /dev/null +++ b/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/config/RocketMQConfiguration.java @@ -0,0 +1,18 @@ +package com.cloud.kicc.common.rocketmq.config; + +import com.cloud.kicc.common.core.factory.YamlPropertySourceFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +/** + *

+ * RocketMQ配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/3/9 + */ +@Configuration +@PropertySource(factory = YamlPropertySourceFactory.class, value = "classpath:kicc-rocketmq.yml") +public class RocketMQConfiguration { +} diff --git a/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/constant/MessageConstant.java b/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/constant/MessageConstant.java new file mode 100644 index 0000000..a1242a6 --- /dev/null +++ b/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/constant/MessageConstant.java @@ -0,0 +1,62 @@ +package com.cloud.kicc.common.rocketmq.constant; + +import com.cloud.kicc.common.core.constant.StringPool; + +/** + *

+ * 消息中心常量 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/3/9 + */ +public class MessageConstant { + + /** + * 生产者标识 + */ + public static final String OUTPUT = "output"; + + /** + * 消费者标识 + */ + public static final String INPUT = "input"; + + /** + * 短信消息 + */ + public static final String SMS_MESSAGE = "sms"; + + /** + * 订单消息 + */ + public static final String ORDER_MESSAGE = "order"; + + /** + * 消息生产者 + */ + public static final String SMS_MESSAGE_OUTPUT = SMS_MESSAGE + StringPool.DASH + OUTPUT; + + + /** + * 订单生产者 + */ + public static final String ORDER_MESSAGE_OUTPUT = ORDER_MESSAGE + StringPool.DASH + OUTPUT; + + /** + * 短信消费者 + */ + public static final String SMS_MESSAGE_INPUT = SMS_MESSAGE + StringPool.DASH + INPUT; + + /** + * 订单消费者 + */ + public static final String ORDER_MESSAGE_INPUT = ORDER_MESSAGE + StringPool.DASH + INPUT; + + /** + * 订单组 + */ + public static final String ORDER_BINDER_GROUP = ORDER_MESSAGE + StringPool.DASH + "binder-group"; + + +} diff --git a/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/entity/Order.java b/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/entity/Order.java new file mode 100644 index 0000000..68f1779 --- /dev/null +++ b/kicc-common-rocketmq/src/main/java/com/cloud/kicc/common/rocketmq/entity/Order.java @@ -0,0 +1,48 @@ +package com.cloud.kicc.common.rocketmq.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; +import lombok.experimental.Accessors; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + *

+ * 演示订单表 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/3/9 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = false) +public class Order implements Serializable { + + private static final long serialVersionUID = 2011242218927120008L; + + private Long id; + + private Long tradeId; + + private Long goodsId; + + private BigDecimal goodsPrice; + + private Integer number; + + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime createTime; + + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss" , timezone = "GMT+8") + private LocalDateTime updateTime; + +} diff --git a/kicc-common-rocketmq/src/main/resources/META-INF/spring.factories b/kicc-common-rocketmq/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..231a2a9 --- /dev/null +++ b/kicc-common-rocketmq/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.cloud.kicc.common.rocketmq.config.RocketMQConfiguration diff --git a/kicc-common-rocketmq/src/main/resources/kicc-rocketmq.yml b/kicc-common-rocketmq/src/main/resources/kicc-rocketmq.yml new file mode 100644 index 0000000..871e668 --- /dev/null +++ b/kicc-common-rocketmq/src/main/resources/kicc-rocketmq.yml @@ -0,0 +1,61 @@ +spring: + # 参考信息,后期可以阅读源码类添加新的配置信息:https://www.jianshu.com/p/7f8fd90564ca + cloud: + # spring-cloud-stream配置项,对应BindingServiceProperties类 + stream: + # Binding配置项,对应 BindingProperties Map + bindings: + # 生产通道配置---------------------------------------------- + sms-output: + # 主题通道,这里使用RocketMQ Topic + destination: sms-topic + # 内容格式,这里使用JSON + content-type: application/json + order-output: + # 主题通道,这里使用RocketMQ Topic + destination: order-topic + # 内容格式,这里使用JSON + content-type: application/json + # 消费通道配置---------------------------------------------- + sms-input: + # 主题通道,这里使用RocketMQ Topic + destination: sms-topic + # 内容格式,这里使用JSON + content-type: text/plain + # 消费组(ps:如果管道没有指定消费组,那么这个匿名消费组会与其它组一起消费消息,会出现了重复消费的问题) + group: sms-group + order-input: + # 主题通道,这里使用RocketMQ Topic + destination: order-topic + # 内容格式,这里使用JSON + content-type: text/plain + # 消费组(ps:如果管道没有指定消费组,那么这个匿名消费组会与其它组一起消费消息,会出现了重复消费的问题) + group: order-group + # spring-cloud-stream-rocketmq配置项 + rocketmq: + # rocketmq自定义binding配置项,对应RocketMQBindingProperties Map + bindings: + # 生产通道绑定配置---------------------------------------------- + sms-output: + # RocketMQ-Producer配置项,对应RocketMQProducerProperties类 + producer: + # 生产者分组 + group: sms-binder-group + # 是否同步发送消息,默认为false异步 + sync: true + order-output: + producer: + # 生产者分组 + group: order-binder-group + # 是否开启分布式事务 + transactional: true + # 消费通道绑定配置---------------------------------------------- + sms-input: + # RocketMQ-Consumer配置项,对应RocketMQConsumerProperties类 + consumer: + # 消息顺序消费 + orderly: true + order-input: + consumer: + # 消息顺序消费 + orderly: true diff --git a/kicc-common-seata/README.md b/kicc-common-seata/README.md new file mode 100644 index 0000000..4915305 --- /dev/null +++ b/kicc-common-seata/README.md @@ -0,0 +1,256 @@ +# Seata + +## 部署安装 + +- 集成nacos配置中心 + + 下载 config ` https://github.com/seata/seata/blob/develop/script/config-center/config.txt` + + 下载 sh脚本 `https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.sh` + + 本地修改config.txt 相关参数,修改nacos-config.sh 相关配置,并执行脚本,将配置推送到nacos + +- Docker部署(需要指定当前宿主机IP) + + ```sh + docker run --name seata-server \ + -p 8091:8091 \ + -e SEATA_IP=10.113.206.85 \ + -e SEATA_PORT=8091 \ + seataio/seata-server + ``` + + 进入容器中,修改registry.conf 配置Nacos相关参数 + + ```sh + registry { + # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa + type = "nacos" + loadBalance = "RandomLoadBalance" + loadBalanceVirtualNodes = 10 + + nacos { + application = "serverAddr" + serverAddr = "10.113.206.34:8848" + group = "DEFAULT_GROUP" + namespace = "" + cluster = "default" + username = "" + password = "" + } + } + + config { + # file、nacos 、apollo、zk、consul、etcd3 + type = "nacos" + + nacos { + serverAddr = "10.113.206.34:8848" + namespace = "" + group = "SEATA_GROUP" + username = "" + password = "" + # dataId = "seataServer.properties" + } + } + ``` + + + +## 注入原理 + +- 添加注解 @EnableAutoDataSourceProxy 配置自动代理数据源 + + ```java + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Import({AutoDataSourceProxyRegistrar.class}) + @Documented + public @interface EnableAutoDataSourceProxy { + boolean useJdkProxy() default false; + + String[] excludes() default {}; + + //设置工作模式 默认AT模式,可以选择TCC、XA、SAGA + String dataSourceProxyMode() default "AT"; + } + ``` + + @Import 引入配置类 AutoDataSourceProxyRegistrar.class 配置了代理数据源对象 + + ```java + + ``` + +- 由于maven依赖了 + + ```java + + io.seata + seata-spring-boot-starter + 1.4.1 + + ``` + + 1:由spring.factories 配置加载配置类 加载bean GlobalTransactionScanner + + ```java + @Configuration + @EnableConfigurationProperties({SeataProperties.class}) + public class SeataAutoConfiguration { + . + . + . + @Bean + @DependsOn({"springApplicationContextProvider", "failureHandler"}) + @ConditionalOnMissingBean({GlobalTransactionScanner.class}) + public GlobalTransactionScanner globalTransactionScanner(SeataProperties seataProperties, FailureHandler failureHandler) { + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Automatically configure Seata"); + } + + return new GlobalTransactionScanner(seataProperties.getApplicationId(), seataProperties.getTxServiceGroup(), failureHandler); + } + + . + . + . + } + ``` + + GlobalTransactionScanner 是个spring bean处理器,主要继承以及实现如下 + + ```java + public class GlobalTransactionScanner extends AbstractAutoProxyCreator implements ConfigurationChangeListener, InitializingBean, ApplicationContextAware, DisposableBean { + + } + + public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport + implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware { + + } + ``` + + 由这部分判断是否有注解@GlobalTransactional 并创建代理对象,增强方法主要在有该注解的方法前,生成XID,在TC注册该全局事务; + + ```java + public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { + if (bean != null) { + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (this.earlyProxyReferences.remove(cacheKey) != bean) { + return wrapIfNecessary(bean, beanName, cacheKey); + } + } + return bean; + } + ``` + + GlobalTransactionalInterceptor 判断 是否有@GlobalTransactional注解 + + ```java + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + Class targetClass = methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null; + Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass); + if (specificMethod != null && !specificMethod.getDeclaringClass().equals(Object.class)) { + Method method = BridgeMethodResolver.findBridgedMethod(specificMethod); + GlobalTransactional globalTransactionalAnnotation = (GlobalTransactional)this.getAnnotation(method, targetClass, GlobalTransactional.class); + GlobalLock globalLockAnnotation = (GlobalLock)this.getAnnotation(method, targetClass, GlobalLock.class); + boolean localDisable = this.disable || degradeCheck && degradeNum >= degradeCheckAllowTimes; + if (!localDisable) { + if (globalTransactionalAnnotation != null) { + return this.handleGlobalTransaction(methodInvocation, globalTransactionalAnnotation); + } + + if (globalLockAnnotation != null) { + return this.handleGlobalLock(methodInvocation, globalLockAnnotation); + } + } + } + + return methodInvocation.proceed(); + } + + ``` + + AT的增强类:SeataAutoDataSourceProxyAdvice + + ```java + public class SeataAutoDataSourceProxyAdvice implements MethodInterceptor, IntroductionInfo { + + public Object invoke(MethodInvocation invocation) throws Throwable { + if (!RootContext.requireGlobalLock() && this.dataSourceProxyMode != RootContext.getBranchType()) { + return invocation.proceed(); + } else { + Method method = invocation.getMethod(); + Object[] args = invocation.getArguments(); + Method m = BeanUtils.findDeclaredMethod(this.dataSourceProxyClazz, method.getName(), method.getParameterTypes()); + if (m != null) { + SeataDataSourceProxy dataSourceProxy = DataSourceProxyHolder.get().putDataSource((DataSource)invocation.getThis(), this.dataSourceProxyMode); + return m.invoke(dataSourceProxy, args); + } else { + return invocation.proceed(); + } + } + } + + } + ``` + + 其中 RootContext.requireGlobalLock() 先判断了当前线程的该ThreadLocal中是否有 TX_LOCK ,如果有,执行代理方法 + + ``` + public static boolean requireGlobalLock() { + return CONTEXT_HOLDER.get("TX_LOCK") != null; + } + ``` + + 2:该spring.factories 配置也同时加载配置类 加载bean HttpAutoConfiguration + + ```java + @Configuration + @ConditionalOnWebApplication + public class HttpAutoConfiguration extends WebMvcConfigurerAdapter { + public HttpAutoConfiguration() { + } + + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new TransactionPropagationInterceptor()); + } + + public void extendHandlerExceptionResolvers(List exceptionResolvers) { + exceptionResolvers.add(new HttpHandlerExceptionResolver()); + } + } + + + ``` + + 新增了拦截器,用于拦截请求,构造上面的 线程ThreadLocal 变量,为之后数据源代理对象,判断是否是分布式事务 + + ```java + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String xid = RootContext.getXID(); + String rpcXid = request.getHeader("TX_XID"); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("xid in RootContext[{}] xid in HttpContext[{}]", xid, rpcXid); + } + + if (xid == null && rpcXid != null) { + RootContext.bind(rpcXid); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("bind[{}] to RootContext", rpcXid); + } + } + + return true; + } + + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { + if (RootContext.inGlobalTransaction()) { + XidResource.cleanXid(request.getHeader("TX_XID")); + } + + } + ``` + +- 未发现用feign拦截器进行request插入参数 XID的配置;对于请求的XID如何传递,暂时未看到源码。 \ No newline at end of file diff --git a/kicc-common-seata/pom.xml b/kicc-common-seata/pom.xml new file mode 100644 index 0000000..324817a --- /dev/null +++ b/kicc-common-seata/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-seata + jar + + kicc 阿里巴巴-seata分布式事务解决方案 + + + + com.cloud + kicc-common-core + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-seata + + + diff --git a/kicc-common-seata/src/main/java/com/cloud/kicc/common/seata/config/SeataAutoConfiguration.java b/kicc-common-seata/src/main/java/com/cloud/kicc/common/seata/config/SeataAutoConfiguration.java new file mode 100644 index 0000000..24946e3 --- /dev/null +++ b/kicc-common-seata/src/main/java/com/cloud/kicc/common/seata/config/SeataAutoConfiguration.java @@ -0,0 +1,67 @@ +package com.cloud.kicc.common.seata.config; + +import com.cloud.kicc.common.core.factory.YamlPropertySourceFactory; +import com.cloud.kicc.common.seata.props.SeataProperties; +import io.seata.spring.annotation.datasource.EnableAutoDataSourceProxy; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.PropertySource; + +import javax.annotation.PostConstruct; +import javax.sql.DataSource; +import java.sql.SQLException; + +/** + *

+ * seata配置 + * @EnableAutoDataSourceProxy 自动开启代理数据源 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/3/11 + */ +@Slf4j +@AllArgsConstructor +@EnableAutoDataSourceProxy +@ConditionalOnBean(DataSource.class) +@EnableConfigurationProperties(SeataProperties.class) +@PropertySource(factory = YamlPropertySourceFactory.class, value = "classpath:kicc-seata.yml") +public class SeataAutoConfiguration { + + @Autowired + public DataSource dataSource; + + public static final String undoLogSql = "CREATE TABLE IF NOT EXISTS undo_log(" + + "`id` bigint(20) NOT NULL AUTO_INCREMENT," + + "`branch_id` bigint(20) NOT NULL," + + "`xid` varchar(100) NOT NULL," + + "`context` varchar(128) NOT NULL," + + "`rollback_info` longblob NOT NULL," + + "`log_status` int(11) NOT NULL," + + "`log_created` datetime NOT NULL," + + "`log_modified` datetime NOT NULL," + + "`ext` varchar(100) DEFAULT NULL," + + "`tenant_id` varchar(255) DEFAULT NULL," + + "PRIMARY KEY (`id`)," + + "UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)" + + ")ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;"; + + public final SeataProperties seataProperties; + + /** + * 判断当前数据库是否有undo_log该表,如果没有 + * 创建该表undo_log为seata记录事务sql执行的记录表第二阶段时,如果confirm会清除记录,如果是cancel会根据记录补偿原数据 + */ + @PostConstruct + public void detectTable() { + try { + dataSource.getConnection().prepareStatement(undoLogSql).execute(); + } catch (SQLException e) { + log.error("创建[seata] undo_log表错误。", e); + } + } + +} diff --git a/kicc-common-seata/src/main/java/com/cloud/kicc/common/seata/props/SeataProperties.java b/kicc-common-seata/src/main/java/com/cloud/kicc/common/seata/props/SeataProperties.java new file mode 100644 index 0000000..999994a --- /dev/null +++ b/kicc-common-seata/src/main/java/com/cloud/kicc/common/seata/props/SeataProperties.java @@ -0,0 +1,24 @@ +package com.cloud.kicc.common.seata.props; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + *

+ * seata配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/3/11 + */ +@Data +@ConfigurationProperties(prefix = "seata") +public class SeataProperties { + + private String applicationId; + + private String txServiceGroup; + +} diff --git a/kicc-common-seata/src/main/resources/META-INF/spring.factories b/kicc-common-seata/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..e3d09a0 --- /dev/null +++ b/kicc-common-seata/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.cloud.kicc.common.seata.config.SeataAutoConfiguration + + + diff --git a/kicc-common-seata/src/main/resources/kicc-seata.yml b/kicc-common-seata/src/main/resources/kicc-seata.yml new file mode 100644 index 0000000..3cb6e9e --- /dev/null +++ b/kicc-common-seata/src/main/resources/kicc-seata.yml @@ -0,0 +1,34 @@ +# seata配置项,对应SeataProperties类 +seata: + enabled: true + # Seata应用编号 + application-id: ${spring.application.name} + # seata事务组编号,用于TC集群名 + tx-service-group: default_tx_group + # seata服务配置项,对应ServiceProperties类 + config: + type: nacos + nacos: + # Nacos 命名空间 + namespace: kicc-seata + # Nacos分组名称 + group: SEATA_GROUP + # nacos服务地址 + server-addr: ${spring.cloud.nacos.discovery.server-addr} + service: + # 虚拟组和分组的映射 + vgroup-mapping: + default_tx_group: default + # seata注册中心配置项,对应RegistryProperties类 + registry: + # 注册中心类型,默认为file + type: nacos + nacos: + # 使用的seata分组 + cluster: default + # Nacos命名空间 + namespace: kicc-seata + # Nacos分组名称 + group: SEATA_GROUP + # Nacos服务地址 + serverAddr: ${spring.cloud.nacos.discovery.server-addr} diff --git a/kicc-common-security/pom.xml b/kicc-common-security/pom.xml new file mode 100644 index 0000000..df763c9 --- /dev/null +++ b/kicc-common-security/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-security + jar + + kicc 安全工具类 + + + + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + + + + org.springframework.boot + spring-boot-starter-aop + + + + com.cloud + kicc-common-data + + + diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/annotation/EnableKiccResourceServer.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/annotation/EnableKiccResourceServer.java new file mode 100644 index 0000000..056e392 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/annotation/EnableKiccResourceServer.java @@ -0,0 +1,32 @@ +package com.cloud.kicc.common.security.annotation; + +import com.cloud.kicc.common.security.config.ResourceServerAutoConfiguration; +import com.cloud.kicc.common.security.config.ResourceServerTokenRelayAutoConfiguration; +import com.cloud.kicc.common.security.exp.KiccSecurityBeanDefinitionRegistrar; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; + +import java.lang.annotation.*; + +/** + *

+ * 扩展资源服务注解 + * 添加方法安全级别 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Documented +@Inherited +@EnableResourceServer +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@EnableGlobalMethodSecurity(prePostEnabled = true) +@Import({ ResourceServerAutoConfiguration.class, + KiccSecurityBeanDefinitionRegistrar.class, + ResourceServerTokenRelayAutoConfiguration.class }) +public @interface EnableKiccResourceServer { + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/annotation/Inner.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/annotation/Inner.java new file mode 100644 index 0000000..2573ad4 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/annotation/Inner.java @@ -0,0 +1,30 @@ +package com.cloud.kicc.common.security.annotation; + +import java.lang.annotation.*; + +/** + *

+ * 服务调用不鉴权注解 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Inner { + + /** + * 是否AOP统一处理 + * @return false, true + */ + boolean value() default true; + + /** + * 为后续扩展做准备,需要特殊判空的字段(预留) + * @return {} + */ + String[] field() default {}; + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/aspect/SecurityInnerAspect.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/aspect/SecurityInnerAspect.java new file mode 100644 index 0000000..3e2df34 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/aspect/SecurityInnerAspect.java @@ -0,0 +1,58 @@ +package com.cloud.kicc.common.security.aspect; + +import cn.hutool.core.util.StrUtil; +import com.cloud.kicc.common.core.constant.SecurityConstants; +import com.cloud.kicc.common.security.annotation.Inner; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.security.access.AccessDeniedException; + +import javax.servlet.http.HttpServletRequest; + +/** + *

+ * 内部接口调用注解切面处理 + * 设置是否是内部接口,不提供给外部访问 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +@Aspect +@RequiredArgsConstructor +public class SecurityInnerAspect implements Ordered { + + @Autowired(required = false) + private HttpServletRequest request; + + @SneakyThrows + @Around("@within(inner) || @annotation(inner)") + public Object around(ProceedingJoinPoint point, Inner inner) { + // 实际注入的inner实体由表达式后一个注解决定,即是方法上的@Inner注解实体,若方法上无@Inner注解,则获取类上的 + if (inner == null) { + Class clazz = point.getTarget().getClass(); + inner = AnnotationUtils.findAnnotation(clazz, Inner.class); + } + String header = request.getHeader(SecurityConstants.FROM); + // 设置是否是内部接口,不提供给外部访问 + if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) { + log.warn("访问接口 {} 没有权限", point.getSignature().getName()); + throw new AccessDeniedException("Access is denied"); + } + return point.proceed(); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE + 1; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/Oauth2SecurityAutoConfiguration.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/Oauth2SecurityAutoConfiguration.java new file mode 100644 index 0000000..48a7d12 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/Oauth2SecurityAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.cloud.kicc.common.security.config; + +import com.cloud.kicc.common.security.properties.CasProperties; +import com.cloud.kicc.common.security.xss.XssFilterAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + *

+ * OAUTH2 配置 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/9/16 + */ +@Configuration(proxyBeanMethods = false) +@Import({ XssFilterAutoConfiguration.class, SecurityMessageSourceConfiguration.class}) +@EnableConfigurationProperties(CasProperties.class) +public class Oauth2SecurityAutoConfiguration { +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/ResourceServerAutoConfiguration.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/ResourceServerAutoConfiguration.java new file mode 100644 index 0000000..e836082 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/ResourceServerAutoConfiguration.java @@ -0,0 +1,51 @@ +package com.cloud.kicc.common.security.config; + +import com.cloud.kicc.common.security.exp.KiccLocalResourceServerTokenServices; +import com.cloud.kicc.common.security.exp.PermissionService; +import com.cloud.kicc.common.security.exp.PermitAllUrlProperties; +import com.cloud.kicc.common.security.exp.ResourceAuthExceptionEntryPoint; +import com.cloud.kicc.common.security.override.KiccBearerTokenExtractor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; + +/** + *

+ * 扩展资源服务器自动配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@EnableConfigurationProperties(PermitAllUrlProperties.class) +public class ResourceServerAutoConfiguration { + + /** 校验接口是否存在权限 */ + @Bean("pms") + public PermissionService permissionService() { + return new PermissionService(); + } + + /** 对公开权限的请求获取token不进行校验 */ + @Bean + public KiccBearerTokenExtractor kiccBearerTokenExtractor(PermitAllUrlProperties urlProperties) { + return new KiccBearerTokenExtractor(urlProperties); + } + + /** 细粒化客户端认证异常处理 */ + @Bean + @ConditionalOnMissingBean + public ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint() { + return new ResourceAuthExceptionEntryPoint(); + } + + /** 扩展资源服务器令牌服务 */ + @Bean + @Primary + public ResourceServerTokenServices resourceServerTokenServices() { + return new KiccLocalResourceServerTokenServices(); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/ResourceServerTokenRelayAutoConfiguration.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/ResourceServerTokenRelayAutoConfiguration.java new file mode 100644 index 0000000..e9c2e7b --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/ResourceServerTokenRelayAutoConfiguration.java @@ -0,0 +1,62 @@ +package com.cloud.kicc.common.security.config; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.AllNestedConditions; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration; +import org.springframework.cloud.commons.security.AccessTokenContextRelay; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration; + +import java.lang.annotation.*; + +/** + *

+ * 注入AccessTokenContextRelay 解决feign 传递token 为空问题 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@AutoConfigureAfter(OAuth2AutoConfiguration.class) +@ConditionalOnWebApplication +@ConditionalOnProperty("security.oauth2.client.client-id") +public class ResourceServerTokenRelayAutoConfiguration { + + @Bean + public AccessTokenContextRelay accessTokenContextRelay(OAuth2ClientContext context) { + return new AccessTokenContextRelay(context); + } + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Conditional(OAuth2OnClientInResourceServerCondition.class) + @interface ConditionalOnOAuth2ClientInResourceServer { + + } + + private static class OAuth2OnClientInResourceServerCondition extends AllNestedConditions { + + public OAuth2OnClientInResourceServerCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnBean(ResourceServerConfiguration.class) + static class Server { + + } + + @ConditionalOnBean(OAuth2ClientConfiguration.class) + static class Client { + + } + + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/SecurityMessageSourceConfiguration.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/SecurityMessageSourceConfiguration.java new file mode 100644 index 0000000..64795c0 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/SecurityMessageSourceConfiguration.java @@ -0,0 +1,29 @@ +package com.cloud.kicc.common.security.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET; + +/** + *

+ * 设置安全模块国际化配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@ConditionalOnWebApplication(type = SERVLET) +public class SecurityMessageSourceConfiguration implements WebMvcConfigurer { + + @Bean + public MessageSource messageSource() { + ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); + messageSource.addBasenames("classpath:org/springframework/security/messages"); + return messageSource; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/TokenStoreAutoCleanScheduleConfiguration.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/TokenStoreAutoCleanScheduleConfiguration.java new file mode 100644 index 0000000..d08877b --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/TokenStoreAutoCleanScheduleConfiguration.java @@ -0,0 +1,34 @@ +package com.cloud.kicc.common.security.config; + +import com.cloud.kicc.common.core.util.SpringContextHolderUtil; +import com.cloud.kicc.common.security.override.KiccRedisTokenStore; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; + +/** + *

+ * redis token store 自动配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +@EnableScheduling +@ConditionalOnBean(AuthorizationServerConfigurerAdapter.class) +public class TokenStoreAutoCleanScheduleConfiguration { + + /** + * 每小时执行一致,避免 redis zset 容量问题 + */ + @Scheduled(cron = "@hourly") + public void doMaintenance() { + KiccRedisTokenStore tokenStore = SpringContextHolderUtil.getBean(KiccRedisTokenStore.class); + long maintenance = tokenStore.doMaintenance(); + log.debug("清理Redis ZADD 过期 token 数量: {}", maintenance); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/TokenStoreAutoConfiguration.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/TokenStoreAutoConfiguration.java new file mode 100644 index 0000000..b9de424 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/config/TokenStoreAutoConfiguration.java @@ -0,0 +1,27 @@ +package com.cloud.kicc.common.security.config; + +import com.cloud.kicc.common.core.constant.CacheConstants; +import com.cloud.kicc.common.security.override.KiccRedisTokenStore; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.security.oauth2.provider.token.TokenStore; + +/** + *

+ * 令牌存储自动配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class TokenStoreAutoConfiguration { + + @Bean + public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) { + KiccRedisTokenStore tokenStore = new KiccRedisTokenStore(redisConnectionFactory); + tokenStore.setPrefix(CacheConstants.OAUTH_ACCESS); + return tokenStore; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/ForbiddenException.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/ForbiddenException.java new file mode 100644 index 0000000..4fff126 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/ForbiddenException.java @@ -0,0 +1,31 @@ +package com.cloud.kicc.common.security.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.springframework.http.HttpStatus; + +/** + *

+ * 禁止异常 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@JsonSerialize(using = KiccAuth2ExceptionSerializer.class) +public class ForbiddenException extends KiccAuth2Exception { + + public ForbiddenException(String msg, Throwable t) { + super(msg, t); + } + + @Override + public String getOAuth2ErrorCode() { + return "access_denied"; + } + + @Override + public int getHttpErrorCode() { + return HttpStatus.FORBIDDEN.value(); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/InvalidException.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/InvalidException.java new file mode 100644 index 0000000..7647af0 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/InvalidException.java @@ -0,0 +1,30 @@ +package com.cloud.kicc.common.security.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +/** + *

+ * 无效异常 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@JsonSerialize(using = KiccAuth2ExceptionSerializer.class) +public class InvalidException extends KiccAuth2Exception { + + public InvalidException(String msg, Throwable t) { + super(msg, t); + } + + @Override + public String getOAuth2ErrorCode() { + return "invalid_exception"; + } + + @Override + public int getHttpErrorCode() { + return 426; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/KiccAuth2Exception.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/KiccAuth2Exception.java new file mode 100644 index 0000000..4b4da9f --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/KiccAuth2Exception.java @@ -0,0 +1,34 @@ +package com.cloud.kicc.common.security.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import lombok.Getter; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; + +/** + *

+ * 自定义OAuth2Exception + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@JsonSerialize(using = KiccAuth2ExceptionSerializer.class) +public class KiccAuth2Exception extends OAuth2Exception { + + @Getter + private String errorCode; + + public KiccAuth2Exception(String msg) { + super(msg); + } + + public KiccAuth2Exception(String msg, Throwable t) { + super(msg, t); + } + + public KiccAuth2Exception(String msg, String errorCode) { + super(msg); + this.errorCode = errorCode; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/KiccAuth2ExceptionSerializer.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/KiccAuth2ExceptionSerializer.java new file mode 100644 index 0000000..e4746d1 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/KiccAuth2ExceptionSerializer.java @@ -0,0 +1,35 @@ +package com.cloud.kicc.common.security.exception; + +import com.cloud.kicc.common.core.enums.ExceptionEnum; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import lombok.SneakyThrows; + +/** + *

+ * OAuth2 异常格式化 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class KiccAuth2ExceptionSerializer extends StdSerializer { + + public KiccAuth2ExceptionSerializer() { + super(KiccAuth2Exception.class); + } + + @Override + @SneakyThrows + public void serialize(KiccAuth2Exception value, JsonGenerator gen, SerializerProvider provider) { + gen.writeStartObject(); + gen.writeObjectField("code", ExceptionEnum.UNAUTHORIZED_ACCESS.getValue()); + gen.writeStringField("msg", value.getMessage()); + gen.writeStringField("data", value.getErrorCode()); + // 资源服务器会读取这个字段 + gen.writeStringField("error", value.getMessage()); + gen.writeEndObject(); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/MethodNotAllowed.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/MethodNotAllowed.java new file mode 100644 index 0000000..6ff974d --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/MethodNotAllowed.java @@ -0,0 +1,31 @@ +package com.cloud.kicc.common.security.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.springframework.http.HttpStatus; + +/** + *

+ * 不允许的方法 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@JsonSerialize(using = KiccAuth2ExceptionSerializer.class) +public class MethodNotAllowed extends KiccAuth2Exception { + + public MethodNotAllowed(String msg, Throwable t) { + super(msg, t); + } + + @Override + public String getOAuth2ErrorCode() { + return "method_not_allowed"; + } + + @Override + public int getHttpErrorCode() { + return HttpStatus.METHOD_NOT_ALLOWED.value(); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/SecurityCheckedException.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/SecurityCheckedException.java new file mode 100644 index 0000000..211c22d --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/SecurityCheckedException.java @@ -0,0 +1,36 @@ +package com.cloud.kicc.common.security.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.springframework.http.HttpStatus; + +/** + *

+ * 安全检查异常 + * 可绕过前端设置的状态码提示消息 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/18 + */ +@JsonSerialize(using = KiccAuth2ExceptionSerializer.class) +public class SecurityCheckedException extends KiccAuth2Exception { + + public SecurityCheckedException(String msg) { + super(msg); + } + + public SecurityCheckedException(String msg, Throwable t) { + super(msg, t); + } + + @Override + public String getOAuth2ErrorCode() { + return "checked_not_pass"; + } + + @Override + public int getHttpErrorCode() { + return HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value(); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/ServerErrorException.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/ServerErrorException.java new file mode 100644 index 0000000..735a2d6 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/ServerErrorException.java @@ -0,0 +1,31 @@ +package com.cloud.kicc.common.security.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.springframework.http.HttpStatus; + +/** + *

+ * 服务器错误异常 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@JsonSerialize(using = KiccAuth2ExceptionSerializer.class) +public class ServerErrorException extends KiccAuth2Exception { + + public ServerErrorException(String msg, Throwable t) { + super(msg, t); + } + + @Override + public String getOAuth2ErrorCode() { + return "server_error"; + } + + @Override + public int getHttpErrorCode() { + return HttpStatus.INTERNAL_SERVER_ERROR.value(); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/TokenInvalidException.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/TokenInvalidException.java new file mode 100644 index 0000000..f9b1dcd --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/TokenInvalidException.java @@ -0,0 +1,31 @@ +package com.cloud.kicc.common.security.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.springframework.http.HttpStatus; + +/** + *

+ * 令牌不合法 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@JsonSerialize(using = KiccAuth2ExceptionSerializer.class) +public class TokenInvalidException extends KiccAuth2Exception { + + public TokenInvalidException(String msg, Throwable t) { + super(msg, t); + } + + @Override + public String getOAuth2ErrorCode() { + return "invalid_token"; + } + + @Override + public int getHttpErrorCode() { + return HttpStatus.FAILED_DEPENDENCY.value(); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/UnConfiguredUserDataException.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/UnConfiguredUserDataException.java new file mode 100644 index 0000000..2e28aab --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/UnConfiguredUserDataException.java @@ -0,0 +1,35 @@ +package com.cloud.kicc.common.security.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.springframework.http.HttpStatus; + +/** + *

+ * 未配置用户数据 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/9/2 + */ +@JsonSerialize(using = KiccAuth2ExceptionSerializer.class) +public class UnConfiguredUserDataException extends KiccAuth2Exception { + + public UnConfiguredUserDataException(String msg) { + super(msg); + } + + public UnConfiguredUserDataException(String msg, Throwable t) { + super(msg, t); + } + + @Override + public String getOAuth2ErrorCode() { + return "un_configured_user_data"; + } + + @Override + public int getHttpErrorCode() { + return HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value(); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/UnauthorizedException.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/UnauthorizedException.java new file mode 100644 index 0000000..cd8c74c --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exception/UnauthorizedException.java @@ -0,0 +1,31 @@ +package com.cloud.kicc.common.security.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.springframework.http.HttpStatus; + +/** + *

+ * 未经授权的异常 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@JsonSerialize(using = KiccAuth2ExceptionSerializer.class) +public class UnauthorizedException extends KiccAuth2Exception { + + public UnauthorizedException(String msg, Throwable t) { + super(msg, t); + } + + @Override + public String getOAuth2ErrorCode() { + return "unauthorized"; + } + + @Override + public int getHttpErrorCode() { + return HttpStatus.UNAUTHORIZED.value(); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/KiccLocalResourceServerTokenServices.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/KiccLocalResourceServerTokenServices.java new file mode 100644 index 0000000..ab564a5 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/KiccLocalResourceServerTokenServices.java @@ -0,0 +1,131 @@ +package com.cloud.kicc.common.security.exp; + +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.json.JSONUtil; +import com.cloud.kicc.common.core.constant.SecurityConstants; +import com.cloud.kicc.common.core.jackson.KiccJavaTimeModule; +import com.cloud.kicc.common.data.entity.CasUser; +import com.cloud.kicc.common.security.exception.UnConfiguredUserDataException; +import com.cloud.kicc.common.security.override.jackson2.SimpleGrantedAuthorityMixin; +import com.cloud.kicc.common.security.properties.CasProperties; +import com.cloud.kicc.common.security.template.UserProviderTemplate; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.util.internal.StringUtil; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; +import org.springframework.security.oauth2.provider.token.TokenStore; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * 本地资源服务器令牌服务 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@RequiredArgsConstructor +public class KiccLocalResourceServerTokenServices implements ResourceServerTokenServices { + + @Autowired + private TokenStore tokenStore; + + @Autowired(required = false) + private UserProviderTemplate userProviderTemplate; + + @Autowired + private CasProperties casProperties; + + @Override + @SneakyThrows + public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { + // 根据token加载身份验证 + OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken); + if (oAuth2Authentication == null) { + return null; + } + OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request(); + // 检测是否是属于认证的CasUser实体用户 + if (!(oAuth2Authentication.getPrincipal() instanceof CasUser)) { + return oAuth2Authentication; + } + CasUser casUser = (CasUser) oAuth2Authentication.getPrincipal(); + + // 设置SSO子系统扩展用户信息 + if (casUser.getExPrincipals().get(casProperties.getIdentity()) == null) { + Object user = userProviderTemplate.selectByCasUserId(casUser.getId()); + if (ObjectUtil.isEmpty(user)) { + throw new UnConfiguredUserDataException("System user not found Contact your system administrator for configuration!", null); + } + + String[] permissions = (String[])ReflectUtil.getMethodByName(user.getClass(), "getPermissions").invoke(user); + List authorities = Arrays.stream(permissions) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + Object exUser = ReflectUtil.newInstance(ClassUtil.loadClass(casProperties.getUserClass()), + casUser.getUsername(), + StringUtil.EMPTY_STRING, + casUser.isEnabled(), + casUser.isAccountNonExpired(), + casUser.isCredentialsNonExpired(), + casUser.isAccountNonLocked(), + authorities + ); + BeanUtils.copyProperties(user, exUser); + casUser.getExPrincipals().put(casProperties.getIdentity(), new ObjectMapper() + .registerModule(new KiccJavaTimeModule()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .writeValueAsString(exUser)); + tokenStore.storeAccessToken(tokenStore.getAccessToken(oAuth2Authentication), oAuth2Authentication); + } + + // 覆盖casUser核心authorities + String str = casUser.getExPrincipals().get(casProperties.getIdentity()); + if (!JSONUtil.isJson(str)) throw new UnConfiguredUserDataException("ExPrincipals not json strings!"); + Object exUser = new ObjectMapper() + .registerModule(new KiccJavaTimeModule()) + .addMixIn(SimpleGrantedAuthority.class, SimpleGrantedAuthorityMixin.class) + .readValue(str, Class.forName(casProperties.getUserClass())); + Collection authorities = (Collection)ReflectUtil.getMethodByName(exUser.getClass(), "getAuthorities").invoke(exUser); + CasUser exCasUser = new CasUser( + casUser.getUsername(), + SecurityConstants.MOCK_PASSWORD, + casUser.isEnabled(), + casUser.isAccountNonExpired(), + casUser.isCredentialsNonExpired(), + casUser.isAccountNonLocked(), + authorities); + BeanUtils.copyProperties(casUser, exCasUser); + + // 每次请求前都预先加载用户名密码身份验证令牌 + Authentication userAuthentication = new UsernamePasswordAuthenticationToken(exCasUser, "N/A", exCasUser.getAuthorities()); + OAuth2Authentication authentication = new OAuth2Authentication(oAuth2Request, userAuthentication); + authentication.setAuthenticated(true); + return authentication; + } + + @Override + public OAuth2AccessToken readAccessToken(String accessToken) { + throw new UnsupportedOperationException("Not supported: read access token"); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/KiccResourceServerConfigurerAdapter.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/KiccResourceServerConfigurerAdapter.java new file mode 100644 index 0000000..ac307a9 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/KiccResourceServerConfigurerAdapter.java @@ -0,0 +1,64 @@ +package com.cloud.kicc.common.security.exp; + +import com.cloud.kicc.common.security.override.KiccBearerTokenExtractor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; + +/** + *

+ * 资源服务配置适配器 + * 覆盖内部不合适的实现,实现适配 + * 1. todo: 支持remoteTokenServices 负载均衡 待实现 + * 2. 支持 获取用户全部信息 + * 3. 接口对外暴露,不校验 Authentication Header 头 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +public class KiccResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter { + + @Autowired + protected ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint; + + //protected RemoteTokenServices remoteTokenServices; + + @Autowired + private PermitAllUrlProperties permitAllUrl; + + @Autowired + private KiccBearerTokenExtractor kiccBearerTokenExtractor; + + @Autowired + private ResourceServerTokenServices resourceServerTokenServices; + + /** + * 默认的配置,对外暴露 + * @param httpSecurity + */ + @Override + @SneakyThrows + public void configure(HttpSecurity httpSecurity) { + // 允许使用iframe 嵌套,避免swagger-ui 不被加载的问题 + httpSecurity.headers().frameOptions().disable(); + // 设置所有的请求必须通过授权认证才可以访问 + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests(); + // 设置添加了系统内部调用注解,才对外提供访问,也可以理解SOA架构互相调用,以及配置了忽略url + permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll()); + registry.anyRequest().authenticated().and().csrf().disable(); + } + + @Override + public void configure(ResourceServerSecurityConfigurer resources) { + resources.authenticationEntryPoint(resourceAuthExceptionEntryPoint).tokenExtractor(kiccBearerTokenExtractor) + .tokenServices(resourceServerTokenServices); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/KiccSecurityBeanDefinitionRegistrar.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/KiccSecurityBeanDefinitionRegistrar.java new file mode 100644 index 0000000..273f3d1 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/KiccSecurityBeanDefinitionRegistrar.java @@ -0,0 +1,41 @@ +package com.cloud.kicc.common.security.exp; + +import com.cloud.kicc.common.core.constant.SecurityConstants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; + +/** + *

+ * 安全 Bean 定义注册器 + * 加载自定义Bean + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +public class KiccSecurityBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { + + /** + * 根据注解值动态注入资源服务器的相关属性 + * @param metadata 注解信息 + * @param registry 注册器 + */ + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + // 覆盖资源认证服务配置适配器 + if (registry.isBeanNameInUse(SecurityConstants.RESOURCE_SERVER_CONFIGURER)) { + log.warn("本地存在资源服务器配置,覆盖默认配置:" + SecurityConstants.RESOURCE_SERVER_CONFIGURER); + return; + } + + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); + beanDefinition.setBeanClass(KiccResourceServerConfigurerAdapter.class); + registry.registerBeanDefinition(SecurityConstants.RESOURCE_SERVER_CONFIGURER, beanDefinition); + + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/PermissionService.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/PermissionService.java new file mode 100644 index 0000000..16d4d97 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/PermissionService.java @@ -0,0 +1,59 @@ +package com.cloud.kicc.common.security.exp; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + *

+ * 接口权限判断 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class PermissionService { + + /** + * 判断接口是否有任意xxx,xxx权限 + * @param permissions 权限 + * @return {boolean} + */ + public boolean hasPermission(String... permissions) { + if (ArrayUtil.isEmpty(permissions)) { + return false; + } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return false; + } + Collection authorities = authentication.getAuthorities(); + return authorities.stream().map(GrantedAuthority::getAuthority).filter(StringUtils::hasText) + .anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x)); + } + + /** + * 验证用户是否具有以下任意一个权限 + * + * @param permissions 以 ,为分隔符的权限列表 + * @return 用户是否具有以下任意一个权限 + */ + public boolean hasAnyPerm(String permissions) { + if (StrUtil.isEmpty(permissions)) { + return false; + } + for (String permission : permissions.split(",")) { + if (permission != null && hasPermission(permission)) { + return true; + } + } + return false; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/PermitAllUrlProperties.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/PermitAllUrlProperties.java new file mode 100644 index 0000000..b58b583 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/PermitAllUrlProperties.java @@ -0,0 +1,72 @@ +package com.cloud.kicc.common.security.exp; + +import cn.hutool.core.util.ReUtil; +import com.cloud.kicc.common.security.annotation.Inner; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + *

+ * 资源服务器对外直接暴露URL,如果设置 contex-path 要特殊处理 + * 主要处理拿取加了Inner注解的方法上的url映射地址,以及配置文件中配置的security.oauth2.ignore.urls,进行对外开放不拦截 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +@ConfigurationProperties(prefix = "security.oauth2.ignore") +public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware { + + private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}"); + + private ApplicationContext applicationContext; + + @Getter + @Setter + private List urls = new ArrayList<>(); + + /** bean初始化后执行 */ + @Override + public void afterPropertiesSet() { + RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); + Map map = mapping.getHandlerMethods(); + + // 处理所有controller请求映射方法 + map.keySet().forEach(info -> { + HandlerMethod handlerMethod = map.get(info); + + // 获取方法上边的注解,替代path variable 为 *,避免前端传递过来的参数变量被拦截 + Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class); + Optional.ofNullable(method).ifPresent(inner -> info.getPatternsCondition().getPatterns() + .forEach(url -> urls.add(ReUtil.replaceAll(url, PATTERN, "*")))); + + // 获取类上边的注解,替代path variable 为 *,避免前端传递过来的参数变量被拦截 + Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class); + Optional.ofNullable(controller).ifPresent(inner -> info.getPatternsCondition().getPatterns() + .forEach(url -> urls.add(ReUtil.replaceAll(url, PATTERN, "*")))); + }); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.applicationContext = context; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/ResourceAuthExceptionEntryPoint.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/ResourceAuthExceptionEntryPoint.java new file mode 100644 index 0000000..ff4e9cc --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/exp/ResourceAuthExceptionEntryPoint.java @@ -0,0 +1,61 @@ +package com.cloud.kicc.common.security.exp; + +import cn.hutool.http.HttpStatus; +import com.cloud.kicc.common.core.api.R; +import com.cloud.kicc.common.core.constant.CommonConstants; +import com.cloud.kicc.common.core.enums.ExceptionEnum; +import com.cloud.kicc.common.security.exception.UnConfiguredUserDataException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; + +/** + *

+ * 客户端认证异常处理 + * 可以根据 AuthenticationException 不同细化异常处理 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint { + + @Autowired + private ObjectMapper objectMapper; + + @Override + @SneakyThrows + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) { + response.setCharacterEncoding(CommonConstants.UTF8); + response.setContentType(CommonConstants.CONTENT_TYPE); + R result = new R<>(); + result.setCode(ExceptionEnum.UNAUTHORIZED_ACCESS.getValue()); + response.setStatus(HttpStatus.HTTP_UNAUTHORIZED); + if (authException != null) { + result.setMsg("error"); + result.setData(authException.getMessage()); + } + + // 针对令牌过期返回特殊的 424 + if (authException instanceof InsufficientAuthenticationException) { + response.setStatus(org.springframework.http.HttpStatus.FAILED_DEPENDENCY.value()); + result.setMsg("token expire"); + + // 未配置用户数据 + if (authException.getCause() instanceof UnConfiguredUserDataException) { + result.setCode(ExceptionEnum.PAGE_NOT_DATA.getValue()); + } + } + PrintWriter printWriter = response.getWriter(); + printWriter.append(objectMapper.writeValueAsString(result)); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/app/CustomAppAuthenticationToken.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/app/CustomAppAuthenticationToken.java new file mode 100644 index 0000000..f21b5d1 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/app/CustomAppAuthenticationToken.java @@ -0,0 +1,54 @@ +package com.cloud.kicc.common.security.grant.app; + +import lombok.Getter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.UserDetails; + +/** + *

+ * 手机验证码登录身份验证令牌 + * 授权类型:app + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class CustomAppAuthenticationToken extends AbstractAuthenticationToken { + + private final Object principal; + + /** 验证码/密码 */ + private String code; + + /** + * 授权类型 + */ + @Getter + private String grantType; + + public CustomAppAuthenticationToken(String phone, String code, String grantType) { + super(AuthorityUtils.NO_AUTHORITIES); + this.principal = phone; + this.code = code; + this.grantType = grantType; + } + + public CustomAppAuthenticationToken(UserDetails sysUser) { + super(sysUser.getAuthorities()); + this.principal = sysUser; + // 设置认证成功 必须 + super.setAuthenticated(true); + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public Object getCredentials() { + return this.code; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/app/ResourceOwnerCustomeAppTokenGranter.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/app/ResourceOwnerCustomeAppTokenGranter.java new file mode 100644 index 0000000..5774a12 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/app/ResourceOwnerCustomeAppTokenGranter.java @@ -0,0 +1,82 @@ +package com.cloud.kicc.common.security.grant.app; + +import cn.hutool.core.util.StrUtil; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AccountStatusException; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; +import org.springframework.security.oauth2.provider.*; +import org.springframework.security.oauth2.provider.token.AbstractTokenGranter; +import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + *

+ * 资源所有者电话令牌授予者 + * 支持手机验证码登录 + * 授权类型:app + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +public class ResourceOwnerCustomeAppTokenGranter extends AbstractTokenGranter { + + private static final String GRANT_TYPE = "app"; + + private final AuthenticationManager authenticationManager; + + public ResourceOwnerCustomeAppTokenGranter(AuthenticationManager authenticationManager, + AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, + OAuth2RequestFactory requestFactory) { + this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE); + } + + protected ResourceOwnerCustomeAppTokenGranter(AuthenticationManager authenticationManager, + AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, + OAuth2RequestFactory requestFactory, String grantType) { + super(tokenServices, clientDetailsService, requestFactory, grantType); + this.authenticationManager = authenticationManager; + } + + @Override + protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { + + Map parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters()); + + // 手机号 + String mobile = parameters.get("mobile"); + // 验证码/密码 + String code = parameters.get("code"); + + if (StrUtil.isBlank(mobile) || StrUtil.isBlank(code)) { + throw new InvalidGrantException("Bad credentials [ params must be has phone with code ]"); + } + + // Protect from downstream leaks of code + parameters.remove("code"); + + Authentication userAuth = new CustomAppAuthenticationToken(mobile, code, tokenRequest.getGrantType()); + ((AbstractAuthenticationToken) userAuth).setDetails(parameters); + try { + userAuth = authenticationManager.authenticate(userAuth); + } + catch (AccountStatusException | BadCredentialsException ase) { + // covers expired, locked, disabled cases (mentioned in section 5.2, draft 31) + throw new InvalidGrantException(ase.getMessage()); + } + // If the phone/code are wrong the spec says we should send 400/invalid grant + + if (userAuth == null || !userAuth.isAuthenticated()) { + throw new InvalidGrantException("Could not authenticate user: " + mobile); + } + + OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest); + return new OAuth2Authentication(storedOAuth2Request, userAuth); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/provider/CustomAppAuthenticationProvider.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/provider/CustomAppAuthenticationProvider.java new file mode 100644 index 0000000..9fd00a6 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/provider/CustomAppAuthenticationProvider.java @@ -0,0 +1,106 @@ +package com.cloud.kicc.common.security.grant.provider; + +import cn.hutool.extra.spring.SpringUtil; +import com.cloud.kicc.common.security.grant.app.CustomAppAuthenticationToken; +import com.cloud.kicc.common.security.service.KiccUserDetailsService; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.security.authentication.AccountStatusUserDetailsChecker; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsChecker; + +import java.util.Comparator; +import java.util.Map; +import java.util.Optional; + +/** + *

+ * 自定义应用身份验证提供程序 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +public class CustomAppAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { + + /** + * user 属性校验 + */ + @Setter + private UserDetailsChecker preAuthenticationChecks = new AccountStatusUserDetailsChecker(); + + /** + * 校验 请求信息userDetails + * @param userDetails 用户信息 + * @param authentication 认证信息 + * @throws AuthenticationException + */ + @Override + protected void additionalAuthenticationChecks(UserDetails userDetails, + UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { + + if (authentication.getCredentials() == null) { + this.logger.debug("Failed to authenticate since no credentials provided"); + throw new BadCredentialsException(this.messages + .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); + } + + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + if (authentication.getCredentials() == null) { + log.debug("Failed to authenticate since no credentials provided"); + throw new BadCredentialsException("Bad credentials"); + } + + CustomAppAuthenticationToken requestToken = (CustomAppAuthenticationToken) authentication; + + // 此处已获得 客户端认证 获取对应 userDetailsService + Authentication clientAuthentication = SecurityContextHolder.getContext().getAuthentication(); + String clientId = clientAuthentication.getName(); + Map userDetailsServiceMap = SpringUtil + .getBeansOfType(KiccUserDetailsService.class); + Optional optional = userDetailsServiceMap.values().stream() + .filter(service -> service.support(clientId, requestToken.getGrantType())) + .max(Comparator.comparingInt(Ordered::getOrder)); + + if (!optional.isPresent()) { + throw new InternalAuthenticationServiceException("UserDetailsService error , not register"); + } + + // 手机号 + String phone = authentication.getName(); + UserDetails userDetails = optional.get().loadUserByUsername(phone); + + // userDetails 校验 + preAuthenticationChecks.check(userDetails); + + CustomAppAuthenticationToken token = new CustomAppAuthenticationToken(userDetails); + token.setDetails(authentication.getDetails()); + return token; + } + + @Override + protected UserDetails retrieveUser(String phone, UsernamePasswordAuthenticationToken authentication) + throws AuthenticationException { + return null; + } + + /** 这里定义provider是否被调用,需要执行结果为true才会执行认证逻辑 */ + @Override + public boolean supports(Class authentication) { + return authentication.isAssignableFrom(CustomAppAuthenticationToken.class); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/provider/KiccDaoAuthenticationProvider.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/provider/KiccDaoAuthenticationProvider.java new file mode 100644 index 0000000..35fd82d --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/grant/provider/KiccDaoAuthenticationProvider.java @@ -0,0 +1,173 @@ +package com.cloud.kicc.common.security.grant.provider; + +import cn.hutool.extra.spring.SpringUtil; +import com.cloud.kicc.common.core.util.WebUtil; +import com.cloud.kicc.common.security.service.KiccUserDetailsService; +import org.springframework.core.Ordered; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.Assert; + +import java.util.Comparator; +import java.util.Map; +import java.util.Optional; + +/** + *

+ * 用户信息身份验证 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class KiccDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { + + /** + * The plaintext password used to perform PasswordEncoder#matches(CharSequence, + * String)} on when the user is not found to avoid SEC-2056. + */ + private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword"; + + private PasswordEncoder passwordEncoder; + + /** + * The password used to perform {@link PasswordEncoder#matches(CharSequence, String)} + * on when the user is not found to avoid SEC-2056. This is necessary, because some + * {@link PasswordEncoder} implementations will short circuit if the password is not + * in a valid format. + */ + private volatile String userNotFoundEncodedPassword; + + private UserDetailsService userDetailsService; + + private UserDetailsPasswordService userDetailsPasswordService; + + public KiccDaoAuthenticationProvider() { + setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); + } + + @Override + @SuppressWarnings("deprecation") + protected void additionalAuthenticationChecks(UserDetails userDetails, + UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { + if (authentication.getCredentials() == null) { + this.logger.debug("Failed to authenticate since no credentials provided"); + throw new BadCredentialsException(this.messages + .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); + } + String presentedPassword = authentication.getCredentials().toString(); + if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { + this.logger.debug("Failed to authenticate since password does not match stored value"); + throw new BadCredentialsException(this.messages + .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); + } + } + + @Override + protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) + throws AuthenticationException { + prepareTimingAttackProtection(); + + // 此处已获得 客户端认证 获取对应 userDetailsService + Authentication clientAuthentication = SecurityContextHolder.getContext().getAuthentication(); + + // SSO NPE 处理 + String clientId; + if (clientAuthentication == null) { + clientId = WebUtil.getRequest().get().getParameter("clientId"); + } + else { + clientId = clientAuthentication.getName(); + } + + Map userDetailsServiceMap = SpringUtil + .getBeansOfType(KiccUserDetailsService.class); + Optional optional = userDetailsServiceMap.values().stream() + .filter(service -> service.support(clientId, null)).max(Comparator.comparingInt(Ordered::getOrder)); + + if (!optional.isPresent()) { + throw new InternalAuthenticationServiceException("UserDetailsService error , not register"); + } + + try { + UserDetails loadedUser = optional.get().loadUserByUsername(username); + if (loadedUser == null) { + throw new InternalAuthenticationServiceException( + "UserDetailsService returned null, which is an interface contract violation"); + } + return loadedUser; + } catch (UsernameNotFoundException ex) { + mitigateAgainstTimingAttack(authentication); + throw ex; + } catch (InternalAuthenticationServiceException ex) { + throw ex; + } + } + + @Override + protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, + UserDetails user) { + boolean upgradeEncoding = this.userDetailsPasswordService != null + && this.passwordEncoder.upgradeEncoding(user.getPassword()); + if (upgradeEncoding) { + String presentedPassword = authentication.getCredentials().toString(); + String newPassword = this.passwordEncoder.encode(presentedPassword); + user = this.userDetailsPasswordService.updatePassword(user, newPassword); + } + return super.createSuccessAuthentication(principal, authentication, user); + } + + private void prepareTimingAttackProtection() { + if (this.userNotFoundEncodedPassword == null) { + this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD); + } + } + + private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) { + if (authentication.getCredentials() != null) { + String presentedPassword = authentication.getCredentials().toString(); + this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword); + } + } + + /** + * Sets the PasswordEncoder instance to be used to encode and validate passwords. If + * not set, the password will be compared using + * {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()} + * @param passwordEncoder must be an instance of one of the {@code PasswordEncoder} + * types. + */ + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); + this.passwordEncoder = passwordEncoder; + this.userNotFoundEncodedPassword = null; + } + + protected PasswordEncoder getPasswordEncoder() { + return this.passwordEncoder; + } + + public void setUserDetailsService(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + protected UserDetailsService getUserDetailsService() { + return this.userDetailsService; + } + + public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) { + this.userDetailsPasswordService = userDetailsPasswordService; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/AbstractAuthenticationFailureEventHandler.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/AbstractAuthenticationFailureEventHandler.java new file mode 100644 index 0000000..b3ed20a --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/AbstractAuthenticationFailureEventHandler.java @@ -0,0 +1,39 @@ +package com.cloud.kicc.common.security.handler; + +import org.springframework.context.ApplicationListener; +import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +/** + *

+ * 认证失败事件处理器 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public abstract class AbstractAuthenticationFailureEventHandler + implements ApplicationListener { + + /** + * Handle an application event. + * @param event the event to respond to + */ + @Override + public void onApplicationEvent(AbstractAuthenticationFailureEvent event) { + AuthenticationException authenticationException = event.getException(); + Authentication authentication = (Authentication) event.getSource(); + + handle(authenticationException, authentication); + } + + /** + * 处理登录成功方法 + *

+ * @param authenticationException 登录的authentication 对象 + * @param authentication 登录的authenticationException 对象 + */ + public abstract void handle(AuthenticationException authenticationException, Authentication authentication); + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/AbstractAuthenticationSuccessEventHandler.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/AbstractAuthenticationSuccessEventHandler.java new file mode 100644 index 0000000..2e2e5cc --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/AbstractAuthenticationSuccessEventHandler.java @@ -0,0 +1,39 @@ +package com.cloud.kicc.common.security.handler; + +import cn.hutool.core.collection.CollUtil; +import org.springframework.context.ApplicationListener; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.core.Authentication; + +/** + *

+ * 认证成功事件处理器 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public abstract class AbstractAuthenticationSuccessEventHandler + implements ApplicationListener { + + /** + * Handle an application event. + * @param event the event to respond to + */ + @Override + public void onApplicationEvent(AuthenticationSuccessEvent event) { + Authentication authentication = (Authentication) event.getSource(); + if (CollUtil.isNotEmpty(authentication.getAuthorities())) { + handle(authentication); + } + } + + /** + * 处理登录成功方法 + *

+ * 获取到登录的authentication 对象 + * @param authentication 登录对象 + */ + public abstract void handle(Authentication authentication); + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/AbstractLogoutSuccessEventHandler.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/AbstractLogoutSuccessEventHandler.java new file mode 100644 index 0000000..86de56b --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/AbstractLogoutSuccessEventHandler.java @@ -0,0 +1,38 @@ +package com.cloud.kicc.common.security.handler; + +import cn.hutool.core.collection.CollUtil; +import org.springframework.context.ApplicationListener; +import org.springframework.security.authentication.event.LogoutSuccessEvent; +import org.springframework.security.core.Authentication; + +/** + *

+ * 退出成功事件处理器 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public abstract class AbstractLogoutSuccessEventHandler implements ApplicationListener { + + /** + * Handle an application event. + * @param event the event to respond to + */ + @Override + public void onApplicationEvent(LogoutSuccessEvent event) { + Authentication authentication = (Authentication) event.getSource(); + if (CollUtil.isNotEmpty(authentication.getAuthorities())) { + handle(authentication); + } + } + + /** + * 处理退出成功方法 + *

+ * 获取到登录的authentication 对象 + * @param authentication 登录对象 + */ + public abstract void handle(Authentication authentication); + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/FormAuthenticationFailureHandler.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/FormAuthenticationFailureHandler.java new file mode 100644 index 0000000..0a39cd9 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/FormAuthenticationFailureHandler.java @@ -0,0 +1,41 @@ +package com.cloud.kicc.common.security.handler; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.http.HttpUtil; +import com.cloud.kicc.common.core.util.WebUtil; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + *

+ * 表单登录失败处理逻辑 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +public class FormAuthenticationFailureHandler implements AuthenticationFailureHandler { + + /** + * Called when an authentication attempt fails. + * @param request the request during which the authentication attempt occurred. + * @param response the response. + * @param exception the exception which was thrown to reject the authentication + */ + @Override + @SneakyThrows + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) { + log.debug("表单登录失败:{}", exception.getLocalizedMessage()); + String url = HttpUtil.encodeParams(String.format("/token/login?error=%s", exception.getMessage()), + CharsetUtil.CHARSET_UTF_8); + WebUtil.getResponse().sendRedirect(url); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/SsoLogoutSuccessHandler.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/SsoLogoutSuccessHandler.java new file mode 100644 index 0000000..7bf6c12 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/handler/SsoLogoutSuccessHandler.java @@ -0,0 +1,43 @@ +package com.cloud.kicc.common.security.handler; + +import cn.hutool.core.util.StrUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + *

+ * sso 退出功能 ,根据客户端传入跳转 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class SsoLogoutSuccessHandler implements LogoutSuccessHandler { + + private static final String REDIRECT_URL = "redirect_url"; + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException { + if (response == null) { + return; + } + + // 获取请求参数中是否包含 回调地址 + String redirectUrl = request.getParameter(REDIRECT_URL); + if (StrUtil.isNotBlank(redirectUrl)) { + response.sendRedirect(redirectUrl); + } + else { + // 默认跳转referer 地址 + String referer = request.getHeader(HttpHeaders.REFERER); + response.sendRedirect(referer); + } + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccBearerTokenExtractor.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccBearerTokenExtractor.java new file mode 100644 index 0000000..1e121b0 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccBearerTokenExtractor.java @@ -0,0 +1,39 @@ +package com.cloud.kicc.common.security.override; + +import com.cloud.kicc.common.security.exp.PermitAllUrlProperties; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; + +import javax.servlet.http.HttpServletRequest; + +/** + *

+ * 重写 {@link BearerTokenExtractor} + * 对公开权限的请求不进行抓取校验直接返回null放过 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +public class KiccBearerTokenExtractor extends BearerTokenExtractor { + + private final PathMatcher pathMatcher; + + private final PermitAllUrlProperties urlProperties; + + public KiccBearerTokenExtractor(PermitAllUrlProperties urlProperties) { + this.urlProperties = urlProperties; + this.pathMatcher = new AntPathMatcher(); + } + + @Override + public Authentication extract(HttpServletRequest request) { + boolean match = urlProperties.getUrls().stream() + .anyMatch(url -> pathMatcher.match(url, request.getRequestURI())); + + return match ? null : super.extract(request); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccClientDetailsService.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccClientDetailsService.java new file mode 100644 index 0000000..5b6611a --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccClientDetailsService.java @@ -0,0 +1,38 @@ +package com.cloud.kicc.common.security.override; + +import com.cloud.kicc.common.core.constant.CacheConstants; +import lombok.SneakyThrows; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; + +import javax.sql.DataSource; + +/** + *

+ * JdbcClientDetailsService + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class KiccClientDetailsService extends JdbcClientDetailsService { + + public KiccClientDetailsService(DataSource dataSource) { + super(dataSource); + } + + /** + * 重写原生方法 + * 加载oauth2.0客户端支持redis缓存 + * @param clientId + * @return + */ + @Override + @SneakyThrows + @Cacheable(value = CacheConstants.OAUTH_CLIENT_DETAILS, key = "#clientId", unless = "#result == null") + public ClientDetails loadClientByClientId(String clientId) { + return super.loadClientByClientId(clientId); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccCustomTokenServices.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccCustomTokenServices.java new file mode 100644 index 0000000..480dc4b --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccCustomTokenServices.java @@ -0,0 +1,400 @@ +package com.cloud.kicc.common.security.override; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.*; +import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; +import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.*; +import org.springframework.security.oauth2.provider.token.*; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import java.util.Date; +import java.util.Set; +import java.util.UUID; + +/** + *

+ * 自定义 token 放发处理逻辑 + * 重写DefaultTokenServices,如果身份验证已更改在创建时重新获取token并删除旧token + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class KiccCustomTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices, + ConsumerTokenServices, InitializingBean { + + // default 30 days. + private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; + + // default 12 hours. + private int accessTokenValiditySeconds = 60 * 60 * 12; + + private boolean supportRefreshToken = false; + + private boolean reuseRefreshToken = true; + + private TokenStore tokenStore; + + private ClientDetailsService clientDetailsService; + + private TokenEnhancer accessTokenEnhancer; + + private AuthenticationManager authenticationManager; + + /** + * Initialize these token services. If no random generator is set, one will be created. + */ + @Override + public void afterPropertiesSet() { + Assert.notNull(tokenStore, "tokenStore must be set"); + } + + @Override + public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { + // OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication); + OAuth2RefreshToken refreshToken = null; + + /*if (existingAccessToken != null) { + if (existingAccessToken.isExpired()) { + if (existingAccessToken.getRefreshToken() != null) { + refreshToken = existingAccessToken.getRefreshToken(); + // 当访问令牌被删除时,令牌存储可以删除刷新令牌,但是我们想要确保一定可以删除刷新令牌 + tokenStore.removeRefreshToken(refreshToken); + } + tokenStore.removeAccessToken(existingAccessToken); + } else { + // 重新存储访问令牌,以防身份验证发生更改 + tokenStore.storeAccessToken(existingAccessToken, authentication); + return existingAccessToken; + } + }*/ + + // 只有在没有与过期的访问令牌关联的现有令牌时,才创建新的刷新令牌。 + // 客户端可能持有现有的刷新令牌,所以我们在旧访问令牌过期的情况下重用它。 + if (refreshToken == null) { + refreshToken = createRefreshToken(authentication); + } + // 但是如果刷新令牌已过期,则可能需要重新颁发它本身。 + /*else if (refreshToken instanceof ExpiringOAuth2RefreshToken) { + ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken; + if (System.currentTimeMillis() > expiring.getExpiration().getTime()) { + refreshToken = createRefreshToken(authentication); + } + }*/ + + OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); + tokenStore.storeAccessToken(accessToken, authentication); + // 以防它被修改 + refreshToken = accessToken.getRefreshToken(); + if (refreshToken != null) { + tokenStore.storeRefreshToken(refreshToken, authentication); + } + return accessToken; + } + + @Override + @Transactional(noRollbackFor = { InvalidTokenException.class, InvalidGrantException.class }) + public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) + throws AuthenticationException { + + if (!supportRefreshToken) { + throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue); + } + + OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue); + if (refreshToken == null) { + throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue); + } + + OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken); + if (this.authenticationManager != null && !authentication.isClientOnly()) { + // The client has already been authenticated, but the user authentication + // might be old now, so give it a + // chance to re-authenticate. + Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", + authentication.getAuthorities()); + user = authenticationManager.authenticate(user); + Object details = authentication.getDetails(); + authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user); + authentication.setDetails(details); + } + String clientId = authentication.getOAuth2Request().getClientId(); + if (clientId == null || !clientId.equals(tokenRequest.getClientId())) { + throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue); + } + + // clear out any access tokens already associated with the refresh + // token. + tokenStore.removeAccessTokenUsingRefreshToken(refreshToken); + + if (isExpired(refreshToken)) { + tokenStore.removeRefreshToken(refreshToken); + throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken); + } + + authentication = createRefreshedAuthentication(authentication, tokenRequest); + + if (!reuseRefreshToken) { + tokenStore.removeRefreshToken(refreshToken); + refreshToken = createRefreshToken(authentication); + } + + OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); + tokenStore.storeAccessToken(accessToken, authentication); + if (!reuseRefreshToken) { + tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication); + } + return accessToken; + } + + @Override + public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { + return tokenStore.getAccessToken(authentication); + } + + /** + * Create a refreshed authentication. + * @param authentication The authentication. + * @param request The scope for the refreshed token. + * @return The refreshed authentication. + * @throws InvalidScopeException If the scope requested is invalid or wider than the + * original scope. + */ + private OAuth2Authentication createRefreshedAuthentication(OAuth2Authentication authentication, + TokenRequest request) { + OAuth2Authentication narrowed = authentication; + Set scope = request.getScope(); + OAuth2Request clientAuth = authentication.getOAuth2Request().refresh(request); + if (scope != null && !scope.isEmpty()) { + Set originalScope = clientAuth.getScope(); + if (originalScope == null || !originalScope.containsAll(scope)) { + throw new InvalidScopeException( + "Unable to narrow the scope of the client authentication to " + scope + ".", originalScope); + } + else { + clientAuth = clientAuth.narrowScope(scope); + } + } + narrowed = new OAuth2Authentication(clientAuth, authentication.getUserAuthentication()); + return narrowed; + } + + protected boolean isExpired(OAuth2RefreshToken refreshToken) { + if (refreshToken instanceof ExpiringOAuth2RefreshToken) { + ExpiringOAuth2RefreshToken expiringToken = (ExpiringOAuth2RefreshToken) refreshToken; + return expiringToken.getExpiration() == null + || System.currentTimeMillis() > expiringToken.getExpiration().getTime(); + } + return false; + } + + @Override + public OAuth2AccessToken readAccessToken(String accessToken) { + return tokenStore.readAccessToken(accessToken); + } + + @Override + public OAuth2Authentication loadAuthentication(String accessTokenValue) + throws AuthenticationException, InvalidTokenException { + OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue); + if (accessToken == null) { + throw new InvalidTokenException("Invalid access token: " + accessTokenValue); + } + else if (accessToken.isExpired()) { + tokenStore.removeAccessToken(accessToken); + throw new InvalidTokenException("Access token expired: " + accessTokenValue); + } + + OAuth2Authentication result = tokenStore.readAuthentication(accessToken); + if (result == null) { + // in case of race condition + throw new InvalidTokenException("Invalid access token: " + accessTokenValue); + } + if (clientDetailsService != null) { + String clientId = result.getOAuth2Request().getClientId(); + try { + clientDetailsService.loadClientByClientId(clientId); + } + catch (ClientRegistrationException e) { + throw new InvalidTokenException("Client not valid: " + clientId, e); + } + } + return result; + } + + public String getClientId(String tokenValue) { + OAuth2Authentication authentication = tokenStore.readAuthentication(tokenValue); + if (authentication == null) { + throw new InvalidTokenException("Invalid access token: " + tokenValue); + } + OAuth2Request clientAuth = authentication.getOAuth2Request(); + if (clientAuth == null) { + throw new InvalidTokenException("Invalid access token (no client id): " + tokenValue); + } + return clientAuth.getClientId(); + } + + @Override + public boolean revokeToken(String tokenValue) { + OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue); + if (accessToken == null) { + return false; + } + if (accessToken.getRefreshToken() != null) { + tokenStore.removeRefreshToken(accessToken.getRefreshToken()); + } + tokenStore.removeAccessToken(accessToken); + return true; + } + + private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) { + if (!isSupportRefreshToken(authentication.getOAuth2Request())) { + return null; + } + int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request()); + String value = UUID.randomUUID().toString(); + if (validitySeconds > 0) { + return new DefaultExpiringOAuth2RefreshToken(value, + new Date(System.currentTimeMillis() + (validitySeconds * 1000L))); + } + return new DefaultOAuth2RefreshToken(value); + } + + private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) { + DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString()); + int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request()); + if (validitySeconds > 0) { + token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L))); + } + token.setRefreshToken(refreshToken); + token.setScope(authentication.getOAuth2Request().getScope()); + + return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token; + } + + /** + * The access token validity period in seconds + * @param clientAuth the current authorization request + * @return the access token validity period in seconds + */ + protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) { + if (clientDetailsService != null) { + ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); + Integer validity = client.getAccessTokenValiditySeconds(); + if (validity != null) { + return validity; + } + } + return accessTokenValiditySeconds; + } + + /** + * The refresh token validity period in seconds + * @param clientAuth the current authorization request + * @return the refresh token validity period in seconds + */ + protected int getRefreshTokenValiditySeconds(OAuth2Request clientAuth) { + if (clientDetailsService != null) { + ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); + Integer validity = client.getRefreshTokenValiditySeconds(); + if (validity != null) { + return validity; + } + } + return refreshTokenValiditySeconds; + } + + /** + * Is a refresh token supported for this client (or the global setting if + * {@link #setClientDetailsService(ClientDetailsService) clientDetailsService} is not + * set. + * @param clientAuth the current authorization request + * @return boolean to indicate if refresh token is supported + */ + protected boolean isSupportRefreshToken(OAuth2Request clientAuth) { + if (clientDetailsService != null) { + ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); + return client.getAuthorizedGrantTypes().contains("refresh_token"); + } + return this.supportRefreshToken; + } + + /** + * An access token enhancer that will be applied to a new token before it is saved in + * the token store. + * @param accessTokenEnhancer the access token enhancer to set + */ + public void setTokenEnhancer(TokenEnhancer accessTokenEnhancer) { + this.accessTokenEnhancer = accessTokenEnhancer; + } + + /** + * The validity (in seconds) of the refresh token. If less than or equal to zero then + * the tokens will be non-expiring. + * @param refreshTokenValiditySeconds The validity (in seconds) of the refresh token. + */ + public void setRefreshTokenValiditySeconds(int refreshTokenValiditySeconds) { + this.refreshTokenValiditySeconds = refreshTokenValiditySeconds; + } + + /** + * The default validity (in seconds) of the access token. Zero or negative for + * non-expiring tokens. If a client details service is set the validity period will be + * read from the client, defaulting to this value if not defined by the client. + * @param accessTokenValiditySeconds The validity (in seconds) of the access token. + */ + public void setAccessTokenValiditySeconds(int accessTokenValiditySeconds) { + this.accessTokenValiditySeconds = accessTokenValiditySeconds; + } + + /** + * Whether to support the refresh token. + * @param supportRefreshToken Whether to support the refresh token. + */ + public void setSupportRefreshToken(boolean supportRefreshToken) { + this.supportRefreshToken = supportRefreshToken; + } + + /** + * Whether to reuse refresh tokens (until expired). + * @param reuseRefreshToken Whether to reuse refresh tokens (until expired). + */ + public void setReuseRefreshToken(boolean reuseRefreshToken) { + this.reuseRefreshToken = reuseRefreshToken; + } + + /** + * The persistence strategy for token storage. + * @param tokenStore the store for access and refresh tokens. + */ + public void setTokenStore(TokenStore tokenStore) { + this.tokenStore = tokenStore; + } + + /** + * An authentication manager that will be used (if provided) to check the user + * authentication when a token is refreshed. + * @param authenticationManager the authenticationManager to set + */ + public void setAuthenticationManager(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + /** + * The client details service to use for looking up clients (if necessary). Optional + * if the access token expiry is set globally via + * {@link #setAccessTokenValiditySeconds(int)}. + * @param clientDetailsService the client details service + */ + public void setClientDetailsService(ClientDetailsService clientDetailsService) { + this.clientDetailsService = clientDetailsService; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccRedisTokenStore.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccRedisTokenStore.java new file mode 100644 index 0000000..f161afb --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccRedisTokenStore.java @@ -0,0 +1,480 @@ +package com.cloud.kicc.common.security.override; + +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2RefreshToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator; +import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.redis.JdkSerializationStrategy; +import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; +import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStoreSerializationStrategy; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; +import java.util.*; + +/** + *

+ * @link https://github.com/spring-projects/spring-security-oauth/pull/1660 + * 重写RedisTokenStore ,主要解决 #1814 oauth2中client_id_to_access数据膨胀问题 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class KiccRedisTokenStore implements TokenStore { + + private static final String ACCESS = "access:"; + + private static final String AUTH_TO_ACCESS = "auth_to_access:"; + + private static final String AUTH = "auth:"; + + private static final String REFRESH_AUTH = "refresh_auth:"; + + private static final String REFRESH = "refresh:"; + + private static final String REFRESH_TO_ACCESS = "refresh_to_access:"; + + private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access_z:"; + + private static final String UNAME_TO_ACCESS = "uname_to_access_z:"; + + private static final boolean springDataRedis_2_0 = ClassUtils.isPresent( + "org.springframework.data.redis.connection.RedisStandaloneConfiguration", + RedisTokenStore.class.getClassLoader()); + + private final RedisConnectionFactory connectionFactory; + + private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator(); + + private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy(); + + private String prefix = ""; + + private Method redisConnectionSet_2_0; + + public KiccRedisTokenStore(RedisConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + if (springDataRedis_2_0) { + this.loadRedisConnectionMethods_2_0(); + } + } + + public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) { + this.authenticationKeyGenerator = authenticationKeyGenerator; + } + + public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) { + this.serializationStrategy = serializationStrategy; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + private void loadRedisConnectionMethods_2_0() { + this.redisConnectionSet_2_0 = ReflectionUtils.findMethod(RedisConnection.class, "set", byte[].class, + byte[].class); + } + + private RedisConnection getConnection() { + return connectionFactory.getConnection(); + } + + private byte[] serialize(Object object) { + return serializationStrategy.serialize(object); + } + + private byte[] serializeKey(String object) { + return serialize(prefix + object); + } + + private OAuth2AccessToken deserializeAccessToken(byte[] bytes) { + return serializationStrategy.deserialize(bytes, OAuth2AccessToken.class); + } + + private OAuth2Authentication deserializeAuthentication(byte[] bytes) { + return serializationStrategy.deserialize(bytes, OAuth2Authentication.class); + } + + private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) { + return serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class); + } + + private byte[] serialize(String string) { + return serializationStrategy.serialize(string); + } + + private String deserializeString(byte[] bytes) { + return serializationStrategy.deserializeString(bytes); + } + + @Override + public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { + String key = authenticationKeyGenerator.extractKey(authentication); + byte[] serializedKey = serializeKey(AUTH_TO_ACCESS + key); + byte[] bytes; + try (RedisConnection conn = getConnection()) { + bytes = conn.get(serializedKey); + } + OAuth2AccessToken accessToken = deserializeAccessToken(bytes); + if (accessToken != null) { + OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue()); + if ((storedAuthentication == null + || !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication)))) { + // Keep the stores consistent (maybe the same user is + // represented by this authentication but the details have + // changed) + storeAccessToken(accessToken, authentication); + } + + } + return accessToken; + } + + @Override + public OAuth2Authentication readAuthentication(OAuth2AccessToken token) { + return readAuthentication(token.getValue()); + } + + @Override + public OAuth2Authentication readAuthentication(String token) { + byte[] bytes; + try (RedisConnection conn = getConnection()) { + bytes = conn.get(serializeKey(AUTH + token)); + } + return deserializeAuthentication(bytes); + } + + @Override + public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) { + return readAuthenticationForRefreshToken(token.getValue()); + } + + public OAuth2Authentication readAuthenticationForRefreshToken(String token) { + try (RedisConnection conn = getConnection()) { + byte[] bytes = conn.get(serializeKey(REFRESH_AUTH + token)); + return deserializeAuthentication(bytes); + } + } + + @Override + public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { + byte[] serializedAccessToken = serialize(token); + byte[] serializedAuth = serialize(authentication); + byte[] accessKey = serializeKey(ACCESS + token.getValue()); + byte[] authKey = serializeKey(AUTH + token.getValue()); + byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication)); + byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication)); + byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId()); + + try (RedisConnection conn = getConnection()) { + conn.openPipeline(); + if (springDataRedis_2_0) { + try { + this.redisConnectionSet_2_0.invoke(conn, accessKey, serializedAccessToken); + this.redisConnectionSet_2_0.invoke(conn, authKey, serializedAuth); + this.redisConnectionSet_2_0.invoke(conn, authToAccessKey, serializedAccessToken); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + else { + conn.set(accessKey, serializedAccessToken); + conn.set(authKey, serializedAuth); + conn.set(authToAccessKey, serializedAccessToken); + } + + if (token.getExpiration() != null) { + int seconds = token.getExpiresIn(); + long expirationTime = token.getExpiration().getTime(); + + if (!authentication.isClientOnly()) { + conn.zAdd(approvalKey, expirationTime, serializedAccessToken); + } + conn.zAdd(clientId, expirationTime, serializedAccessToken); + + conn.expire(accessKey, seconds); + conn.expire(authKey, seconds); + conn.expire(authToAccessKey, seconds); + conn.expire(clientId, seconds); + conn.expire(approvalKey, seconds); + } + else { + conn.zAdd(clientId, -1, serializedAccessToken); + if (!authentication.isClientOnly()) { + conn.zAdd(approvalKey, -1, serializedAccessToken); + } + } + OAuth2RefreshToken refreshToken = token.getRefreshToken(); + if (refreshToken != null && refreshToken.getValue() != null) { + byte[] auth = serialize(token.getValue()); + byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue()); + if (springDataRedis_2_0) { + try { + this.redisConnectionSet_2_0.invoke(conn, refreshToAccessKey, auth); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + else { + conn.set(refreshToAccessKey, auth); + } + if (refreshToken instanceof ExpiringOAuth2RefreshToken) { + ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken; + Date expiration = expiringRefreshToken.getExpiration(); + if (expiration != null) { + int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L) + .intValue(); + conn.expire(refreshToAccessKey, seconds); + } + } + } + conn.closePipeline(); + } + } + + private static String getApprovalKey(OAuth2Authentication authentication) { + String userName = authentication.getUserAuthentication() == null ? "" + : authentication.getUserAuthentication().getName(); + return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName); + } + + private static String getApprovalKey(String clientId, String userName) { + return clientId + (userName == null ? "" : ":" + userName); + } + + @Override + public void removeAccessToken(OAuth2AccessToken accessToken) { + removeAccessToken(accessToken.getValue()); + } + + @Override + public OAuth2AccessToken readAccessToken(String tokenValue) { + byte[] key = serializeKey(ACCESS + tokenValue); + byte[] bytes; + try (RedisConnection conn = getConnection()) { + bytes = conn.get(key); + } + return deserializeAccessToken(bytes); + } + + public void removeAccessToken(String tokenValue) { + byte[] accessKey = serializeKey(ACCESS + tokenValue); + byte[] authKey = serializeKey(AUTH + tokenValue); + try (RedisConnection conn = getConnection()) { + conn.openPipeline(); + conn.get(accessKey); + conn.get(authKey); + conn.del(accessKey); + // Don't remove the refresh token - it's up to the caller to do that + conn.del(authKey); + List results = conn.closePipeline(); + byte[] access = (byte[]) results.get(0); + byte[] auth = (byte[]) results.get(1); + + OAuth2Authentication authentication = deserializeAuthentication(auth); + if (authentication != null) { + String key = authenticationKeyGenerator.extractKey(authentication); + byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key); + byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication)); + byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId()); + conn.openPipeline(); + conn.del(authToAccessKey); + conn.zRem(unameKey, access); + conn.zRem(clientId, access); + conn.del(serialize(ACCESS + key)); + conn.closePipeline(); + } + } + } + + @Override + public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) { + byte[] refreshKey = serializeKey(REFRESH + refreshToken.getValue()); + byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + refreshToken.getValue()); + byte[] serializedRefreshToken = serialize(refreshToken); + try (RedisConnection conn = getConnection()) { + conn.openPipeline(); + if (springDataRedis_2_0) { + try { + this.redisConnectionSet_2_0.invoke(conn, refreshKey, serializedRefreshToken); + this.redisConnectionSet_2_0.invoke(conn, refreshAuthKey, serialize(authentication)); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + else { + conn.set(refreshKey, serializedRefreshToken); + conn.set(refreshAuthKey, serialize(authentication)); + } + if (refreshToken instanceof ExpiringOAuth2RefreshToken) { + ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken; + Date expiration = expiringRefreshToken.getExpiration(); + if (expiration != null) { + int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L).intValue(); + conn.expire(refreshKey, seconds); + conn.expire(refreshAuthKey, seconds); + } + } + conn.closePipeline(); + } + } + + @Override + public OAuth2RefreshToken readRefreshToken(String tokenValue) { + byte[] key = serializeKey(REFRESH + tokenValue); + byte[] bytes; + try (RedisConnection conn = getConnection()) { + bytes = conn.get(key); + } + return deserializeRefreshToken(bytes); + } + + @Override + public void removeRefreshToken(OAuth2RefreshToken refreshToken) { + removeRefreshToken(refreshToken.getValue()); + } + + public void removeRefreshToken(String tokenValue) { + byte[] refreshKey = serializeKey(REFRESH + tokenValue); + byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + tokenValue); + byte[] refresh2AccessKey = serializeKey(REFRESH_TO_ACCESS + tokenValue); + try (RedisConnection conn = getConnection()) { + conn.openPipeline(); + conn.del(refreshKey); + conn.del(refreshAuthKey); + conn.del(refresh2AccessKey); + conn.closePipeline(); + } + } + + @Override + public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { + removeAccessTokenUsingRefreshToken(refreshToken.getValue()); + } + + private void removeAccessTokenUsingRefreshToken(String refreshToken) { + byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken); + List results; + try (RedisConnection conn = getConnection()) { + conn.openPipeline(); + conn.get(key); + conn.del(key); + results = conn.closePipeline(); + } + byte[] bytes = (byte[]) results.get(0); + String accessToken = deserializeString(bytes); + if (accessToken != null) { + removeAccessToken(accessToken); + } + } + + private List getZByteLists(byte[] key, RedisConnection conn) { + // Sorted Set expiration maintenance + long currentTime = System.currentTimeMillis(); + conn.zRemRangeByScore(key, 0, currentTime); + + List byteList; + Long size = conn.zCard(key); + assert size != null; + byteList = new ArrayList<>(size.intValue()); + Cursor cursor = conn.zScan(key, ScanOptions.NONE); + + while (cursor.hasNext()) { + RedisZSetCommands.Tuple t = cursor.next(); + + // Probably not necessary because of the maintenance at the beginning but why + // not... + if (t.getScore() == -1 || t.getScore() > currentTime) { + byteList.add(t.getValue()); + } + } + return byteList; + } + + /** + * Runs a maintenance of the RedisTokenStore. + *

+ * SortedSets UNAME_TO_ACCESS and CLIENT_ID_TO_ACCESS contains access tokens that can + * expire. This expiration is set as a score of the Redis SortedSet data structure. + * Redis does not support expiration of items in a container data structure. It + * supports only expiration of whole key. In case there is still new access tokens + * being stored into the RedisTokenStore before whole key gets expired, the expiration + * is prolonged and the key is not effectively deleted. To do "garbage collection" + * this method should be called once upon a time. + * @return how many items were removed + */ + public long doMaintenance() { + long removed = 0; + try (RedisConnection conn = getConnection()) { + // client_id_to_acccess maintenance + Cursor clientToAccessKeys = conn + .scan(ScanOptions.scanOptions().match(prefix + CLIENT_ID_TO_ACCESS + "*").build()); + while (clientToAccessKeys.hasNext()) { + byte[] clientIdToAccessKey = clientToAccessKeys.next(); + + removed += conn.zRemRangeByScore(clientIdToAccessKey, 0, System.currentTimeMillis()); + } + + // uname_to_access maintenance + Cursor unameToAccessKeys = conn + .scan(ScanOptions.scanOptions().match(prefix + UNAME_TO_ACCESS + "*").build()); + while (unameToAccessKeys.hasNext()) { + byte[] unameToAccessKey = unameToAccessKeys.next(); + + removed += conn.zRemRangeByScore(unameToAccessKey, 0, System.currentTimeMillis()); + } + } + return removed; + } + + @Override + public Collection findTokensByClientIdAndUserName(String clientId, String userName) { + byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName)); + List byteList; + try (RedisConnection conn = getConnection()) { + byteList = getZByteLists(approvalKey, conn); + } + if (byteList.size() == 0) { + return Collections.emptySet(); + } + List accessTokens = new ArrayList<>(byteList.size()); + for (byte[] bytes : byteList) { + OAuth2AccessToken accessToken = deserializeAccessToken(bytes); + accessTokens.add(accessToken); + } + return Collections.unmodifiableCollection(accessTokens); + } + + @Override + public Collection findTokensByClientId(String clientId) { + byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId); + List byteList; + try (RedisConnection conn = getConnection()) { + byteList = getZByteLists(key, conn); + } + if (byteList.size() == 0) { + return Collections.emptySet(); + } + List accessTokens = new ArrayList<>(byteList.size()); + for (byte[] bytes : byteList) { + OAuth2AccessToken accessToken = deserializeAccessToken(bytes); + accessTokens.add(accessToken); + } + return Collections.unmodifiableCollection(accessTokens); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccWebResponseExceptionTranslator.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccWebResponseExceptionTranslator.java new file mode 100644 index 0000000..f99ce03 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/KiccWebResponseExceptionTranslator.java @@ -0,0 +1,102 @@ +package com.cloud.kicc.common.security.override; + +import com.cloud.kicc.common.security.exception.*; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.*; +import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; +import org.springframework.security.web.util.ThrowableAnalyzer; +import org.springframework.web.HttpRequestMethodNotSupportedException; + +/** + *

+ * 异常处理,重写oauth 默认实现 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +public class KiccWebResponseExceptionTranslator implements WebResponseExceptionTranslator { + + private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); + + @Override + @SneakyThrows + public ResponseEntity translate(Exception e) { + + Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e); + + Exception ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain); + if (ase != null) { + return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e)); + } + + ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain); + if (ase != null) { + return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase)); + } + + ase = (InvalidGrantException) throwableAnalyzer.getFirstThrowableOfType(InvalidGrantException.class, causeChain); + if (ase != null) { + return handleOAuth2Exception(new InvalidException(ase.getMessage(), ase)); + } + + // token 过期 特殊处理 返回 424 不是 401 + ase = (InvalidTokenException) throwableAnalyzer.getFirstThrowableOfType(InvalidTokenException.class, causeChain); + if (ase != null) { + return handleOAuth2Exception(new TokenInvalidException(ase.getMessage(), ase)); + } + + ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain); + if (ase != null) { + return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase)); + } + + ase = (SecurityCheckedException) throwableAnalyzer.getFirstThrowableOfType(SecurityCheckedException.class, causeChain); + if (ase != null) { + return handleOAuth2Exception(new SecurityCheckedException(ase.getMessage(), ase)); + } + + ase = (UnConfiguredUserDataException) throwableAnalyzer.getFirstThrowableOfType(UnConfiguredUserDataException.class, causeChain); + if (ase != null) { + return handleOAuth2Exception(new UnConfiguredUserDataException(ase.getMessage(), ase)); + } + + ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain); + if (ase != null) { + return handleOAuth2Exception((OAuth2Exception) ase); + } + + return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e)); + + } + + private ResponseEntity handleOAuth2Exception(OAuth2Exception e) { + + int status = e.getHttpErrorCode(); + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.CACHE_CONTROL, "no-store"); + headers.set(HttpHeaders.PRAGMA, "no-cache"); + if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) { + headers.set(HttpHeaders.WWW_AUTHENTICATE, + String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary())); + } + + // 客户端异常直接返回客户端,不然无法解析 + if (e instanceof ClientAuthenticationException) { + return new ResponseEntity<>(e, headers, HttpStatus.valueOf(status)); + } + return new ResponseEntity<>(new KiccAuth2Exception(e.getMessage(), e.getOAuth2ErrorCode()), headers, + HttpStatus.valueOf(status)); + + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/jackson2/SimpleGrantedAuthorityMixin.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/jackson2/SimpleGrantedAuthorityMixin.java new file mode 100644 index 0000000..5325bad --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/override/jackson2/SimpleGrantedAuthorityMixin.java @@ -0,0 +1,32 @@ + + +package com.cloud.kicc.common.security.override.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + *

+ * 反序列化扩展SSO用户权限 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/8/19 + */ +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class SimpleGrantedAuthorityMixin { + + /** + * Mixin Constructor. + * @param role the role + */ + @JsonCreator + public SimpleGrantedAuthorityMixin(@JsonProperty("authority") String role) { + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/properties/CasProperties.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/properties/CasProperties.java new file mode 100644 index 0000000..4519ff7 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/properties/CasProperties.java @@ -0,0 +1,23 @@ +package com.cloud.kicc.common.security.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + *

+ * Central Authentication Service configuration + *

+ * + * @Author: wangxiang4 + * @Since: 2023/9/16 + */ +@Data +@ConfigurationProperties(prefix = "security.cas") +public class CasProperties { + + private String userClass = "com.cloud.kicc.common.data.entity.KiccUser"; + + private String identity = "KICC"; + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/service/KiccUserDetailsService.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/service/KiccUserDetailsService.java new file mode 100644 index 0000000..98707f1 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/service/KiccUserDetailsService.java @@ -0,0 +1,93 @@ +package com.cloud.kicc.common.security.service; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.cloud.kicc.common.core.constant.CommonConstants; +import com.cloud.kicc.common.core.constant.SecurityConstants; +import com.cloud.kicc.common.data.entity.CasUser; +import com.cloud.kicc.common.data.entity.SsoUser; +import com.cloud.kicc.common.security.exception.SecurityCheckedException; +import lombok.SneakyThrows; +import org.springframework.core.Ordered; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; + +/** + *

+ * 用户详细信息服务 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public interface KiccUserDetailsService extends UserDetailsService, Ordered { + + /** + * 是否支持此客户端校验 + * @param clientId 目标客户端 + * @return true/false + */ + default boolean support(String clientId, String grantType) { + return true; + } + + /** + * 排序值 默认取最大的 + * @return 排序值 + */ + @Override + default int getOrder() { + return 0; + } + + /** + * 构建userDetails + * @param user 用户信息 + * @return UserDetails + */ + @SneakyThrows + default UserDetails getUserDetails(SsoUser user) { + if (ObjectUtil.isEmpty(user)) { + throw new SecurityCheckedException("SSO User not found, Try again after registration."); + } + + // 构造security用户 + return new CasUser( + user.getUserName(), + SecurityConstants.BCRYPT + user.getPassword(), + true, + true, + true, + StrUtil.equals(user.getStatus(), CommonConstants.STATUS_NORMAL), + AuthorityUtils.createAuthorityList(), + user.getId(), + user.getNickName(), + user.getEmail(), + user.getPhone(), + user.getSex(), + user.getAvatar(), + user.getLoginIp(), + user.getLoginTime(), + user.getStatus(), + user.getCreateById(), + user.getCreateByName(), + user.getCreateTime(), + user.getUpdateById(), + user.getUpdateByName(), + user.getUpdateTime(), + user.getRemarks(), + null, + user.getTenantId() + ); + } + + /** + * 通过用户实体查询 + * @param casUser user + */ + default UserDetails loadUserByUser(CasUser casUser) { + return this.loadUserByUsername(casUser.getUsername()); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/service/impl/KiccAppUserDetailsServiceImpl.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/service/impl/KiccAppUserDetailsServiceImpl.java new file mode 100644 index 0000000..d1cc5a6 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/service/impl/KiccAppUserDetailsServiceImpl.java @@ -0,0 +1,59 @@ +package com.cloud.kicc.common.security.service.impl; + +import com.cloud.kicc.common.core.constant.SecurityConstants; +import com.cloud.kicc.common.data.entity.CasUser; +import com.cloud.kicc.common.data.entity.SsoUser; +import com.cloud.kicc.common.security.service.KiccUserDetailsService; +import com.cloud.kicc.common.security.template.UserProviderTemplate; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; + +/** + *

+ * 用户详细信息 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +@RequiredArgsConstructor +public class KiccAppUserDetailsServiceImpl implements KiccUserDetailsService { + + @Autowired(required = false) + private UserProviderTemplate userProviderTemplate; + + /** + * 手机号登录 + * @param phone 手机号 + */ + @Override + @SneakyThrows + public UserDetails loadUserByUsername(String phone) { + SsoUser result = userProviderTemplate.selectByPhone(phone); + return getUserDetails(result); + } + + /** + * 通过用户实体查询 + * @param casUser user + */ + @Override + public UserDetails loadUserByUser(CasUser casUser) { + return this.loadUserByUsername(casUser.getPhone()); + } + + /** + * 是否支持此客户端校验 + * @param clientId 目标客户端 + * @return true/false + */ + @Override + public boolean support(String clientId, String grantType) { + return SecurityConstants.APP.equals(grantType); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/service/impl/KiccUserDetailsServiceImpl.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/service/impl/KiccUserDetailsServiceImpl.java new file mode 100644 index 0000000..ebc3cca --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/service/impl/KiccUserDetailsServiceImpl.java @@ -0,0 +1,45 @@ +package com.cloud.kicc.common.security.service.impl; + +import com.cloud.kicc.common.data.entity.SsoUser; +import com.cloud.kicc.common.security.service.KiccUserDetailsService; +import com.cloud.kicc.common.security.template.UserProviderTemplate; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.security.core.userdetails.UserDetails; + +/** + *

+ * 用户详细信息 + *

+ * + * @Author: wangxiang4 + * @Since: 2023/9/16 + */ +@Slf4j +@Primary +@RequiredArgsConstructor +public class KiccUserDetailsServiceImpl implements KiccUserDetailsService { + + @Autowired(required = false) + private UserProviderTemplate userProviderTemplate; + + /** + * 用户名密码登录 + * @param username 用户名 + */ + @Override + @SneakyThrows + public UserDetails loadUserByUsername(String username) { + SsoUser result = userProviderTemplate.selectByUserName(username); + return getUserDetails(result); + } + + @Override + public int getOrder() { + return Integer.MIN_VALUE; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/template/UserProviderTemplate.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/template/UserProviderTemplate.java new file mode 100644 index 0000000..6455d22 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/template/UserProviderTemplate.java @@ -0,0 +1,33 @@ +package com.cloud.kicc.common.security.template; + +import com.cloud.kicc.common.data.entity.CasUser; +import com.cloud.kicc.common.data.entity.SsoUser; + +/** + *

+ * The user must implement the template + * Contains SSO handler related to login + *

+ * + * @Author: wangxiang4 + * @Since: 2023/9/16 + */ +public interface UserProviderTemplate { + + default SsoUser selectByUserName(String userName) { + return null; + } + + default SsoUser selectByPhone(String phone) { + return null; + }; + + default T selectByCasUserId(String casUserId) { + return null; + }; + + default T selectByUserId(String userid) { + return null; + }; + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/util/AuthUtils.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/util/AuthUtils.java new file mode 100644 index 0000000..0f75d44 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/util/AuthUtils.java @@ -0,0 +1,68 @@ +package com.cloud.kicc.common.security.util; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.CharsetUtil; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; + +import javax.servlet.http.HttpServletRequest; + +/** + *

+ * 认证授权相关工具类 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +@UtilityClass +public class AuthUtils { + + private final String BASIC_ = "Basic "; + + /** + * 从header 请求中的clientId/clientsecect + * @param header header中的参数 + */ + @SneakyThrows + public String[] extractAndDecodeHeader(String header) { + + byte[] base64Token = header.substring(6).getBytes("UTF-8"); + byte[] decoded; + try { + decoded = Base64.decode(base64Token); + } + catch (IllegalArgumentException e) { + throw new RuntimeException("Failed to decode basic authentication token"); + } + + String token = new String(decoded, CharsetUtil.UTF_8); + + int delim = token.indexOf(":"); + + if (delim == -1) { + throw new RuntimeException("Invalid basic authentication token"); + } + return new String[] { token.substring(0, delim), token.substring(delim + 1) }; + } + + /** + * *从header 请求中的clientId/clientsecect + * @param request + * @return + */ + @SneakyThrows + public String[] extractAndDecodeHeader(HttpServletRequest request) { + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (header == null || !header.startsWith(BASIC_)) { + throw new RuntimeException("请求头中client信息为空"); + } + + return extractAndDecodeHeader(header); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/util/SecurityUtils.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/util/SecurityUtils.java new file mode 100644 index 0000000..209446b --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/util/SecurityUtils.java @@ -0,0 +1,181 @@ +package com.cloud.kicc.common.security.util; + +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.cloud.kicc.common.core.constant.CommonConstants; +import com.cloud.kicc.common.core.constant.SecurityConstants; +import com.cloud.kicc.common.core.jackson.KiccJavaTimeModule; +import com.cloud.kicc.common.core.util.SpringContextHolderUtil; +import com.cloud.kicc.common.data.entity.CasUser; +import com.cloud.kicc.common.security.exception.UnConfiguredUserDataException; +import com.cloud.kicc.common.security.override.jackson2.SimpleGrantedAuthorityMixin; +import com.cloud.kicc.common.security.properties.CasProperties; +import com.cloud.kicc.common.security.template.UserProviderTemplate; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import org.springframework.beans.BeanUtils; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * 安全工具类 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@UtilityClass +public class SecurityUtils { + + CasProperties casProperties = SpringContextHolderUtil.getBean(CasProperties.class); + + /** + * 获取Authentication + */ + public Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 获取用户 + */ + public CasUser getCasUser(Authentication authentication) { + Object principal = authentication.getPrincipal(); + if (principal instanceof CasUser) { + return (CasUser) principal; + } + return null; + } + + /** + * 获取CAS用户 + */ + public CasUser getCasUser() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + return getCasUser(authentication); + } + + /** + * 获取完整用户 + * @param identity SSO系统身份 + * @param valueType 自定义扩展用户 + * @return T + */ + @SneakyThrows + public T getUser(String identity, Class valueType) { + CasUser casUser = getCasUser(); + if (casUser == null) return null; + String str = casUser.getExPrincipals().get(identity); + if (JSONUtil.isJson(str)) + return new ObjectMapper() + .registerModule(new KiccJavaTimeModule()) + .addMixIn(SimpleGrantedAuthority.class, SimpleGrantedAuthorityMixin.class) + .readValue(str, valueType); + return null; + } + + /** + * 获取SSO扩展用户 + */ + public T getUser() { + return getUser(casProperties.getIdentity(), ClassUtil.loadClass(casProperties.getUserClass())); + } + + /** + * 获取用户角色信息 + * @return 角色集合 + */ + public List getRoles() { + Authentication authentication = getAuthentication(); + Collection authorities = authentication.getAuthorities(); + + List roleIds = new ArrayList<>(); + authorities.stream().filter(granted -> StrUtil.startWith(granted.getAuthority(), SecurityConstants.ROLE)) + .forEach(granted -> { + String id = StrUtil.removePrefix(granted.getAuthority(), SecurityConstants.ROLE); + roleIds.add(id); + }); + return roleIds; + } + + /** + * 对外开放接口临时登录会话 + * @param userId 用户id + * @return User 用户对象 + */ + @SneakyThrows + public T openInterfaceTemporaryLoginSession(String userId) { + UserProviderTemplate userProviderTemplate = SpringContextHolderUtil.getBean(UserProviderTemplate.class); + Object user = userProviderTemplate.selectByUserId(userId); + if (ObjectUtil.isEmpty(user)) { + throw new UnConfiguredUserDataException("System user not found Contact your system administrator for configuration!", null); + } + + String[] permissions = (String[]) ReflectUtil.getMethodByName(user.getClass(), "getPermissions").invoke(user); + List authorities = Arrays.stream(permissions) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + String username = (String) ReflectUtil.getMethodByName(user.getClass(), "getUsername").invoke(user); + String password = (String) ReflectUtil.getMethodByName(user.getClass(), "getPassword").invoke(user); + String status = (String) ReflectUtil.getMethodByName(user.getClass(), "getStatus").invoke(user); + String[] roleIds = (String[]) ReflectUtil.getMethodByName(user.getClass(), "getRoleIds").invoke(user); + String[] tenantIds = (String[]) ReflectUtil.getMethodByName(user.getClass(), "getTenantIds").invoke(user); + + CasUser casUser = new CasUser( + username, + SecurityConstants.BCRYPT + password, + true, + true, + true, + StrUtil.equals(status, CommonConstants.STATUS_NORMAL), + authorities + ); + BeanUtils.copyProperties(user, casUser); + casUser.setRoleId(String.join(",", roleIds)); + casUser.setTenantId(String.join(",", tenantIds)); + Object exUser = ReflectUtil.newInstance(ClassUtil.loadClass(casProperties.getUserClass()), + casUser.getUsername(), + casUser.getPassword(), + casUser.isEnabled(), + casUser.isAccountNonExpired(), + casUser.isCredentialsNonExpired(), + casUser.isAccountNonLocked(), + authorities + ); + + BeanUtils.copyProperties(user, exUser); + // 设置扩展用户数据 + casUser.getExPrincipals().put(casProperties.getIdentity(), new ObjectMapper() + .registerModule(new KiccJavaTimeModule()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .writeValueAsString(exUser)); + Authentication authentication = new UsernamePasswordAuthenticationToken(casUser, "N/A", casUser.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + return (T) user; + } + + public static void main(String[] args) { + System.out.println(new BCryptPasswordEncoder().encode("123456")); + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssFilter.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssFilter.java new file mode 100644 index 0000000..b3fe540 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssFilter.java @@ -0,0 +1,84 @@ +package com.cloud.kicc.common.security.xss; + +import cn.hutool.core.util.StrUtil; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

+ * 过滤层mvc + * 防止XSS攻击的过滤器 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +public class XssFilter implements Filter { + /** + * 排除链接 + */ + public List excludes = new ArrayList<>(); + + /** + * xss过滤开关 + */ + public boolean enabled = false; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + String tempExcludes = filterConfig.getInitParameter("excludes"); + String tempEnabled = filterConfig.getInitParameter("enabled"); + if (StrUtil.isNotEmpty(tempExcludes)) { + String[] url = tempExcludes.split(","); + for (int i = 0; url != null && i < url.length; i++) { + excludes.add(url[i]); + } + } + if (StrUtil.isNotEmpty(tempEnabled)) { + enabled = Boolean.valueOf(tempEnabled); + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + if (handleExcludeURL(req, resp)) { + chain.doFilter(request, response); + return; + } + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(xssRequest, response); + } + + private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) { + if (!enabled) { + return true; + } + if (excludes == null || excludes.isEmpty()) { + return false; + } + String url = request.getServletPath(); + for (String pattern : excludes) { + Pattern p = Pattern.compile("^" + pattern); + Matcher m = p.matcher(url); + if (m.find()) { + return true; + } + } + return false; + } + + @Override + public void destroy() { + + } +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssFilterAutoConfiguration.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssFilterAutoConfiguration.java new file mode 100644 index 0000000..931fd8e --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssFilterAutoConfiguration.java @@ -0,0 +1,41 @@ +package com.cloud.kicc.common.security.xss; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.DispatcherType; +import java.util.HashMap; +import java.util.Map; + +/** + *

+ * XSS-Filter注册 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(XssProperties.class) +public class XssFilterAutoConfiguration { + + @Bean + public FilterRegistrationBean xssFilterRegistration(XssProperties xssProperties) { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns(StrUtil.splitToArray(xssProperties.getUrlPatterns(), ",")); + registration.setName("xssFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + Map initParameters = new HashMap(); + initParameters.put("excludes", ObjectUtil.defaultIfNull(xssProperties.getExcludes(), "")); + initParameters.put("enabled", ObjectUtil.defaultIfNull(xssProperties.getEnabled(), "")); + registration.setInitParameters(initParameters); + return registration; + } + +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssHttpServletRequestWrapper.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..c07c97f --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssHttpServletRequestWrapper.java @@ -0,0 +1,115 @@ +package com.cloud.kicc.common.security.xss; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HtmlUtil; +import com.cloud.kicc.common.core.util.HTMLFilterUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + *

+ * XSS、sql过滤处理 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/19 + */ +@Slf4j +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { + + private static String[] SQL_KEYWORDS = {"master", "truncate", "insert", "select" + , "delete", "update", "declare", "alter", "drop", "sleep"}; + //sql 替换字符 + private static String REPLACE_STR = ""; + + /** + * @param request + */ + public XssHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public String[] getParameterValues(String name) { + String[] values = super.getParameterValues(name); + if (values != null) { + int length = values.length; + String[] escapseValues = new String[length]; + for (int i = 0; i < length; i++) { + // 防xss攻击和过滤前后空格 + escapseValues[i] = HtmlUtil.filter(values[i]).trim(); + //防sql注入 + escapseValues[i] = cleanSqlKeyWords(escapseValues[i]); + } + return escapseValues; + } + return super.getParameterValues(name); + } + + private String cleanSqlKeyWords(String value) { + String paramValue = value; + for (String keyword : SQL_KEYWORDS) { + if (paramValue.length() > keyword.length() + 4 + && (paramValue.contains(" " + keyword) || paramValue.contains(keyword + " ") || paramValue.contains(" " + keyword + " "))) { + paramValue = StrUtil.replace(paramValue, keyword, REPLACE_STR); + } + } + return paramValue; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + // 非json类型,直接返回 + if (!isJsonRequest()) { + return super.getInputStream(); + } + + // 为空,直接返回 + String json = IoUtil.read(super.getInputStream(), "utf-8"); + if (StrUtil.isEmpty(json)) { + return super.getInputStream(); + } + + // xss过滤 + json = new HTMLFilterUtil().filter(json).trim(); + final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes("utf-8")); + return new ServletInputStream() { + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + @Override + public int read() throws IOException { + return bis.read(); + } + }; + } + + /** + * 是否是Json请求 + */ + public boolean isJsonRequest() { + String header = super.getHeader(HttpHeaders.CONTENT_TYPE); + return MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(header) + || MediaType.APPLICATION_JSON_UTF8_VALUE.equalsIgnoreCase(header); + } +} diff --git a/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssProperties.java b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssProperties.java new file mode 100644 index 0000000..0f942d0 --- /dev/null +++ b/kicc-common-security/src/main/java/com/cloud/kicc/common/security/xss/XssProperties.java @@ -0,0 +1,24 @@ +package com.cloud.kicc.common.security.xss; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + *

+ * xss 配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/24 + */ +@Data +@ConfigurationProperties(prefix = "xss") +public class XssProperties { + + private String enabled; + + private String excludes; + + private String urlPatterns; + +} diff --git a/kicc-common-security/src/main/resources/META-INF/spring.factories b/kicc-common-security/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..0ba864d --- /dev/null +++ b/kicc-common-security/src/main/resources/META-INF/spring.factories @@ -0,0 +1,7 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.cloud.kicc.common.security.aspect.SecurityInnerAspect,\ + com.cloud.kicc.common.security.service.impl.KiccUserDetailsServiceImpl,\ + com.cloud.kicc.common.security.service.impl.KiccAppUserDetailsServiceImpl,\ + com.cloud.kicc.common.security.config.TokenStoreAutoConfiguration,\ + com.cloud.kicc.common.security.config.TokenStoreAutoCleanScheduleConfiguration,\ + com.cloud.kicc.common.security.config.Oauth2SecurityAutoConfiguration diff --git a/kicc-common-swagger/pom.xml b/kicc-common-swagger/pom.xml new file mode 100644 index 0000000..1291014 --- /dev/null +++ b/kicc-common-swagger/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + com.cloud + kicc-tool + 1.0.0 + + + kicc-common-swagger + jar + + kicc 接口文档 + + + + + + io.springfox + springfox-swagger-ui + ${swagger.fox.version} + + + com.github.xiaoymin + knife4j-spring-ui + ${knife4j.version} + + + io.springfox + springfox-swagger2 + ${swagger.fox.version} + + + io.springfox + springfox-oas + ${swagger.fox.version} + + + + org.springframework + spring-webflux + provided + + + + org.springframework.cloud + spring-cloud-gateway-server + provided + + + org.springframework.cloud + spring-cloud-commons + provided + + + org.springframework + spring-webmvc + provided + + + diff --git a/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/annotation/EnableKiccSwagger2.java b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/annotation/EnableKiccSwagger2.java new file mode 100644 index 0000000..47d281a --- /dev/null +++ b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/annotation/EnableKiccSwagger2.java @@ -0,0 +1,27 @@ +package com.cloud.kicc.common.swagger.annotation; + +import com.cloud.kicc.common.swagger.config.GatewaySwaggerAutoConfiguration; +import com.cloud.kicc.common.swagger.config.SwaggerAutoConfiguration; +import com.cloud.kicc.common.swagger.support.SwaggerProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + *

+ * 开启 swagger2 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@EnableConfigurationProperties(SwaggerProperties.class) +@Import({ SwaggerAutoConfiguration.class, GatewaySwaggerAutoConfiguration.class }) +public @interface EnableKiccSwagger2 { + +} diff --git a/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/config/GatewaySwaggerAutoConfiguration.java b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/config/GatewaySwaggerAutoConfiguration.java new file mode 100644 index 0000000..375dd9a --- /dev/null +++ b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/config/GatewaySwaggerAutoConfiguration.java @@ -0,0 +1,94 @@ +package com.cloud.kicc.common.swagger.config; + +import com.cloud.kicc.common.swagger.support.*; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.cloud.gateway.config.GatewayProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import springfox.documentation.swagger.web.SecurityConfiguration; +import springfox.documentation.swagger.web.SecurityConfigurationBuilder; +import springfox.documentation.swagger.web.UiConfiguration; +import springfox.documentation.swagger.web.UiConfigurationBuilder; + +/** + *

+ * 网关swagger 配置类,仅在webflux 环境生效 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +public class GatewaySwaggerAutoConfiguration { + + /** 聚合网关路由url资源接口文档 */ + @Bean + public SwaggerProvider swaggerProvider(SwaggerProperties swaggerProperties, GatewayProperties gatewayProperties) { + return new SwaggerProvider(swaggerProperties, gatewayProperties); + } + + /** swagger资源WebFlux返回处理 */ + @Bean + public SwaggerResourceHandler swaggerResourceHandler(SwaggerProvider swaggerProvider) { + return new SwaggerResourceHandler(swaggerProvider); + } + + /** 添加swagger-ui静态资源路径配置 */ + @Bean + public WebFluxSwaggerConfiguration fluxSwaggerConfiguration() { + return new WebFluxSwaggerConfiguration(); + } + + /** 设置网关全局安全拦截器,检查是否有客户端ID */ + @Bean + @ConditionalOnProperty(value = "swagger.basic.enabled", havingValue = "true") + public SwaggerBasicGatewayFilter swaggerBasicGatewayFilter(SwaggerProperties swaggerProperties) { + return new SwaggerBasicGatewayFilter(swaggerProperties); + } + + /** swagger安全验证WebFlux返回处理 */ + @Bean + public SwaggerSecurityHandler swaggerSecurityHandler(ObjectProvider securityConfigurationObjectProvider) { + SecurityConfiguration securityConfiguration = securityConfigurationObjectProvider + .getIfAvailable(() -> SecurityConfigurationBuilder.builder().build()); + return new SwaggerSecurityHandler(securityConfiguration); + } + + /** swagger-ui操作WebFlux返回处理 */ + @Bean + public SwaggerUiHandler swaggerUiHandler(ObjectProvider uiConfigurationObjectProvider) { + UiConfiguration uiConfiguration = uiConfigurationObjectProvider + .getIfAvailable(() -> UiConfigurationBuilder.builder().build()); + return new SwaggerUiHandler(uiConfiguration); + } + + @Bean + public RouterFunction swaggerRouterFunction(SwaggerProperties swaggerProperties, + SwaggerUiHandler swaggerUiHandler, SwaggerSecurityHandler swaggerSecurityHandler, + SwaggerResourceHandler swaggerResourceHandler) { + // 开启swagger 匹配路由 + if (swaggerProperties.getEnabled()) { + return RouterFunctions + .route(RequestPredicates.GET("/swagger-resources").and(RequestPredicates.accept(MediaType.ALL)), + swaggerResourceHandler) + .andRoute(RequestPredicates.GET("/swagger-resources/configuration/ui") + .and(RequestPredicates.accept(MediaType.ALL)), swaggerUiHandler) + .andRoute(RequestPredicates.GET("/swagger-resources/configuration/security") + .and(RequestPredicates.accept(MediaType.ALL)), swaggerSecurityHandler); + } else { + // 关闭时,返回404 + return RouterFunctions + .route(RequestPredicates.GET("/swagger-ui/**").and(RequestPredicates.accept(MediaType.ALL)), + serverRequest -> ServerResponse.notFound().build()) + .andRoute(RequestPredicates.GET("/*/v2/api-docs").and(RequestPredicates.accept(MediaType.ALL)), + serverRequest -> ServerResponse.notFound().build()); + } + } + +} diff --git a/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/config/SwaggerAutoConfiguration.java b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/config/SwaggerAutoConfiguration.java new file mode 100644 index 0000000..bcfe4d6 --- /dev/null +++ b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/config/SwaggerAutoConfiguration.java @@ -0,0 +1,169 @@ +package com.cloud.kicc.common.swagger.config; + +import com.cloud.kicc.common.swagger.support.SwaggerProperties; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.builders.RequestParameterBuilder; +import springfox.documentation.schema.ScalarType; +import springfox.documentation.service.*; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.ApiSelectorBuilder; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + *

+ * swagger配置 + * + * 禁用方法1:使用注解@Profile({"dev","test"})表示在开发或测试环境开启,而在生产关闭。(推荐使用) + * 禁用方法2:使用注解@ConditionalOnProperty(name = "swagger.enable", havingValue = "true") + * 然后在测试配置或者开发配置中添加swagger.enable=true即可开启,生产环境不填则默认关闭Swagger. + * + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + * + */ +@EnableSwagger2 +@ConditionalOnProperty(name = "swagger.enabled", matchIfMissing = true) +@ConditionalOnMissingClass("org.springframework.cloud.gateway.config.GatewayAutoConfiguration") +public class SwaggerAutoConfiguration { + + /** + * 默认的排除路径,排除Spring Boot默认的错误处理路径和端点 + */ + private static final List DEFAULT_EXCLUDE_PATH = Arrays.asList("/error", "/actuator/**"); + + private static final String BASE_PATH = "/**"; + + @Bean + public Docket api(SwaggerProperties swaggerProperties) { + // base-path处理 + if (swaggerProperties.getBasePath().isEmpty()) { + swaggerProperties.getBasePath().add(BASE_PATH); + } + + // exclude-path处理 + if (swaggerProperties.getExcludePath().isEmpty()) { + swaggerProperties.getExcludePath().addAll(DEFAULT_EXCLUDE_PATH); + } + List> excludePath = new ArrayList<>(); + swaggerProperties.getExcludePath().forEach(path -> excludePath.add(PathSelectors.ant(path))); + + // 版本请求头处理 + List pars = new ArrayList<>(); + + RequestParameterBuilder versionPar = new RequestParameterBuilder().description("灰度路由版本信息") + .in(ParameterType.HEADER).name("VERSION").required(false) + .query(param -> param.model(model -> model.scalarModel(ScalarType.STRING))); + + pars.add(versionPar.build()); + + ApiSelectorBuilder builder = new Docket(DocumentationType.SWAGGER_2).host(swaggerProperties.getHost()) + .apiInfo(apiInfo(swaggerProperties)).globalRequestParameters(pars).select() + .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage())); + + swaggerProperties.getBasePath().forEach(p -> builder.paths(PathSelectors.ant(p))); + swaggerProperties.getExcludePath().forEach(p -> builder.paths(PathSelectors.ant(p).negate())); + + return builder.build().securitySchemes(Collections.singletonList(securitySchema(swaggerProperties))) + .securityContexts(Collections.singletonList(securityContext(swaggerProperties))).pathMapping("/"); + } + + /** + * 配置默认的全局鉴权策略的开关,通过正则表达式进行匹配;默认匹配所有URL + * @return + */ + private static SecurityContext securityContext(SwaggerProperties swaggerProperties) { + return SecurityContext.builder().securityReferences(defaultAuth(swaggerProperties)).build(); + } + + /** + * 默认的全局鉴权策略 + * @return + */ + private static List defaultAuth(SwaggerProperties swaggerProperties) { + ArrayList authorizationScopeList = new ArrayList<>(); + swaggerProperties.getAuthorization().getAuthorizationScopeList() + .forEach(authorizationScope -> authorizationScopeList.add( + new AuthorizationScope(authorizationScope.getScope(), authorizationScope.getDescription()))); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[authorizationScopeList.size()]; + return Collections + .singletonList(SecurityReference.builder().reference(swaggerProperties.getAuthorization().getName()) + .scopes(authorizationScopeList.toArray(authorizationScopes)).build()); + } + + private static OAuth securitySchema(SwaggerProperties swaggerProperties) { + ArrayList authorizationScopeList = new ArrayList<>(); + swaggerProperties.getAuthorization().getAuthorizationScopeList() + .forEach(authorizationScope -> authorizationScopeList.add( + new AuthorizationScope(authorizationScope.getScope(), authorizationScope.getDescription()))); + ArrayList grantTypes = new ArrayList<>(); + swaggerProperties.getAuthorization().getTokenUrlList() + .forEach(tokenUrl -> grantTypes.add(new ResourceOwnerPasswordCredentialsGrant(tokenUrl))); + return new OAuth(swaggerProperties.getAuthorization().getName(), authorizationScopeList, grantTypes); + } + + private static ApiInfo apiInfo(SwaggerProperties swaggerProperties) { + return new ApiInfoBuilder().title(swaggerProperties.getTitle()).description(swaggerProperties.getDescription()) + .license(swaggerProperties.getLicense()).licenseUrl(swaggerProperties.getLicenseUrl()) + .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl()) + .contact(new Contact(swaggerProperties.getContact().getName(), swaggerProperties.getContact().getUrl(), + swaggerProperties.getContact().getEmail())) + .version(swaggerProperties.getVersion()).build(); + } + + @Bean + public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() { + return new BeanPostProcessor() { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof WebMvcRequestHandlerProvider) { + customizeSpringfoxHandlerMappings(getHandlerMappings(bean)); + } + return bean; + } + + private void customizeSpringfoxHandlerMappings( + List mappings) { + List copy = mappings.stream().filter(mapping -> mapping.getPatternParser() == null) + .collect(Collectors.toList()); + mappings.clear(); + mappings.addAll(copy); + } + + @SuppressWarnings("unchecked") + private List getHandlerMappings(Object bean) { + try { + Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings"); + field.setAccessible(true); + return (List) field.get(bean); + } + catch (IllegalArgumentException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + }; + } + +} diff --git a/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/config/WebFluxSwaggerConfiguration.java b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/config/WebFluxSwaggerConfiguration.java new file mode 100644 index 0000000..3eb8f55 --- /dev/null +++ b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/config/WebFluxSwaggerConfiguration.java @@ -0,0 +1,23 @@ +package com.cloud.kicc.common.swagger.config; + +import org.springframework.web.reactive.config.ResourceHandlerRegistry; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +/** + *

+ * webflux 网关 swagger 资源路径配置 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class WebFluxSwaggerConfiguration implements WebFluxConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") + .resourceChain(false); + } + +} diff --git a/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerBasicGatewayFilter.java b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerBasicGatewayFilter.java new file mode 100644 index 0000000..8a24b27 --- /dev/null +++ b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerBasicGatewayFilter.java @@ -0,0 +1,78 @@ +package com.cloud.kicc.common.swagger.support; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.Base64Utils; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +/** + *

+ * swagger 开启basic 认证 + * 检测请求是否有Base64的客户端ID,如何没有进行过滤 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +@RequiredArgsConstructor +public class SwaggerBasicGatewayFilter implements GlobalFilter { + + private static final String API_URI = "/v2/api-docs"; + + private static final String BASIC_PREFIX = "Basic "; + + private final SwaggerProperties swaggerProperties; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + + if (!request.getURI().getPath().contains(API_URI)) { + return chain.filter(exchange); + } + + if (hasAuth(exchange)) { + return chain.filter(exchange); + } + else { + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(HttpStatus.UNAUTHORIZED); + response.getHeaders().add(HttpHeaders.WWW_AUTHENTICATE, "Basic Realm=\"kicc\""); + return response.setComplete(); + } + } + + /** + * 简单的basic认证 + * @param exchange 上下文 + * @return 是否有权限 + */ + private boolean hasAuth(ServerWebExchange exchange) { + ServerHttpRequest request = exchange.getRequest(); + String auth = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + log.info("Basic认证信息为:{}", auth); + if (!StringUtils.hasText(auth) || !auth.startsWith(BASIC_PREFIX)) { + return Boolean.FALSE; + } + + String username = swaggerProperties.getBasic().getUsername(); + String password = swaggerProperties.getBasic().getPassword(); + + String encodeToString = Base64Utils + .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); + + return auth.equals(BASIC_PREFIX + encodeToString); + } + +} diff --git a/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerProperties.java b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerProperties.java new file mode 100644 index 0000000..1eae24b --- /dev/null +++ b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerProperties.java @@ -0,0 +1,177 @@ +package com.cloud.kicc.common.swagger.support; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * SwaggerProperties + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Data +@ConfigurationProperties("swagger") +public class SwaggerProperties { + + /** + * 是否开启swagger + */ + private Boolean enabled = true; + + /** + * swagger会解析的包路径 + **/ + private String basePackage = ""; + + /** + * swagger会解析的url规则 + **/ + private List basePath = new ArrayList<>(); + + /** + * 在basePath基础上需要排除的url规则 + **/ + private List excludePath = new ArrayList<>(); + + /** + * 需要排除的服务 + */ + private List ignoreProviders = new ArrayList<>(); + + /** + * 标题 + **/ + private String title = ""; + + /** + * 描述 + **/ + private String description = ""; + + /** + * 版本 + **/ + private String version = ""; + + /** + * 许可证 + **/ + private String license = ""; + + /** + * 许可证URL + **/ + private String licenseUrl = ""; + + /** + * 服务条款URL + **/ + private String termsOfServiceUrl = ""; + + /** + * host信息 + **/ + private String host = ""; + + /** + * 联系人信息 + */ + private Contact contact = new Contact(); + + /** + * 全局统一鉴权配置 + **/ + private Authorization authorization = new Authorization(); + + /** + * 认证参数 + */ + private SwaggerBasic basic = new SwaggerBasic(); + + @Data + @NoArgsConstructor + public static class Contact { + + /** + * 联系人 + **/ + private String name = ""; + + /** + * 联系人url + **/ + private String url = ""; + + /** + * 联系人email + **/ + private String email = ""; + + } + + @Data + @NoArgsConstructor + public static class Authorization { + + /** + * 鉴权策略ID,需要和SecurityReferences ID保持一致 + */ + private String name = ""; + + /** + * 需要开启鉴权URL的正则 + */ + private String authRegex = "^.*$"; + + /** + * 鉴权作用域列表 + */ + private List authorizationScopeList = new ArrayList<>(); + + private List tokenUrlList = new ArrayList<>(); + + } + + @Data + @NoArgsConstructor + public static class AuthorizationScope { + + /** + * 作用域名称 + */ + private String scope = ""; + + /** + * 作用域描述 + */ + private String description = ""; + + } + + @Data + public static class SwaggerBasic { + + /** + * 是否开启 basic 认证 + */ + private Boolean enabled; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + } + +} diff --git a/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerProvider.java b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerProvider.java new file mode 100644 index 0000000..100e0f7 --- /dev/null +++ b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerProvider.java @@ -0,0 +1,65 @@ +package com.cloud.kicc.common.swagger.support; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.config.GatewayProperties; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.support.NameUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; +import springfox.documentation.swagger.web.SwaggerResource; +import springfox.documentation.swagger.web.SwaggerResourcesProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * 聚合接口文档注册,和zuul实现类似 + * 获取网关重新截取网关代理前缀拼接/v2/api-docs,创建resources文档 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Primary +@RequiredArgsConstructor +public class SwaggerProvider implements SwaggerResourcesProvider { + + private static final String API_URI = "/v2/api-docs"; + + private final SwaggerProperties swaggerProperties; + + private final GatewayProperties gatewayProperties; + + @Lazy + @Autowired + private RouteLocator routeLocator; + + @Override + public List get() { + List resources = new ArrayList<>(); + List routes = new ArrayList<>(); + // 添加网关路由Id + routeLocator.getRoutes().subscribe(route -> routes.add(route.getId())); + // 当前加载进内存的网关路由去匹配yml中配置的网关路由匹配则添加到Swagger中 + gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())) + .forEach(routeDefinition -> routeDefinition.getPredicates().stream() + .filter(predicateDefinition -> "Path".equalsIgnoreCase(predicateDefinition.getName())) + .filter(predicateDefinition -> !swaggerProperties.getIgnoreProviders() + .contains(routeDefinition.getId())) + .forEach(predicateDefinition -> resources + .add(swaggerResource(routeDefinition.getId(), predicateDefinition.getArgs() + .get(NameUtils.GENERATED_NAME_PREFIX + "0").replace("/**", API_URI))))); + return resources; + } + + private static SwaggerResource swaggerResource(String name, String location) { + SwaggerResource swaggerResource = new SwaggerResource(); + swaggerResource.setName(name); + swaggerResource.setLocation(location); + swaggerResource.setSwaggerVersion("2.0"); + return swaggerResource; + } + +} diff --git a/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerResourceHandler.java b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerResourceHandler.java new file mode 100644 index 0000000..53c34f1 --- /dev/null +++ b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerResourceHandler.java @@ -0,0 +1,40 @@ +package com.cloud.kicc.common.swagger.support; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import springfox.documentation.swagger.web.SwaggerResourcesProvider; + +/** + *

+ * Swagger资源处理 + * WebFlux返回 + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +@RequiredArgsConstructor +public class SwaggerResourceHandler implements HandlerFunction { + + private final SwaggerResourcesProvider swaggerResources; + + /** + * Handle the given request. + * @param request the request to handler + * @return the response + */ + @Override + public Mono handle(ServerRequest request) { + return ServerResponse.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(swaggerResources.get())); + } + +} diff --git a/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerSecurityHandler.java b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerSecurityHandler.java new file mode 100644 index 0000000..1a45b34 --- /dev/null +++ b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerSecurityHandler.java @@ -0,0 +1,39 @@ +package com.cloud.kicc.common.swagger.support; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import springfox.documentation.swagger.web.SecurityConfiguration; + +/** + *

+ * SwaggerSecurityHandler + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +@RequiredArgsConstructor +public class SwaggerSecurityHandler implements HandlerFunction { + + private final SecurityConfiguration securityConfiguration; + + /** + * Handle the given request. + * @param request the request to handler + * @return the response + */ + @Override + public Mono handle(ServerRequest request) { + return ServerResponse.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(securityConfiguration)); + } + +} diff --git a/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerUiHandler.java b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerUiHandler.java new file mode 100644 index 0000000..1662b6a --- /dev/null +++ b/kicc-common-swagger/src/main/java/com/cloud/kicc/common/swagger/support/SwaggerUiHandler.java @@ -0,0 +1,39 @@ +package com.cloud.kicc.common.swagger.support; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import springfox.documentation.swagger.web.UiConfiguration; + +/** + *

+ * SwaggerUiHandler + *

+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +@Slf4j +@RequiredArgsConstructor +public class SwaggerUiHandler implements HandlerFunction { + + private final UiConfiguration uiConfiguration; + + /** + * Handle the given request. + * @param request the request to handler + * @return the response + */ + @Override + public Mono handle(ServerRequest request) { + return ServerResponse.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(uiConfiguration)); + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0213085 --- /dev/null +++ b/pom.xml @@ -0,0 +1,160 @@ + + + 4.0.0 + + com.cloud + ${project.artifactId} + 1.0.0 + pom + kicc-tool + + https://www.kanglailab.com + 康来生物有限公司kicc 工具库 + + + UTF-8 + 1.8 + 1.8 + 2.6.3 + 2021.0.6 + 2021.0.5.0 + 2.6.2 + 5.7.19 + 3.6.1 + 2.1.0 + 3.0.0 + 2.0.9 + 2.3.0 + 4.4 + 12.2.0.1 + 4.9.9 + 0.0.29 + 3.8.1 + 3.3.0 + 0.32.0 + + + + + + + + com.cloud + kicc-common-bom + ${project.version} + pom + import + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + + + ${project.name} + + + src/main/resources + true + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + ${git.commit.plugin} + + + + get-the-git-infos + + revision + + initialize + + + + false + true + + yyyy-MM-dd HH:mm:ss + + ^git.build.(time|version)$ + ^git.commit.(id|message|time).*$ + + + + + + io.spring.javaformat + spring-javaformat-maven-plugin + ${spring.checkstyle.plugin} + + + + org.apache.maven.plugins + maven-compiler-plugin + ${apache.compiler.plugin} + + ${maven.compiler.source} + ${maven.compiler.target} + ${project.build.sourceEncoding} + + + + + org.apache.maven.plugins + maven-source-plugin + ${apache.source.plugin} + + + attach-sources + + jar + + + + + + + + + kicc-common-bom + kicc-common-core + kicc-common-log + kicc-common-datasource + kicc-common-data + kicc-common-security + kicc-common-feign + kicc-common-swagger + kicc-common-mock + kicc-common-job + kicc-common-rocketmq + kicc-common-seata + + +