build: continew-starter 2.14.0 => 2.15.0

1.适配 cs Knife4j 替换为 NextDoc4j 1.1.5
2.适配 cs continew-starter-auth-justauth
3.cs DataPermission 注解增加缓存处理,缓存 Mapper 接口方法上携带 DataPermission 的值
4.cs 修复开启 SSL 后,mail.host 不被 JDK 信任的问题
5.cs EnumValue 比较枚举值时,不再区分大小写
6.cs 依赖全面升级 spring-boot 3.3.12 => 3.4.10,snail-job 1.5.0 => 1.8.0
This commit is contained in:
2025-12-28 22:40:17 +08:00
parent 9776dfbf31
commit c1a0eb165f
23 changed files with 558 additions and 616 deletions

View File

@@ -4,7 +4,7 @@
<img src="https://img.shields.io/badge/SNAPSHOT-v4.2.0-%23ff3f59.svg" alt="Release" />
</a>
<a href="https://github.com/continew-org/continew-starter" title="ContiNew Starter" target="_blank">
<img src="https://img.shields.io/badge/ContiNew Starter-2.14.0-%236CB52D.svg" alt="ContiNew Starter" />
<img src="https://img.shields.io/badge/ContiNew Starter-2.15.0-%236CB52D.svg" alt="ContiNew Starter" />
</a>
<a href="https://spring.io/projects/spring-boot" title="Spring Boot" target="_blank">
<img src="https://img.shields.io/badge/Spring Boot-3.3.12-%236CB52D.svg?logo=Spring-Boot" alt="Spring Boot" />
@@ -234,41 +234,41 @@ public class DeptController extends BaseController<DeptService, DeptResp, DeptDe
## 核心技术栈
| 名称 | 版本 | 简介 |
| :----------------------------------------------------------- |:-------------| :----------------------------------------------------------- |
| <a href="https://vuejs.org/" target="_blank">Vue</a> | 3.5.4 | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 |
| <a href="https://arco.design/vue/docs/start" target="_blank">Arco Design</a> | 2.57.0 | 字节跳动推出的前端 UI 框架,年轻化的色彩和组件设计。 |
| <a href="https://www.typescriptlang.org/zh/" target="_blank">TypeScript</a> | 5.0.4 | TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成。 |
| <a href="https://vite.dev/" target="_blank">Vite</a> | 5.1.5 | 下一代的前端工具链,为开发提供极速响应。 |
| [ContiNew Starter](https://github.com/continew-org/continew-starter) | 2.14.0 | ContiNew Starter 包含了一系列经过企业实践优化的依赖包(如 MyBatis-Plus、SaToken可轻松集成到应用中为开发人员减少手动引入依赖及配置的麻烦为 Spring Boot Web 项目的灵活快速构建提供支持。 |
| <a href="https://spring.io/projects/spring-boot" target="_blank">Spring Boot</a> | 3.3.12 | 简化 Spring 应用的初始搭建和开发过程基于“约定优于配置”的理念使开发人员不再需要定义样板化的配置。Spring Boot 3.0 开始,要求 Java 17 作为最低版本) |
| <a href="https://undertow.io/" target="_blank">Undertow</a> | 2.3.18.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 |
| <a href="https://sa-token.dev33.cn/" target="_blank">Sa-Token + JWT</a> | 1.44.0 | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。 |
| <a href="https://baomidou.com/" target="_blank">MyBatis Plus</a> | 3.5.12 | MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率。 |
| 名称 | 版本 | 简介 |
|:----------------------------------------------------------------------------------------------------------------------------------|:-------------| :----------------------------------------------------------- |
| <a href="https://vuejs.org/" target="_blank">Vue</a> | 3.5.4 | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 |
| <a href="https://arco.design/vue/docs/start" target="_blank">Arco Design</a> | 2.57.0 | 字节跳动推出的前端 UI 框架,年轻化的色彩和组件设计。 |
| <a href="https://www.typescriptlang.org/zh/" target="_blank">TypeScript</a> | 5.0.4 | TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成。 |
| <a href="https://vite.dev/" target="_blank">Vite</a> | 5.1.5 | 下一代的前端工具链,为开发提供极速响应。 |
| [ContiNew Starter](https://github.com/continew-org/continew-starter) | 2.15.0 | ContiNew Starter 包含了一系列经过企业实践优化的依赖包(如 MyBatis-Plus、SaToken可轻松集成到应用中为开发人员减少手动引入依赖及配置的麻烦为 Spring Boot Web 项目的灵活快速构建提供支持。 |
| <a href="https://spring.io/projects/spring-boot" target="_blank">Spring Boot</a> | 3.3.12 | 简化 Spring 应用的初始搭建和开发过程基于“约定优于配置”的理念使开发人员不再需要定义样板化的配置。Spring Boot 3.0 开始,要求 Java 17 作为最低版本) |
| <a href="https://undertow.io/" target="_blank">Undertow</a> | 2.3.18.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 |
| <a href="https://sa-token.dev33.cn/" target="_blank">Sa-Token + JWT</a> | 1.44.0 | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。 |
| <a href="https://baomidou.com/" target="_blank">MyBatis Plus</a> | 3.5.12 | MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率。 |
| <a href="https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611" target="_blank">dynamic-datasource-spring-boot-starter</a> | 4.3.1 | 基于 Spring Boot 的快速集成多数据源的启动器。 |
| Hikari | 5.1.0 | JDBC 连接池,号称 “史上最快连接池”SpringBoot 在 2.0 之后,采用的默认数据库连接池就是 Hikari。 |
| <a href="https://dev.mysql.com/downloads/mysql/" target="_blank">MySQL</a> | 8.0.42 | 体积小、速度快、总体拥有成本低,是最流行的关系型数据库管理系统之一。 |
| <a href="https://dev.mysql.com/doc/connector-j/8.0/en/" target="_blank">mysql-connector-j</a> | 8.3.0 | MySQL Java 驱动。 |
| <a href="https://github.com/p6spy/p6spy" target="_blank">P6Spy</a> | 3.9.1 | SQL 性能分析组件。 |
| <a href="https://github.com/liquibase/liquibase" target="_blank">Liquibase</a> | 4.27.0 | 用于管理数据库版本,跟踪、管理和应用数据库变化。 |
| [JetCache](https://github.com/alibaba/jetcache/blob/master/docs/CN/Readme.md) | 2.7.8 | 一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新还提供了 Cache 接口用于手工缓存操作。 |
| <a href="https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D" target="_blank">Redisson</a> | 3.49.0 | 不仅仅是一个 Redis Java 客户端Redisson 充分的利用了 Redis 键值数据库提供的一系列优势,为使用者提供了一系列具有分布式特性的常用工具:分布式锁、限流器等。 |
| <a href="https://redis.io/" target="_blank">Redis</a> | 7.2.8 | 高性能的 key-value 数据库。 |
| [Snail Job](https://snailjob.opensnail.com/) | 1.5.0 | 灵活,可靠和快速的分布式任务重试和分布式任务调度平台。 |
| [X File Storage](https://x-file-storage.xuyanwu.cn/#/) | 2.2.1 | 一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS...等其它兼容 S3 协议的存储平台。 |
| <a href="https://sms4j.com/" target="_blank">SMS4J</a> | 3.3.4 | 短信聚合框架,轻松集成多家短信服务,解决接入多个短信 SDK 的繁琐流程。 |
| <a href="https://justauth.cn/" target="_blank">Just Auth</a> | 1.16.7 | 开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK让登录变得 So easy |
| <a href="https://github.com/fast-excel/fastexcel" target="_blank">Fast Excel</a> | 1.2.0 | (由原 EasyExcel 作者创建的新项目)一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。 |
| [AJ-Captcha](https://ajcaptcha.beliefteam.cn/captcha-doc/) | 1.3.0 | Java 行为验证码包含滑动拼图、文字点选两种方式UI支持弹出和嵌入两种方式。 |
| Easy Captcha | 1.6.2 | Java 图形验证码,支持 gif、中文、算术等类型可用于 Java Web、JavaSE 等项目。 |
| [Crane4j](https://createsequence.gitee.io/crane4j-doc/#/) | 2.9.0 | 一个基于注解的,用于完成一切 “根据 A 的 key 值拿到 B再把 B 的属性映射到 A” 这类需求的字段填充框架。 |
| [SpEL Validator](https://spel-validator.sticki.cn/) | 0.5.2-beta | 基于 SpEL 的 jakarta.validation-api 扩展增强包。 |
| [CosID](https://cosid.ahoo.me/guide/getting-started.html) | 2.13.0 | 旨在提供通用、灵活、高性能的分布式 ID 生成器。 |
| [Graceful Response](https://doc.feiniaojin.com/graceful-response/home.html) | 5.0.4-boot3 | 一个Spring Boot技术栈下的优雅响应处理组件可以帮助开发者完成响应数据封装、异常处理、错误码填充等过程提高开发效率提高代码质量。 |
| <a href="https://doc.xiaominfo.com/" target="_blank">Knife4j</a> | 4.5.0 | 前身是 swagger-bootstrap-ui集 Swagger2 和 OpenAPI3 为一体的增强解决方案。 |
| [OpenFeign](https://springdoc.cn/spring-cloud-openfeign/) | 13.5 | Spring Cloud OpenFeign 是一种基于 Spring Cloud 的声明式 REST 客户端,它简化了与 HTTP 服务交互的过程。 |
| <a href="https://www.hutool.cn/" target="_blank">Hutool</a> | 5.8.38 | 小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。 |
| <a href="https://projectlombok.org/" target="_blank">Lombok</a> | 1.18.36 | 在 Java 开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码,让编写的类更加简洁。 |
| Hikari | 5.1.0 | JDBC 连接池,号称 “史上最快连接池”SpringBoot 在 2.0 之后,采用的默认数据库连接池就是 Hikari。 |
| <a href="https://dev.mysql.com/downloads/mysql/" target="_blank">MySQL</a> | 8.0.42 | 体积小、速度快、总体拥有成本低,是最流行的关系型数据库管理系统之一。 |
| <a href="https://dev.mysql.com/doc/connector-j/8.0/en/" target="_blank">mysql-connector-j</a> | 8.3.0 | MySQL Java 驱动。 |
| <a href="https://github.com/p6spy/p6spy" target="_blank">P6Spy</a> | 3.9.1 | SQL 性能分析组件。 |
| <a href="https://github.com/liquibase/liquibase" target="_blank">Liquibase</a> | 4.27.0 | 用于管理数据库版本,跟踪、管理和应用数据库变化。 |
| [JetCache](https://github.com/alibaba/jetcache/blob/master/docs/CN/Readme.md) | 2.7.8 | 一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新还提供了 Cache 接口用于手工缓存操作。 |
| <a href="https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D" target="_blank">Redisson</a> | 3.49.0 | 不仅仅是一个 Redis Java 客户端Redisson 充分的利用了 Redis 键值数据库提供的一系列优势,为使用者提供了一系列具有分布式特性的常用工具:分布式锁、限流器等。 |
| <a href="https://redis.io/" target="_blank">Redis</a> | 7.2.8 | 高性能的 key-value 数据库。 |
| [Snail Job](https://snailjob.opensnail.com/) | 1.5.0 | 灵活,可靠和快速的分布式任务重试和分布式任务调度平台。 |
| [X File Storage](https://x-file-storage.xuyanwu.cn/#/) | 2.2.1 | 一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS...等其它兼容 S3 协议的存储平台。 |
| <a href="https://sms4j.com/" target="_blank">SMS4J</a> | 3.3.4 | 短信聚合框架,轻松集成多家短信服务,解决接入多个短信 SDK 的繁琐流程。 |
| <a href="https://justauth.cn/" target="_blank">Just Auth</a> | 1.16.7 | 开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK让登录变得 So easy |
| <a href="https://github.com/fast-excel/fastexcel" target="_blank">Fast Excel</a> | 1.2.0 | (由原 EasyExcel 作者创建的新项目)一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。 |
| [AJ-Captcha](https://ajcaptcha.beliefteam.cn/captcha-doc/) | 1.3.0 | Java 行为验证码包含滑动拼图、文字点选两种方式UI支持弹出和嵌入两种方式。 |
| Easy Captcha | 1.6.2 | Java 图形验证码,支持 gif、中文、算术等类型可用于 Java Web、JavaSE 等项目。 |
| [Crane4j](https://createsequence.gitee.io/crane4j-doc/#/) | 2.9.0 | 一个基于注解的,用于完成一切 “根据 A 的 key 值拿到 B再把 B 的属性映射到 A” 这类需求的字段填充框架。 |
| [SpEL Validator](https://spel-validator.sticki.cn/) | 0.5.2-beta | 基于 SpEL 的 jakarta.validation-api 扩展增强包。 |
| [CosID](https://cosid.ahoo.me/guide/getting-started.html) | 2.13.0 | 旨在提供通用、灵活、高性能的分布式 ID 生成器。 |
| [Graceful Response](https://doc.feiniaojin.com/graceful-response/home.html) | 5.0.4-boot3 | 一个Spring Boot技术栈下的优雅响应处理组件可以帮助开发者完成响应数据封装、异常处理、错误码填充等过程提高开发效率提高代码质量。 |
| <a href="https://nextdoc4j.top/" target="_blank">NextDoc4j</a> | 1.1.5 | 现代化 API 文档 UI 工具,全面替代 Swagger UI。 |
| [OpenFeign](https://springdoc.cn/spring-cloud-openfeign/) | 13.5 | Spring Cloud OpenFeign 是一种基于 Spring Cloud 的声明式 REST 客户端,它简化了与 HTTP 服务交互的过程。 |
| <a href="https://www.hutool.cn/" target="_blank">Hutool</a> | 5.8.38 | 小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。 |
| <a href="https://projectlombok.org/" target="_blank">Lombok</a> | 1.18.36 | 在 Java 开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码,让编写的类更加简洁。 |
## 快速开始
@@ -550,7 +550,7 @@ ContiNew 系列项目采用清晰的分支策略,确保开发与维护有序
### 特别鸣谢
- 感谢 <a href="https://www.jetbrains.com/" target="_blank">JetBrains</a> 提供的 <a href="https://jb.gg/OpenSourceSupport" target="_blank">非商业开源软件开发授权</a>
- 感谢 <a href="https://github.com/baomidou/mybatis-plus" target="_blank">MyBatis Plus</a><a href="https://github.com/dromara/sa-token" target="_blank">Sa-Token</a><a href="https://github.com/alibaba/jetcache" target="_blank">JetCache</a><a href="https://github.com/opengoofy/crane4j" target="_blank">Crane4j</a><a href="https://github.com/xiaoymin/knife4j" target="_blank">Knife4j</a><a href="https://github.com/dromara/hutool" target="_blank">Hutool</a> 等开源组件作者为国内开源世界作出的贡献
- 感谢 <a href="https://github.com/baomidou/mybatis-plus" target="_blank">MyBatis Plus</a><a href="https://github.com/dromara/sa-token" target="_blank">Sa-Token</a><a href="https://github.com/alibaba/jetcache" target="_blank">JetCache</a><a href="https://github.com/opengoofy/crane4j" target="_blank">Crane4j</a><a href="https://nextdoc4j.top/" target="_blank">NextDoc4j</a><a href="https://github.com/dromara/hutool" target="_blank">Hutool</a> 等开源组件作者为国内开源世界作出的贡献
- 感谢项目使用或未使用到的每一款开源组件,致敬各位开源先驱 :fire:
## License

View File

@@ -101,6 +101,11 @@
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sign</artifactId>
</dependency>
<!-- NextDoc4j - SaToken 权限展示插件 -->
<dependency>
<groupId>top.nextdoc4j</groupId>
<artifactId>nextdoc4j-plugin-security-satoken</artifactId>
</dependency>
<!-- ContiNew Starter 认证模块 - JustAuth -->
<dependency>

View File

@@ -1,217 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.common.config.doc;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.map.MapUtil;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.*;
import top.continew.starter.apidoc.autoconfigure.SpringDocExtensionProperties;
import top.continew.starter.auth.satoken.autoconfigure.SaTokenExtensionProperties;
import top.continew.starter.core.util.CollUtils;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
/**
* 全局鉴权参数定制器
*
* @author echo
* @since 2024/12/31 13:36
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GlobalAuthenticationCustomizer implements GlobalOpenApiCustomizer {
private final SpringDocExtensionProperties properties;
private final SaTokenExtensionProperties saTokenExtensionProperties;
private final ApplicationContext context;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 定制 OpenAPI 文档
*
* @param openApi 当前 OpenAPI 对象
*/
@Override
public void customise(OpenAPI openApi) {
if (MapUtil.isEmpty(openApi.getPaths())) {
return;
}
// 收集需要排除的路径(包括 Sa-Token 配置中的排除路径和 @SaIgnore 注解路径)
Set<String> excludedPaths = collectExcludedPaths();
// 遍历所有路径,为需要鉴权的路径添加安全认证配置
openApi.getPaths().forEach((path, pathItem) -> {
if (isPathExcluded(path, excludedPaths)) {
// 路径在排除列表中,跳过处理
return;
}
// 为路径添加安全认证参数
addAuthenticationParameters(pathItem);
});
}
/**
* 收集所有需要排除的路径
*
* @return 排除路径集合
*/
private Set<String> collectExcludedPaths() {
Set<String> excludedPaths = new HashSet<>();
excludedPaths.addAll(Arrays.asList(saTokenExtensionProperties.getSecurity().getExcludes()));
excludedPaths.addAll(resolveSaIgnorePaths());
return excludedPaths;
}
/**
* 为路径项添加认证参数
*
* @param pathItem 当前路径项
*/
private void addAuthenticationParameters(PathItem pathItem) {
Components components = properties.getComponents();
if (components == null || MapUtil.isEmpty(components.getSecuritySchemes())) {
return;
}
Map<String, SecurityScheme> securitySchemes = components.getSecuritySchemes();
List<String> schemeNames = CollUtils.mapToList(securitySchemes.values(), SecurityScheme::getName);
pathItem.readOperations().forEach(operation -> {
SecurityRequirement securityRequirement = new SecurityRequirement();
schemeNames.forEach(securityRequirement::addList);
operation.addSecurityItem(securityRequirement);
});
}
/**
* 解析所有带有 @SaIgnore 注解的路径
*
* @return 被忽略的路径集合
*/
private Set<String> resolveSaIgnorePaths() {
// 获取所有标注 @RestController 的 Bean
Map<String, Object> controllers = context.getBeansWithAnnotation(RestController.class);
Set<String> ignoredPaths = new HashSet<>();
// 遍历所有控制器,解析 @SaIgnore 注解路径
controllers.values().forEach(controllerBean -> {
Class<?> controllerClass = AopUtils.getTargetClass(controllerBean);
List<String> classPaths = getClassPaths(controllerClass);
// 类级别的 @SaIgnore 注解
if (controllerClass.isAnnotationPresent(SaIgnore.class)) {
classPaths.forEach(classPath -> ignoredPaths.add(classPath + "/**"));
}
// 方法级别的 @SaIgnore 注解
Arrays.stream(controllerClass.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(SaIgnore.class))
.forEach(method -> ignoredPaths.addAll(combinePaths(classPaths, getMethodPaths(method))));
});
return ignoredPaths;
}
/**
* 获取类上的所有路径
*
* @param controller 控制器类
* @return 类路径列表
*/
private List<String> getClassPaths(Class<?> controller) {
List<String> classPaths = new ArrayList<>();
// 处理 @RequestMapping 注解
if (controller.isAnnotationPresent(RequestMapping.class)) {
RequestMapping mapping = controller.getAnnotation(RequestMapping.class);
classPaths.addAll(Arrays.asList(mapping.value()));
}
// 处理 @CrudRequestMapping 注解
if (controller.isAnnotationPresent(CrudRequestMapping.class)) {
CrudRequestMapping mapping = controller.getAnnotation(CrudRequestMapping.class);
if (!mapping.value().isEmpty()) {
classPaths.add(mapping.value());
}
}
return classPaths;
}
/**
* 获取方法上的所有路径
*
* @param method 控制器方法
* @return 方法路径列表
*/
private List<String> getMethodPaths(Method method) {
List<String> methodPaths = new ArrayList<>();
// 检查方法上的各种映射注解
if (method.isAnnotationPresent(GetMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(GetMapping.class).value()));
} else if (method.isAnnotationPresent(PostMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(PostMapping.class).value()));
} else if (method.isAnnotationPresent(PutMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(PutMapping.class).value()));
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(DeleteMapping.class).value()));
} else if (method.isAnnotationPresent(RequestMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(RequestMapping.class).value()));
} else if (method.isAnnotationPresent(PatchMapping.class)) {
methodPaths.addAll(Arrays.asList(method.getAnnotation(PatchMapping.class).value()));
}
return methodPaths;
}
/**
* 组合类路径和方法路径
*
* @param classPaths 类路径列表
* @param methodPaths 方法路径列表
* @return 完整路径集合
*/
private Set<String> combinePaths(List<String> classPaths, List<String> methodPaths) {
return classPaths.stream()
.flatMap(classPath -> methodPaths.stream().map(methodPath -> classPath + methodPath))
.collect(Collectors.toSet());
}
/**
* 检查路径是否在排除列表中
*
* @param path 当前路径
* @param excludedPaths 排除路径集合,支持通配符
* @return 是否匹配排除规则
*/
private boolean isPathExcluded(String path, Set<String> excludedPaths) {
return excludedPaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, path));
}
}

View File

@@ -1,64 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.common.config.doc;
import cn.hutool.core.util.StrUtil;
import io.swagger.v3.oas.models.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import java.util.ArrayList;
import java.util.List;
/**
* 全局描述定制器 - 处理 sa-token 的注解权限码
*
* @author echo
* @since 2025/1/24 14:59
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GlobalDescriptionCustomizer implements GlobalOperationCustomizer {
@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
// 将 sa-token 注解数据添加到 operation 的描述中
// 权限
List<String> noteList = new ArrayList<>(new OperationDescriptionCustomizer().getPermission(handlerMethod));
// 如果注解数据列表为空,直接返回原 operation
if (noteList.isEmpty()) {
return operation;
}
// 拼接注解数据为字符串
String noteStr = StrUtil.join("<br/>", noteList);
// 获取原描述
String originalDescription = operation.getDescription();
// 根据原描述是否为空,更新描述
String newDescription = StrUtil.isNotEmpty(originalDescription)
? originalDescription + "<br/>" + noteStr
: noteStr;
// 设置新描述
operation.setDescription(newDescription);
return operation;
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.common.config.doc;
import cn.hutool.core.util.ClassUtil;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.Schema;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.reflect.TypeUtils;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.service.GenericResponseService;
import org.springdoc.core.service.OperationService;
import org.springdoc.core.utils.PropertyResolverUtils;
import org.springdoc.core.utils.SpringDocAnnotationsUtils;
import org.springframework.core.ResolvableType;
import org.springframework.stereotype.Component;
import top.continew.starter.web.autoconfigure.response.GlobalResponseProperties;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import static org.springdoc.core.converters.ConverterUtils.isResponseTypeWrapper;
import static org.springdoc.core.utils.SpringDocAnnotationsUtils.extractSchema;
/**
* 全局响应操作自定义器
* <p>
* 自定义 OpenAPI 文档中的响应结构,将原始返回类型包装为统一的响应格式
* </p>
*
* @author echo
* @since 2025/07/08 09:34
*/
@Component
public class GlobalSpringDocResponseOperationCustomizer extends GenericResponseService {
private final GlobalResponseProperties globalResponseProperties;
private final Class<Object> responseClass;
private final PropertyResolverUtils propertyResolverUtils;
public GlobalSpringDocResponseOperationCustomizer(OperationService operationService,
SpringDocConfigProperties springDocConfigProperties,
PropertyResolverUtils propertyResolverUtils,
GlobalResponseProperties globalResponseProperties) {
super(operationService, springDocConfigProperties, propertyResolverUtils);
this.globalResponseProperties = globalResponseProperties;
this.responseClass = ClassUtil.loadClass(globalResponseProperties.getResponseClassFullName());
this.propertyResolverUtils = propertyResolverUtils;
}
/**
* 构建响应内容
*
* @param components 组件信息
* @param annotations 方法注解
* @param methodProduces 方法支持的媒体类型
* @param jsonView JSON 视图
* @param returnType 返回类型
* @return 响应内容
*/
@Override
public Content buildContent(Components components,
Annotation[] annotations,
String[] methodProduces,
JsonView jsonView,
Type returnType) {
if (ArrayUtils.isEmpty(methodProduces)) {
return new Content();
}
// 如果返回类型已经包含全局响应包装类,直接处理
if (isAlreadyWrapped(returnType)) {
return buildContentForWrappedType(components, annotations, methodProduces, jsonView, returnType);
}
// 包装返回类型为全局响应格式
Type wrappedType = wrapReturnType(returnType);
if (isVoid(wrappedType)) {
return null;
}
return buildContentForWrappedType(components, annotations, methodProduces, jsonView, wrappedType);
}
/**
* 检查返回类型是否已被全局响应包装
*
* @param returnType 返回类型
* @return 是否已被包装
*/
private boolean isAlreadyWrapped(Type returnType) {
return returnType.getTypeName().contains(globalResponseProperties.getResponseClassFullName());
}
/**
* 包装返回类型为全局响应格式
*
* @param returnType 原始返回类型
* @return 包装后的类型
*/
private Type wrapReturnType(Type returnType) {
if (returnType == void.class || returnType == Void.class) {
return TypeUtils.parameterize(responseClass, Void.class);
}
return TypeUtils.parameterize(responseClass, returnType);
}
/**
* 为包装后的类型构建内容
*
* @param components 组件信息
* @param annotations 方法注解
* @param methodProduces 方法支持的媒体类型
* @param jsonView JSON 视图
* @param returnType 返回类型
* @return 响应内容
*/
private Content buildContentForWrappedType(Components components,
Annotation[] annotations,
String[] methodProduces,
JsonView jsonView,
Type returnType) {
Content content = new Content();
Schema<?> schema = calculateSchema(components, returnType, jsonView, annotations);
if (schema != null) {
io.swagger.v3.oas.models.media.MediaType mediaType = new io.swagger.v3.oas.models.media.MediaType();
mediaType.setSchema(schema);
setContent(methodProduces, content, mediaType);
}
return content;
}
/**
* 检查类型是否为 void 类型
*
* @param returnType 返回类型
* @return 是否为 void 类型
*/
private boolean isVoid(Type returnType) {
if (Void.TYPE.equals(returnType) || Void.class.equals(returnType)) {
return true;
}
if (returnType instanceof ParameterizedType parameterizedType) {
Type[] types = parameterizedType.getActualTypeArguments();
if (isResponseTypeWrapper(ResolvableType.forType(returnType).getRawClass())) {
return isVoid(types[0]);
}
}
return false;
}
/**
* 计算响应 Schema
*
* @param components 组件信息
* @param returnType 返回类型
* @param jsonView JSON 视图
* @param annotations 方法注解
* @return Schema 对象
*/
private Schema<?> calculateSchema(Components components,
Type returnType,
JsonView jsonView,
Annotation[] annotations) {
if (isVoid(returnType) || SpringDocAnnotationsUtils.isAnnotationToIgnore(returnType)) {
return null;
}
return extractSchema(components, returnType, jsonView, annotations, propertyResolverUtils.getSpecVersion());
}
/**
* 设置响应内容的媒体类型
*
* @param methodProduces 方法支持的媒体类型数组
* @param content 响应内容
* @param mediaType 媒体类型对象
*/
private void setContent(String[] methodProduces,
Content content,
io.swagger.v3.oas.models.media.MediaType mediaType) {
Arrays.stream(methodProduces).forEach(mediaTypeStr -> content.addMediaType(mediaTypeStr, mediaType));
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.common.config.doc;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import top.continew.starter.auth.satoken.autoconfigure.SaTokenExtensionProperties;
import top.nextdoc4j.security.core.enhancer.NextDoc4jPathExcluder;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* NextDoc4j 自定义路径过滤
*
* @author echo
* @since 2025/12/18
*/
@Component
@RequiredArgsConstructor
public class NextDoc4jCustomPathFiltering implements NextDoc4jPathExcluder {
private final SaTokenExtensionProperties saTokenExtensionProperties;
@Override
public Set<String> getExcludedPaths() {
Set<String> paths = new HashSet<>();
this.addConfiguredExcludes(paths);
return paths;
}
/**
* 添加 Sa-Token 配置中的排除路径
*/
private void addConfiguredExcludes(Set<String> paths) {
if (saTokenExtensionProperties == null || saTokenExtensionProperties
.getSecurity() == null || saTokenExtensionProperties.getSecurity().getExcludes() == null) {
return;
}
paths.addAll(Arrays.asList(saTokenExtensionProperties.getSecurity().getExcludes()));
}
@Override
public int getOrder() {
// 在 RequestMappingHandlerMapping Excluder 之后执行
return 200;
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.common.config.doc;
import io.swagger.v3.oas.models.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import top.continew.admin.common.base.controller.BaseController;
import top.continew.admin.common.config.crud.CrudApiPermissionPrefixCache;
import top.continew.starter.extension.crud.annotation.CrudApi;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
import top.nextdoc4j.security.core.enhancer.NextDoc4jSecurityMetadataResolver;
import top.nextdoc4j.security.core.model.NextDoc4jSecurityMetadata;
import top.nextdoc4j.security.satoken.constant.NextDoc4jSaTokenConstant;
import java.lang.annotation.Annotation;
/**
* NextDoc4j 自定义权限码展示
*
* @author echo
* @since 2025/12/18
*/
@Component
@RequiredArgsConstructor
public class NextDoc4jCustomPermissionDisplay implements NextDoc4jSecurityMetadataResolver {
@Override
public void resolve(HandlerMethod handlerMethod, Operation operation, NextDoc4jSecurityMetadata metadata) {
// 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息
resolveCrudPermission(handlerMethod, metadata);
}
@Override
public boolean supports(HandlerMethod handlerMethod) {
// 检查类上是否有 CrudRequestMapping 注解且方法上有 CrudApi 注解
Class<?> targetClass = handlerMethod.getBeanType();
CrudRequestMapping crudRequestMapping = targetClass.getAnnotation(CrudRequestMapping.class);
CrudApi crudApi = handlerMethod.getMethodAnnotation(CrudApi.class);
return crudRequestMapping != null && crudApi != null;
}
@Override
public String getName() {
return "CustomPermissionDisplay";
}
/**
* 解析 CRUD 权限信息
*/
private void resolveCrudPermission(HandlerMethod handlerMethod, NextDoc4jSecurityMetadata metadata) {
Class<?> targetClass = handlerMethod.getBeanType();
CrudRequestMapping crudRequestMapping = targetClass.getAnnotation(CrudRequestMapping.class);
CrudApi crudApi = handlerMethod.getMethodAnnotation(CrudApi.class);
if (crudRequestMapping == null || crudApi == null) {
return;
}
// 检查方法上是否有 @SaIgnore 注解,如果有则跳过
if (hasSaIgnore(handlerMethod)) {
return;
}
// 检查方法上是否已经有 @SaCheckRole 或 @SaCheckPermission 注解
// 如果有,重写了方法,跳过 CRUD 自动生成,让插件处理
if (hasSaTokenAnnotation(handlerMethod)) {
return;
}
// 跳过字典类型的 API
if (Api.DICT.equals(crudApi.value()) || Api.TREE_DICT.equals(crudApi.value())) {
return;
}
// 获取权限前缀
String permissionPrefix = CrudApiPermissionPrefixCache.get(targetClass);
if (permissionPrefix == null || permissionPrefix.isEmpty()) {
return;
}
// 获取 API 名称并生成权限字符串
String apiName = BaseController.getApiName(crudApi.value());
String permission = "%s:%s".formatted(permissionPrefix, apiName.toLowerCase());
// 添加到权限列表中
metadata.addPermission(new String[] {permission}, "AND", "crud", new String[0]);
}
/**
* 检查方法或类上是否有 @SaIgnore 注解
*/
private boolean hasSaIgnore(HandlerMethod handlerMethod) {
// 检查方法上的 @SaIgnore 注解
Annotation methodAnnotation = handlerMethod.getMethodAnnotation(NextDoc4jSaTokenConstant.SA_IGNORE_CLASS);
if (methodAnnotation != null) {
return true;
}
// 检查类上的 @SaIgnore 注解
Annotation classAnnotation = handlerMethod.getBeanType()
.getAnnotation(NextDoc4jSaTokenConstant.SA_IGNORE_CLASS);
return classAnnotation != null;
}
/**
* 检查方法或类上是否有 @SaCheckRole 或 @SaCheckPermission 注解
* 如果有这些注解,说明开发者手动配置了权限,应该跳过 CRUD 自动生成
*/
private boolean hasSaTokenAnnotation(HandlerMethod handlerMethod) {
// 检查方法上的注解
Annotation methodPermission = handlerMethod
.getMethodAnnotation(NextDoc4jSaTokenConstant.SA_CHECK_PERMISSION_CLASS);
Annotation methodRole = handlerMethod.getMethodAnnotation(NextDoc4jSaTokenConstant.SA_CHECK_ROLE_CLASS);
if (methodPermission != null || methodRole != null) {
return true;
}
// 检查类上的注解
Class<?> beanType = handlerMethod.getBeanType();
Annotation classPermission = beanType.getAnnotation(NextDoc4jSaTokenConstant.SA_CHECK_PERMISSION_CLASS);
Annotation classRole = beanType.getAnnotation(NextDoc4jSaTokenConstant.SA_CHECK_ROLE_CLASS);
return classPermission != null || classRole != null;
}
@Override
public int getOrder() {
return 200;
}
}

View File

@@ -1,182 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.admin.common.config.doc;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaMode;
import org.springframework.web.method.HandlerMethod;
import top.continew.admin.common.base.controller.BaseController;
import top.continew.admin.common.config.crud.CrudApiPermissionPrefixCache;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.crud.annotation.CrudApi;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Operation 描述定制器 处理 sa-token 鉴权标识符
*
* @author echo
* @since 2024/6/14 11:18
*/
public class OperationDescriptionCustomizer {
/**
* 获取 sa-token 注解信息
*
* @param handlerMethod 处理程序方法
* @return 包含权限和角色校验信息的列表
*/
public List<String> getPermission(HandlerMethod handlerMethod) {
List<String> values = new ArrayList<>();
// 获取权限校验信息
String permissionInfo = getAnnotationInfo(handlerMethod, SaCheckPermission.class, "权限校验:");
if (!permissionInfo.isEmpty()) {
values.add(permissionInfo);
}
// 获取角色校验信息
String roleInfo = getAnnotationInfo(handlerMethod, SaCheckRole.class, "角色校验:");
if (!roleInfo.isEmpty()) {
values.add(roleInfo);
}
// 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息
String crudPermissionInfo = getCrudPermissionInfo(handlerMethod);
if (!crudPermissionInfo.isEmpty()) {
values.add(crudPermissionInfo);
}
return values;
}
/**
* 获取类和方法上指定注解的信息
*
* @param handlerMethod 处理程序方法
* @param annotationClass 注解类
* @param title 信息标题
* @param <A> 注解类型
* @return 拼接好的注解信息字符串
*/
@SuppressWarnings("unchecked")
private <A extends Annotation> String getAnnotationInfo(HandlerMethod handlerMethod,
Class<A> annotationClass,
String title) {
StringBuilder infoBuilder = new StringBuilder();
// 获取类上的注解
A classAnnotation = handlerMethod.getBeanType().getAnnotation(annotationClass);
if (classAnnotation != null) {
appendAnnotationInfo(infoBuilder, "类:", classAnnotation);
}
// 获取方法上的注解
A methodAnnotation = handlerMethod.getMethodAnnotation(annotationClass);
if (methodAnnotation != null) {
appendAnnotationInfo(infoBuilder, "方法:", methodAnnotation);
}
// 如果有注解信息,添加标题
if (!infoBuilder.isEmpty()) {
infoBuilder.insert(0, "<font style=\"color:red\" class=\"light-red\">" + title + "</font></br>");
}
return infoBuilder.toString();
}
/**
* 拼接注解信息到 StringBuilder 中
*
* @param builder 用于拼接信息的 StringBuilder
* @param prefix 前缀信息,如 "类:" 或 "方法:"
* @param annotation 注解对象
*/
private void appendAnnotationInfo(StringBuilder builder, String prefix, Annotation annotation) {
String[] values = null;
SaMode mode = null;
String type = "";
String[] orRole = new String[0];
if (annotation instanceof SaCheckPermission checkPermission) {
values = checkPermission.value();
mode = checkPermission.mode();
type = checkPermission.type();
orRole = checkPermission.orRole();
} else if (annotation instanceof SaCheckRole checkRole) {
values = checkRole.value();
mode = checkRole.mode();
type = checkRole.type();
}
if (values != null && mode != null) {
builder.append("<font style=\"color:red\" class=\"light-red\">");
builder.append(prefix);
if (!type.isEmpty()) {
builder.append("(类型:").append(type).append("");
}
builder.append(getAnnotationNote(values, mode));
if (orRole.length > 0) {
builder.append(" 或 角色校验(").append(getAnnotationNote(orRole, mode)).append("");
}
builder.append("</font></br>");
}
}
/**
* 根据注解的模式拼接注解值
*
* @param values 注解的值数组
* @param mode 注解的模式AND 或 OR
* @return 拼接好的注解值字符串
*/
private String getAnnotationNote(String[] values, SaMode mode) {
if (mode.equals(SaMode.AND)) {
return String.join("", values);
} else {
return String.join("", values);
}
}
/**
* 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息
*
* @param handlerMethod 处理程序方法
* @return 拼接好的权限信息字符串
* @see BaseController#preHandle(CrudApi, Object[], Method, Class)
*/
private String getCrudPermissionInfo(HandlerMethod handlerMethod) {
Class<?> targetClass = handlerMethod.getBeanType();
CrudRequestMapping crudRequestMapping = targetClass.getAnnotation(CrudRequestMapping.class);
CrudApi crudApi = handlerMethod.getMethodAnnotation(CrudApi.class);
if (crudRequestMapping == null || crudApi == null) {
return StringConstants.EMPTY;
}
if (Api.DICT.equals(crudApi.value()) || Api.TREE_DICT.equals(crudApi.value())) {
return StringConstants.EMPTY;
}
String permissionPrefix = CrudApiPermissionPrefixCache.get(targetClass);
String apiName = BaseController.getApiName(crudApi.value());
String permission = "%s:%s".formatted(permissionPrefix, apiName.toLowerCase());
return "<font style=\"color:red\" class=\"light-red\">CRUD 权限校验:</font></br><font style=\"color:red\" class=\"light-red\">方法:</font><font style=\"color:red\" class=\"light-red\">" + permission + "</font>";
}
}

View File

@@ -25,6 +25,7 @@ import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
@@ -39,8 +40,6 @@ import top.continew.starter.core.exception.BaseException;
import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.web.model.R;
import org.springframework.validation.BindException;
/**
* 全局异常处理器
*

View File

@@ -17,7 +17,7 @@
<properties>
<!-- SnailJob 服务端 -->
<snail-job.version>1.5.0</snail-job.version>
<snail-job.version>1.8.0</snail-job.version>
</properties>
<dependencies>

View File

@@ -1,6 +1,6 @@
-- liquibase formatted sql
-- changeset snail-job-server:1.5.0
-- changeset snail-job-server:1.8.0
-- 默认用户admin/admin
INSERT INTO `sj_system_user` (username, password, role)
VALUES ('admin', '465c194afb65670f38322df087f0a9bb225cc257e43eb4ac5a0c98ef5b3173ac', 2);

View File

@@ -1,6 +1,6 @@
-- liquibase formatted sql
-- changeset snail-job-server:1.5.0
-- changeset snail-job-server:1.8.0
SET NAMES utf8mb4;
CREATE TABLE `sj_namespace`

View File

@@ -1,6 +1,6 @@
-- liquibase formatted sql
-- changeset snail-job-server:1.5.0
-- changeset snail-job-server:1.8.0
-- 默认用户admin/admin
INSERT INTO sj_system_user (username, password, role)
VALUES ('admin', '465c194afb65670f38322df087f0a9bb225cc257e43eb4ac5a0c98ef5b3173ac', 2);

View File

@@ -1,6 +1,6 @@
-- liquibase formatted sql
-- changeset snail-job-server:1.5.0
-- changeset snail-job-server:1.8.0
-- sj_namespace
CREATE TABLE sj_namespace
(

View File

@@ -17,11 +17,11 @@
package top.continew.admin;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.alicp.jetcache.anno.config.EnableMethodCache;
import com.github.xiaoymin.knife4j.spring.configuration.Knife4jProperties;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -35,10 +35,12 @@ import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import top.continew.starter.core.ContiNewStarterVersion;
import top.continew.starter.core.autoconfigure.application.ApplicationProperties;
import top.continew.starter.extension.crud.annotation.EnableCrudApi;
import top.continew.starter.web.annotation.EnableGlobalResponse;
import top.continew.starter.web.model.R;
import top.nextdoc4j.core.configuration.NextDoc4jProperties;
/**
* 启动程序
@@ -61,7 +63,9 @@ public class ContiNewAdminApplication implements ApplicationRunner {
private final ServerProperties serverProperties;
public static void main(String[] args) {
SpringApplication.run(ContiNewAdminApplication.class, args);
SpringApplication application = new SpringApplication(ContiNewAdminApplication.class);
application.setDefaultProperties(MapUtil.of("continew-starter.version", ContiNewStarterVersion.getVersion()));
application.run(args);
}
@Hidden
@@ -79,13 +83,13 @@ public class ContiNewAdminApplication implements ApplicationRunner {
String baseUrl = URLUtil.normalize("%s:%s%s".formatted(hostAddress, port, contextPath));
log.info("--------------------------------------------------------");
log.info("{} server started successfully.", applicationProperties.getName());
log.info("ContiNew Starter: v{} (Spring Boot: v{})", SpringUtil
.getProperty("application.starter"), SpringBootVersion.getVersion());
log.info("ContiNew Starter: v{} (Spring Boot: v{})", ContiNewStarterVersion.getVersion(), SpringBootVersion
.getVersion());
log.info("当前版本: v{} (Profile: {})", applicationProperties.getVersion(), SpringUtil
.getProperty("spring.profiles.active"));
log.info("服务地址: {}", baseUrl);
Knife4jProperties knife4jProperties = SpringUtil.getBean(Knife4jProperties.class);
if (!knife4jProperties.isProduction()) {
NextDoc4jProperties docProperties = SpringUtil.getBean(NextDoc4jProperties.class);
if (!docProperties.isProduction()) {
log.info("接口文档: {}/doc.html", baseUrl);
}
log.info("吐槽广场: https://continew.top/docs/admin/issue-hub.html");
@@ -94,4 +98,4 @@ public class ContiNewAdminApplication implements ApplicationRunner {
log.info("ContiNew Admin: 持续迭代优化的,高质量多租户中后台管理系统框架");
log.info("--------------------------------------------------------");
}
}
}

View File

@@ -5,5 +5,5 @@
\____|\___/ |_| |_| \__||_||_| \_| \___| \_/\_/ /_/ \_\\__,_||_| |_| |_||_||_| |_|
:: ${application.name} :: v${application.version} (Profile: ${spring.profiles.active})
:: ContiNew Starter :: v${application.starter}
:: ContiNew Starter :: v${continew-starter.version}
:: Spring Boot :: v${spring-boot.version}

View File

@@ -213,29 +213,8 @@ continew-starter.messaging.websocket:
# 配置允许跨域的域名
allowed-origins: '*'
--- ### Sa-Token 扩展配置
sa-token.extension:
# 安全配置:排除(放行)路径配置
security.excludes:
- /error
# 静态资源
- /*.html
- /*/*.html
- /*/*.css
- /*/*.js
- /websocket/**
# 接口文档相关资源
- /favicon.ico
- /doc.html
- /webjars/**
- /swagger-ui/**
- /swagger-resources/**
- /*/api-docs/**
# 本地存储资源
- /file/**
--- ### Just Auth 配置
justauth:
continew-starter.justauth:
enabled: true
type:
WECHAT_OPEN:
@@ -253,6 +232,28 @@ justauth:
cache:
type: REDIS
--- ### Sa-Token 扩展配置
sa-token.extension:
# 安全配置:排除(放行)路径配置
security.excludes:
- /error
# 静态资源
- /*.html
- /*/*.html
- /*/*.css
- /*/*.js
- /websocket/**
# 接口文档相关资源
- /favicon.ico
- /doc.html
- /nextdoc/**
- /webjars/**
- /swagger-ui/**
- /swagger-resources/**
- /*/api-docs/**
# 本地存储资源
- /file/**
--- ### Snail Job 配置
snail-job:
enabled: false

View File

@@ -97,7 +97,7 @@ jetcache:
--- ### 接口文档配置
## 接口文档增强配置
knife4j:
nextdoc4j:
# 开启生产环境屏蔽
production: ${application.production}
@@ -222,22 +222,8 @@ continew-starter.messaging.websocket:
allowed-origins:
- ${application.url}
--- ### Sa-Token 扩展配置
sa-token.extension:
# 安全配置:排除(放行)路径配置
security.excludes:
- /error
# 静态资源
- /*.html
- /*/*.html
- /*/*.css
- /*/*.js
- /websocket/**
# 本地存储资源
- /file/**
--- ### Just Auth 配置
justauth:
continew-starter.justauth:
enabled: true
type:
WECHAT_OPEN:
@@ -255,6 +241,20 @@ justauth:
cache:
type: REDIS
--- ### Sa-Token 扩展配置
sa-token.extension:
# 安全配置:排除(放行)路径配置
security.excludes:
- /error
# 静态资源
- /*.html
- /*/*.html
- /*/*.css
- /*/*.js
- /websocket/**
# 本地存储资源
- /file/**
--- ### Snail Job 配置
snail-job:
# 客户端地址(默认自动获取本机 IP

View File

@@ -7,7 +7,6 @@ application:
description: 持续迭代优化的前后端分离中后台管理系统框架,开箱即用,持续提供舒适的开发体验。
# 版本
version: 4.2.0-SNAPSHOT
starter: 2.14.0
# 基本包
base-package: top.continew.admin
## 作者信息配置
@@ -123,15 +122,29 @@ springdoc:
name: ${sa-token.token-name}
scheme: ${sa-token.token-prefix}
## 接口文档增强配置
knife4j:
enable: true
setting:
# 是否显示默认的 footer默认 true显示
enable-footer: false
# 是否自定义 footer默认 false非自定义
enable-footer-custom: true
# 自定义 footer 内容,支持 Markdown 语法
footer-custom-content: 'Copyright © 2022-present [${application.contact.name}](${application.contact.url})&nbsp;⋅&nbsp;[${application.name}](${application.url}) v${application.version}'
nextdoc4j:
# 启用开关
enabled: true
# 扩展配置
extension:
# 扩展开关
enabled: true
# 品牌配置
brand:
# logo 展示
logo: classpath:favicon.ico
# 标题
title: ${application.name}
# 自定义 footer 内容
footer-text: 'Copyright © 2022-present [${application.contact.name}](${application.contact.url})&nbsp;⋅&nbsp;[${application.name}](${application.url}) v${application.version}'
# 插件配置
plugin:
# 枚举展示开关
enum:
enabled: true
# 认证展示和 sa-token 权限码展示开关
security:
enabled: true
--- ### 全局响应配置
continew-starter.web.response:

View File

@@ -19,7 +19,6 @@ package top.continew.admin.auth.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
@@ -27,10 +26,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import me.zhyd.oauth.AuthRequestBuilder;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import top.continew.admin.auth.model.req.LoginReq;
import top.continew.admin.auth.model.resp.LoginResp;
@@ -40,10 +38,12 @@ import top.continew.admin.auth.model.resp.UserInfoResp;
import top.continew.admin.auth.service.AuthService;
import top.continew.admin.common.context.UserContext;
import top.continew.admin.common.context.UserContextHolder;
import top.continew.admin.system.enums.SocialSourceEnum;
import top.continew.admin.system.model.resp.user.UserDetailResp;
import top.continew.admin.system.service.UserService;
import top.continew.starter.core.exception.BadRequestException;
import top.continew.starter.auth.justauth.AuthRequestFactory;
import top.continew.starter.log.annotation.Log;
import top.continew.starter.validation.constraints.EnumValue;
import java.util.List;
@@ -55,6 +55,7 @@ import java.util.List;
*/
@Tag(name = "认证 API")
@Log(module = "登录")
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
@@ -62,7 +63,7 @@ public class AuthController {
private final AuthService authService;
private final UserService userService;
private final JustAuthProperties authProperties;
private final AuthRequestFactory authRequestFactory;
@SaIgnore
@Operation(summary = "登录", description = "用户登录")
@@ -84,8 +85,8 @@ public class AuthController {
@Operation(summary = "三方账号登录授权", description = "三方账号登录授权")
@Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH)
@GetMapping("/{source}")
public SocialAuthAuthorizeResp authorize(@PathVariable String source) {
AuthRequest authRequest = this.getAuthRequest(source);
public SocialAuthAuthorizeResp authorize(@PathVariable @EnumValue(value = SocialSourceEnum.class, message = "第三方平台无效") String source) {
AuthRequest authRequest = authRequestFactory.getAuthRequest(source);
return SocialAuthAuthorizeResp.builder()
.authorizeUrl(authRequest.authorize(AuthStateUtils.createState()))
.build();
@@ -110,13 +111,4 @@ public class AuthController {
public List<RouteResp> listRoute() {
return authService.buildRouteTree(UserContextHolder.getUserId());
}
private AuthRequest getAuthRequest(String source) {
try {
AuthConfig authConfig = authProperties.getType().get(source.toUpperCase());
return AuthRequestBuilder.builder().source(source).authConfig(authConfig).build();
} catch (Exception e) {
throw new BadRequestException("暂不支持 [%s] 平台账号登录".formatted(source));
}
}
}

View File

@@ -24,11 +24,8 @@ import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import me.zhyd.oauth.AuthRequestBuilder;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
@@ -54,8 +51,8 @@ import top.continew.admin.system.service.DeptService;
import top.continew.admin.system.service.MessageService;
import top.continew.admin.system.service.UserRoleService;
import top.continew.admin.system.service.UserSocialService;
import top.continew.starter.auth.justauth.AuthRequestFactory;
import top.continew.starter.core.autoconfigure.application.ApplicationProperties;
import top.continew.starter.core.exception.BadRequestException;
import top.continew.starter.core.util.validation.ValidationUtils;
import java.time.LocalDateTime;
@@ -72,7 +69,7 @@ import java.util.Collections;
@RequiredArgsConstructor
public class SocialLoginHandler extends AbstractLoginHandler<SocialLoginReq> {
private final JustAuthProperties authProperties;
private final AuthRequestFactory authRequestFactory;
private final UserSocialService userSocialService;
private final UserRoleService userRoleService;
private final MessageService messageService;
@@ -83,7 +80,7 @@ public class SocialLoginHandler extends AbstractLoginHandler<SocialLoginReq> {
@Transactional
public LoginResp login(SocialLoginReq req, ClientResp client, HttpServletRequest request) {
// 获取第三方登录信息
AuthRequest authRequest = this.getAuthRequest(req.getSource());
AuthRequest authRequest = authRequestFactory.getAuthRequest(req.getSource());
AuthCallback callback = new AuthCallback();
callback.setCode(req.getCode());
callback.setState(req.getState());
@@ -153,21 +150,6 @@ public class SocialLoginHandler extends AbstractLoginHandler<SocialLoginReq> {
return AuthTypeEnum.SOCIAL;
}
/**
* 获取 AuthRequest
*
* @param source 平台名称
* @return AuthRequest
*/
private AuthRequest getAuthRequest(String source) {
try {
AuthConfig authConfig = authProperties.getType().get(source.toUpperCase());
return AuthRequestBuilder.builder().source(source).authConfig(authConfig).build();
} catch (Exception e) {
throw new BadRequestException("暂不支持 [%s] 平台账号登录".formatted(source));
}
}
/**
* 发送安全消息
*

View File

@@ -16,7 +16,6 @@
package top.continew.admin.system.controller;
import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
@@ -24,8 +23,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import me.zhyd.oauth.AuthRequestBuilder;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
@@ -46,10 +43,11 @@ import top.continew.admin.system.model.resp.AvatarResp;
import top.continew.admin.system.model.resp.user.UserSocialBindResp;
import top.continew.admin.system.service.UserService;
import top.continew.admin.system.service.UserSocialService;
import top.continew.starter.auth.justauth.AuthRequestFactory;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.exception.BadRequestException;
import top.continew.starter.core.util.CollUtils;
import top.continew.starter.core.util.validation.ValidationUtils;
import top.continew.starter.validation.constraints.EnumValue;
import java.io.IOException;
import java.util.List;
@@ -71,7 +69,7 @@ public class UserProfileController {
private static final String CAPTCHA_EXPIRED = "验证码已失效";
private final UserService userService;
private final UserSocialService userSocialService;
private final JustAuthProperties authProperties;
private final AuthRequestFactory authRequestFactory;
@Operation(summary = "修改头像", description = "用户修改个人头像")
@PatchMapping("/avatar")
@@ -135,8 +133,9 @@ public class UserProfileController {
@Operation(summary = "绑定三方账号", description = "绑定三方账号")
@Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH)
@PostMapping("/social/{source}")
public void bindSocial(@PathVariable String source, @RequestBody AuthCallback callback) {
AuthRequest authRequest = this.getAuthRequest(source);
public void bindSocial(@PathVariable @EnumValue(value = SocialSourceEnum.class, message = "第三方平台无效") String source,
@RequestBody AuthCallback callback) {
AuthRequest authRequest = authRequestFactory.getAuthRequest(source);
AuthResponse<AuthUser> response = authRequest.login(callback);
ValidationUtils.throwIf(!response.ok(), response.getMsg());
AuthUser authUser = response.getData();
@@ -149,13 +148,4 @@ public class UserProfileController {
public void unbindSocial(@PathVariable String source) {
userSocialService.deleteBySourceAndUserId(source, UserContextHolder.getUserId());
}
private AuthRequest getAuthRequest(String source) {
try {
AuthConfig authConfig = authProperties.getType().get(source.toUpperCase());
return AuthRequestBuilder.builder().source(source).authConfig(authConfig).build();
} catch (Exception e) {
throw new BadRequestException("暂不支持 [%s] 平台账号登录".formatted(source));
}
}
}

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>top.continew.starter</groupId>
<artifactId>continew-starter</artifactId>
<version>2.14.0</version>
<version>2.15.0</version>
</parent>
<groupId>top.continew.admin</groupId>