diff --git a/README.md b/README.md index f5fe1f06..e3cf6fc4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Release -ContiNew Starter +ContiNew Starter Spring Boot @@ -234,41 +234,41 @@ public class DeptController extends BaseControllerVue | 3.5.4 | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 | -| Arco Design | 2.57.0 | 字节跳动推出的前端 UI 框架,年轻化的色彩和组件设计。 | -| TypeScript | 5.0.4 | TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成。 | -| Vite | 5.1.5 | 下一代的前端工具链,为开发提供极速响应。 | -| [ContiNew Starter](https://github.com/continew-org/continew-starter) | 2.14.0 | ContiNew Starter 包含了一系列经过企业实践优化的依赖包(如 MyBatis-Plus、SaToken),可轻松集成到应用中,为开发人员减少手动引入依赖及配置的麻烦,为 Spring Boot Web 项目的灵活快速构建提供支持。 | -| Spring Boot | 3.3.12 | 简化 Spring 应用的初始搭建和开发过程,基于“约定优于配置”的理念,使开发人员不再需要定义样板化的配置。(Spring Boot 3.0 开始,要求 Java 17 作为最低版本) | -| Undertow | 2.3.18.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 | -| Sa-Token + JWT | 1.44.0 | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。 | -| MyBatis Plus | 3.5.12 | MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率。 | +| 名称 | 版本 | 简介 | +|:----------------------------------------------------------------------------------------------------------------------------------|:-------------| :----------------------------------------------------------- | +| Vue | 3.5.4 | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 | +| Arco Design | 2.57.0 | 字节跳动推出的前端 UI 框架,年轻化的色彩和组件设计。 | +| TypeScript | 5.0.4 | TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成。 | +| Vite | 5.1.5 | 下一代的前端工具链,为开发提供极速响应。 | +| [ContiNew Starter](https://github.com/continew-org/continew-starter) | 2.15.0 | ContiNew Starter 包含了一系列经过企业实践优化的依赖包(如 MyBatis-Plus、SaToken),可轻松集成到应用中,为开发人员减少手动引入依赖及配置的麻烦,为 Spring Boot Web 项目的灵活快速构建提供支持。 | +| Spring Boot | 3.3.12 | 简化 Spring 应用的初始搭建和开发过程,基于“约定优于配置”的理念,使开发人员不再需要定义样板化的配置。(Spring Boot 3.0 开始,要求 Java 17 作为最低版本) | +| Undertow | 2.3.18.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 | +| Sa-Token + JWT | 1.44.0 | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。 | +| MyBatis Plus | 3.5.12 | MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率。 | | dynamic-datasource-spring-boot-starter | 4.3.1 | 基于 Spring Boot 的快速集成多数据源的启动器。 | -| Hikari | 5.1.0 | JDBC 连接池,号称 “史上最快连接池”,SpringBoot 在 2.0 之后,采用的默认数据库连接池就是 Hikari。 | -| MySQL | 8.0.42 | 体积小、速度快、总体拥有成本低,是最流行的关系型数据库管理系统之一。 | -| mysql-connector-j | 8.3.0 | MySQL Java 驱动。 | -| P6Spy | 3.9.1 | SQL 性能分析组件。 | -| Liquibase | 4.27.0 | 用于管理数据库版本,跟踪、管理和应用数据库变化。 | -| [JetCache](https://github.com/alibaba/jetcache/blob/master/docs/CN/Readme.md) | 2.7.8 | 一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新,还提供了 Cache 接口用于手工缓存操作。 | -| Redisson | 3.49.0 | 不仅仅是一个 Redis Java 客户端,Redisson 充分的利用了 Redis 键值数据库提供的一系列优势,为使用者提供了一系列具有分布式特性的常用工具:分布式锁、限流器等。 | -| Redis | 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 协议的存储平台。 | -| SMS4J | 3.3.4 | 短信聚合框架,轻松集成多家短信服务,解决接入多个短信 SDK 的繁琐流程。 | -| Just Auth | 1.16.7 | 开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK,让登录变得 So easy! | -| Fast Excel | 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技术栈下的优雅响应处理组件,可以帮助开发者完成响应数据封装、异常处理、错误码填充等过程,提高开发效率,提高代码质量。 | -| Knife4j | 4.5.0 | 前身是 swagger-bootstrap-ui,集 Swagger2 和 OpenAPI3 为一体的增强解决方案。 | -| [OpenFeign](https://springdoc.cn/spring-cloud-openfeign/) | 13.5 | Spring Cloud OpenFeign 是一种基于 Spring Cloud 的声明式 REST 客户端,它简化了与 HTTP 服务交互的过程。 | -| Hutool | 5.8.38 | 小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。 | -| Lombok | 1.18.36 | 在 Java 开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码,让编写的类更加简洁。 | +| Hikari | 5.1.0 | JDBC 连接池,号称 “史上最快连接池”,SpringBoot 在 2.0 之后,采用的默认数据库连接池就是 Hikari。 | +| MySQL | 8.0.42 | 体积小、速度快、总体拥有成本低,是最流行的关系型数据库管理系统之一。 | +| mysql-connector-j | 8.3.0 | MySQL Java 驱动。 | +| P6Spy | 3.9.1 | SQL 性能分析组件。 | +| Liquibase | 4.27.0 | 用于管理数据库版本,跟踪、管理和应用数据库变化。 | +| [JetCache](https://github.com/alibaba/jetcache/blob/master/docs/CN/Readme.md) | 2.7.8 | 一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新,还提供了 Cache 接口用于手工缓存操作。 | +| Redisson | 3.49.0 | 不仅仅是一个 Redis Java 客户端,Redisson 充分的利用了 Redis 键值数据库提供的一系列优势,为使用者提供了一系列具有分布式特性的常用工具:分布式锁、限流器等。 | +| Redis | 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 协议的存储平台。 | +| SMS4J | 3.3.4 | 短信聚合框架,轻松集成多家短信服务,解决接入多个短信 SDK 的繁琐流程。 | +| Just Auth | 1.16.7 | 开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK,让登录变得 So easy! | +| Fast Excel | 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技术栈下的优雅响应处理组件,可以帮助开发者完成响应数据封装、异常处理、错误码填充等过程,提高开发效率,提高代码质量。 | +| NextDoc4j | 1.1.5 | 现代化 API 文档 UI 工具,全面替代 Swagger UI。 | +| [OpenFeign](https://springdoc.cn/spring-cloud-openfeign/) | 13.5 | Spring Cloud OpenFeign 是一种基于 Spring Cloud 的声明式 REST 客户端,它简化了与 HTTP 服务交互的过程。 | +| Hutool | 5.8.38 | 小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。 | +| Lombok | 1.18.36 | 在 Java 开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码,让编写的类更加简洁。 | ## 快速开始 @@ -550,7 +550,7 @@ ContiNew 系列项目采用清晰的分支策略,确保开发与维护有序 ### 特别鸣谢 - 感谢 JetBrains 提供的 非商业开源软件开发授权 -- 感谢 MyBatis PlusSa-TokenJetCacheCrane4jKnife4jHutool 等开源组件作者为国内开源世界作出的贡献 +- 感谢 MyBatis PlusSa-TokenJetCacheCrane4jNextDoc4jHutool 等开源组件作者为国内开源世界作出的贡献 - 感谢项目使用或未使用到的每一款开源组件,致敬各位开源先驱 :fire: ## License diff --git a/continew-common/pom.xml b/continew-common/pom.xml index 2a27cc71..d6564727 100644 --- a/continew-common/pom.xml +++ b/continew-common/pom.xml @@ -101,6 +101,11 @@ cn.dev33 sa-token-sign + + + top.nextdoc4j + nextdoc4j-plugin-security-satoken + diff --git a/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalAuthenticationCustomizer.java b/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalAuthenticationCustomizer.java deleted file mode 100644 index 0b0885eb..00000000 --- a/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalAuthenticationCustomizer.java +++ /dev/null @@ -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 excludedPaths = collectExcludedPaths(); - - // 遍历所有路径,为需要鉴权的路径添加安全认证配置 - openApi.getPaths().forEach((path, pathItem) -> { - if (isPathExcluded(path, excludedPaths)) { - // 路径在排除列表中,跳过处理 - return; - } - // 为路径添加安全认证参数 - addAuthenticationParameters(pathItem); - }); - } - - /** - * 收集所有需要排除的路径 - * - * @return 排除路径集合 - */ - private Set collectExcludedPaths() { - Set 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 securitySchemes = components.getSecuritySchemes(); - List 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 resolveSaIgnorePaths() { - // 获取所有标注 @RestController 的 Bean - Map controllers = context.getBeansWithAnnotation(RestController.class); - Set ignoredPaths = new HashSet<>(); - - // 遍历所有控制器,解析 @SaIgnore 注解路径 - controllers.values().forEach(controllerBean -> { - Class controllerClass = AopUtils.getTargetClass(controllerBean); - List 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 getClassPaths(Class controller) { - List 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 getMethodPaths(Method method) { - List 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 combinePaths(List classPaths, List 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 excludedPaths) { - return excludedPaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, path)); - } -} diff --git a/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalDescriptionCustomizer.java b/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalDescriptionCustomizer.java deleted file mode 100644 index dc4b9c51..00000000 --- a/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalDescriptionCustomizer.java +++ /dev/null @@ -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 noteList = new ArrayList<>(new OperationDescriptionCustomizer().getPermission(handlerMethod)); - - // 如果注解数据列表为空,直接返回原 operation - if (noteList.isEmpty()) { - return operation; - } - // 拼接注解数据为字符串 - String noteStr = StrUtil.join("
", noteList); - // 获取原描述 - String originalDescription = operation.getDescription(); - // 根据原描述是否为空,更新描述 - String newDescription = StrUtil.isNotEmpty(originalDescription) - ? originalDescription + "
" + noteStr - : noteStr; - - // 设置新描述 - operation.setDescription(newDescription); - return operation; - } -} diff --git a/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalSpringDocResponseOperationCustomizer.java b/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalSpringDocResponseOperationCustomizer.java new file mode 100644 index 00000000..f7828341 --- /dev/null +++ b/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalSpringDocResponseOperationCustomizer.java @@ -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; + +/** + * 全局响应操作自定义器 + *

+ * 自定义 OpenAPI 文档中的响应结构,将原始返回类型包装为统一的响应格式 + *

+ * + * @author echo + * @since 2025/07/08 09:34 + */ +@Component +public class GlobalSpringDocResponseOperationCustomizer extends GenericResponseService { + + private final GlobalResponseProperties globalResponseProperties; + private final Class 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)); + } +} \ No newline at end of file diff --git a/continew-common/src/main/java/top/continew/admin/common/config/doc/NextDoc4jCustomPathFiltering.java b/continew-common/src/main/java/top/continew/admin/common/config/doc/NextDoc4jCustomPathFiltering.java new file mode 100644 index 00000000..384eaa17 --- /dev/null +++ b/continew-common/src/main/java/top/continew/admin/common/config/doc/NextDoc4jCustomPathFiltering.java @@ -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 getExcludedPaths() { + Set paths = new HashSet<>(); + this.addConfiguredExcludes(paths); + return paths; + } + + /** + * 添加 Sa-Token 配置中的排除路径 + */ + private void addConfiguredExcludes(Set 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; + } +} diff --git a/continew-common/src/main/java/top/continew/admin/common/config/doc/NextDoc4jCustomPermissionDisplay.java b/continew-common/src/main/java/top/continew/admin/common/config/doc/NextDoc4jCustomPermissionDisplay.java new file mode 100644 index 00000000..e0f3e469 --- /dev/null +++ b/continew-common/src/main/java/top/continew/admin/common/config/doc/NextDoc4jCustomPermissionDisplay.java @@ -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; + } +} diff --git a/continew-common/src/main/java/top/continew/admin/common/config/doc/OperationDescriptionCustomizer.java b/continew-common/src/main/java/top/continew/admin/common/config/doc/OperationDescriptionCustomizer.java deleted file mode 100644 index 6b6d65dc..00000000 --- a/continew-common/src/main/java/top/continew/admin/common/config/doc/OperationDescriptionCustomizer.java +++ /dev/null @@ -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 getPermission(HandlerMethod handlerMethod) { - List 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 注解类型 - * @return 拼接好的注解信息字符串 - */ - @SuppressWarnings("unchecked") - private String getAnnotationInfo(HandlerMethod handlerMethod, - Class 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, "" + title + "
"); - } - - 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(""); - 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("
"); - } - } - - /** - * 根据注解的模式拼接注解值 - * - * @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 "CRUD 权限校验:
方法:" + permission + ""; - } -} diff --git a/continew-common/src/main/java/top/continew/admin/common/config/exception/GlobalExceptionHandler.java b/continew-common/src/main/java/top/continew/admin/common/config/exception/GlobalExceptionHandler.java index 2990b5eb..09ef861d 100644 --- a/continew-common/src/main/java/top/continew/admin/common/config/exception/GlobalExceptionHandler.java +++ b/continew-common/src/main/java/top/continew/admin/common/config/exception/GlobalExceptionHandler.java @@ -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; - /** * 全局异常处理器 * diff --git a/continew-extension/continew-extension-schedule-server/pom.xml b/continew-extension/continew-extension-schedule-server/pom.xml index dd6f7c02..8cabe839 100644 --- a/continew-extension/continew-extension-schedule-server/pom.xml +++ b/continew-extension/continew-extension-schedule-server/pom.xml @@ -17,7 +17,7 @@ - 1.5.0 + 1.8.0 diff --git a/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/mysql/snail-job_data.sql b/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/mysql/snail-job_data.sql index 3bd1ea02..daaff14a 100644 --- a/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/mysql/snail-job_data.sql +++ b/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/mysql/snail-job_data.sql @@ -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); diff --git a/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/mysql/snail-job_table.sql b/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/mysql/snail-job_table.sql index 75e6be81..7e0f9338 100644 --- a/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/mysql/snail-job_table.sql +++ b/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/mysql/snail-job_table.sql @@ -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` diff --git a/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/postgresql/snail-job_data.sql b/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/postgresql/snail-job_data.sql index 9aa70614..93725935 100644 --- a/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/postgresql/snail-job_data.sql +++ b/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/postgresql/snail-job_data.sql @@ -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); diff --git a/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/postgresql/snail-job_table.sql b/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/postgresql/snail-job_table.sql index dbac74d3..ec1887ca 100644 --- a/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/postgresql/snail-job_table.sql +++ b/continew-extension/continew-extension-schedule-server/src/main/resources/db/changelog/postgresql/snail-job_table.sql @@ -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 ( diff --git a/continew-server/src/main/java/top/continew/admin/ContiNewAdminApplication.java b/continew-server/src/main/java/top/continew/admin/ContiNewAdminApplication.java index 1f99f6e8..c8f41df7 100644 --- a/continew-server/src/main/java/top/continew/admin/ContiNewAdminApplication.java +++ b/continew-server/src/main/java/top/continew/admin/ContiNewAdminApplication.java @@ -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("--------------------------------------------------------"); } -} +} \ No newline at end of file diff --git a/continew-server/src/main/resources/banner.txt b/continew-server/src/main/resources/banner.txt index 6f7af400..56fab2d2 100644 --- a/continew-server/src/main/resources/banner.txt +++ b/continew-server/src/main/resources/banner.txt @@ -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} diff --git a/continew-server/src/main/resources/config/application-dev.yml b/continew-server/src/main/resources/config/application-dev.yml index 3e3b84e3..3a268aa7 100644 --- a/continew-server/src/main/resources/config/application-dev.yml +++ b/continew-server/src/main/resources/config/application-dev.yml @@ -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 diff --git a/continew-server/src/main/resources/config/application-prod.yml b/continew-server/src/main/resources/config/application-prod.yml index 94f0fd7c..0abece84 100644 --- a/continew-server/src/main/resources/config/application-prod.yml +++ b/continew-server/src/main/resources/config/application-prod.yml @@ -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) diff --git a/continew-server/src/main/resources/config/application.yml b/continew-server/src/main/resources/config/application.yml index b4c2518c..677867ce 100644 --- a/continew-server/src/main/resources/config/application.yml +++ b/continew-server/src/main/resources/config/application.yml @@ -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}) ⋅ [${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}) ⋅ [${application.name}](${application.url}) v${application.version}' + # 插件配置 + plugin: + # 枚举展示开关 + enum: + enabled: true + # 认证展示和 sa-token 权限码展示开关 + security: + enabled: true --- ### 全局响应配置 continew-starter.web.response: diff --git a/continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java b/continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java index 33b624f2..ee268a86 100644 --- a/continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java +++ b/continew-system/src/main/java/top/continew/admin/auth/controller/AuthController.java @@ -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 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)); - } - } } diff --git a/continew-system/src/main/java/top/continew/admin/auth/handler/SocialLoginHandler.java b/continew-system/src/main/java/top/continew/admin/auth/handler/SocialLoginHandler.java index dfb2f935..bc0420ff 100644 --- a/continew-system/src/main/java/top/continew/admin/auth/handler/SocialLoginHandler.java +++ b/continew-system/src/main/java/top/continew/admin/auth/handler/SocialLoginHandler.java @@ -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 { - 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 { @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 { 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)); - } - } - /** * 发送安全消息 * diff --git a/continew-system/src/main/java/top/continew/admin/system/controller/UserProfileController.java b/continew-system/src/main/java/top/continew/admin/system/controller/UserProfileController.java index b30d03e4..98a38668 100644 --- a/continew-system/src/main/java/top/continew/admin/system/controller/UserProfileController.java +++ b/continew-system/src/main/java/top/continew/admin/system/controller/UserProfileController.java @@ -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 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)); - } - } } diff --git a/pom.xml b/pom.xml index 466ee6e6..6e721cac 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ top.continew.starter continew-starter - 2.14.0 + 2.15.0 top.continew.admin