diff --git a/README.md b/README.md index 7b2080bab..03bb45912 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ > > 😜 给项目点点 Star 吧,这对我们真的很重要! +![架构图](https://static.iocoder.cn/ruoyi-vue-pro-architecture.png) + * 管理后台的 Vue3 版本采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) ,Vue2 版本采用 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) * 管理后台的移动端采用 [uni-app](https://github.com/dcloudio/uni-app) 方案,一份代码多终端适配,同时支持 APP、小程序、H5! * 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson @@ -42,16 +44,19 @@ | `yudao-cloud` | Spring Cloud 微服务 | **[Gitee](https://gitee.com/zhijiantianya/yudao-cloud)**     [Github](https://github.com/YunaiV/yudao-cloud) | | `Spring-Boot-Labs` | Spring Boot & Cloud 入门 | **[Gitee](https://gitee.com/zhijiantianya/SpringBoot-Labs)**     [Github](https://github.com/YunaiV/SpringBoot-Labs) | - ## 🐼 内置功能 -分成多种内置功能: +系统内置多种多种业务功能,可以用于快速你的业务系统: + +![功能分层](https://static.iocoder.cn/ruoyi-vue-pro-biz.png) + * 系统功能 * 基础设施 * 工作流程 * 支付系统 -* 商城系统 +* 会员中心 * 数据报表 +* 商城系统 > 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。 > @@ -169,29 +174,29 @@ ps:核心功能已经实现,正在对接微信小程序中... ### 后端 -| 框架 | 说明 | 版本 | 学习指南 | -|---------------------------------------------------------------------------------------------|-----------------------|---------|----------------------------------------------------------------| -| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.6.10 | [文档](https://github.com/YunaiV/SpringBoot-Labs) | -| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 | | -| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.11 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | -| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.2 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) | -| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | -| [Redis](https://redis.io/) | key-value 数据库 | 5.0 | | -| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.17.4 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) | -| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.20 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) | -| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.6.5 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) | -| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.3 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) | -| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.7.2 | [文档](https://doc.iocoder.cn/bpm/) | -| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) | -| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) | -| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) | -| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) | -| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.6.7 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) | -| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.3 | | -| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) | -| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) | -| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - | -| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.0.0 | - | +| 框架 | 说明 | 版本 | 学习指南 | +|---------------------------------------------------------------------------------------------|-----------------------|------------------|----------------------------------------------------------------| +| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.6.12 | [文档](https://github.com/YunaiV/SpringBoot-Labs) | +| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | | +| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.13-SNSAPSHOT | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | +| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.2 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) | +| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.5.2 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | +| [Redis](https://redis.io/) | key-value 数据库 | 5.0 | | +| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.17.7 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) | +| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.20 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) | +| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.6.5 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) | +| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.3 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) | +| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.7.2 | [文档](https://doc.iocoder.cn/bpm/) | +| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) | +| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) | +| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) | +| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) | +| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.6.9 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) | +| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.3 | | +| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.5.2.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) | +| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.24 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) | +| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - | +| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.8.0 | - | ### [管理后台 Vue2 前端](./yudao-ui-admin) @@ -204,14 +209,14 @@ ps:核心功能已经实现,正在对接微信小程序中... | 框架 | 说明 | 版本 | |----------------------------------------------------------------------|-----------------|--------| -| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.37 | -| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.0.4 | -| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.12 | -| [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.7.4 | -| [pinia](https://pinia.vuejs.org/) | vuex5 | 2.0.17 | +| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.41 | +| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.1.8 | +| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.18 | +| [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.8.4 | +| [pinia](https://pinia.vuejs.org/) | vuex5 | 2.0.23 | | [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 | | [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 | -| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.1 | +| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.0.0 | ### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp) diff --git a/pom.xml b/pom.xml index 63aa70ecb..c08cea284 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ yudao-framework yudao-server - + yudao-module-member yudao-module-bpm yudao-module-system @@ -20,6 +20,8 @@ yudao-module-pay yudao-module-mall yudao-module-visualization + + yudao-example ${project.artifactId} @@ -35,8 +37,8 @@ 3.0.0-M5 3.8.0 - 1.18.20 - 1.4.1.Final + 1.18.24 + 1.5.2.Final UTF-8 diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index bd90ad8e7..c692fafa5 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -16,58 +16,58 @@ 1.6.4-snapshot - 2.6.10 + 2.6.12 3.0.3 - 1.6.6 + 1.6.7 2.5 - 1.2.11 + 1.2.13-SNSAPSHOT 3.5.2 3.5.2 - 3.5.0 - 3.17.4 + 3.5.2 + 3.17.7 - 1.9.2 + 2.0.1 - 2.2.0 + 2.2.2 1.7.1 - 8.7.0 - 2.6.7 - 0.31.0 + 8.12.0 + 2.6.9 + 0.33.0 - 7.2.6.RELEASE - 0.1.16 - 4.0.0 + 7.2.9.RELEASE + 1.0.4 + 4.8.0 6.7.2 3.0.4 - 1.18.20 - 1.4.1.Final - 5.8.5 + 1.18.24 + 1.5.2.Final + 5.8.8 3.1.1 2.3 1.0.5 - 1.2.83 - 30.1.1-jre + 1.2.83 + 31.1-jre 5.1.0 - 2.12.2 + 2.14.0 3.8.0 0.1.55 - 2.4.1 + 2.5.0 1.3.0 4.1.82.Final 8.2.2 - 4.6.0 + 4.6.2 2.2.1 3.1.561 1.2.7 1.4.0 - 1.5.2 - 2.12.0 + 1.5.3 + 2.12.2 diff --git a/yudao-example/pom.xml b/yudao-example/pom.xml new file mode 100644 index 000000000..82b85a5f9 --- /dev/null +++ b/yudao-example/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + + + cn.iocoder.boot + yudao-example + 1.0.0-snapshot + pom + + yudao-sso-demo-by-code + yudao-sso-demo-by-password + + + ${project.artifactId} + 提供各种示例,例如说:SSO 单点登录 + https://github.com/YunaiV/ruoyi-vue-pro + + diff --git a/yudao-example/yudao-sso-demo-by-code/pom.xml b/yudao-example/yudao-sso-demo-by-code/pom.xml new file mode 100644 index 000000000..ffb4dc610 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + + cn.iocoder.boot + yudao-sso-demo-by-code + 1.0.0-snapshot + jar + + ${project.artifactId} + 基于授权码模式,如何实现 SSO 单点登录? + https://github.com/YunaiV/ruoyi-vue-pro + + + + 8 + 8 + UTF-8 + + 2.6.10 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + cn.hutool + hutool-all + 5.8.8 + + + + org.projectlombok + lombok + true + + + + diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java new file mode 100644 index 000000000..f6b160745 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.ssodemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SSODemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SSODemoApplication.class, args); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java new file mode 100644 index 000000000..d2e160f6f --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java @@ -0,0 +1,157 @@ +package cn.iocoder.yudao.ssodemo.client; + +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.Base64Utils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; + +/** + * OAuth 2.0 客户端 + * + * 对应调用 OAuth2OpenController 接口 + */ +@Component +public class OAuth2Client { + + private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2"; + + /** + * 租户编号 + * + * 默认使用 1;如果使用别的租户,可以调整 + */ + public static final Long TENANT_ID = 1L; + + private static final String CLIENT_ID = "yudao-sso-demo-by-code"; + private static final String CLIENT_SECRET = "test"; + + +// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * 使用 code 授权码,获得访问令牌 + * + * @param code 授权码 + * @param redirectUri 重定向 URI + * @return 访问令牌 + */ + public CommonResult postAccessToken(String code, String redirectUri) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "authorization_code"); + body.add("code", code); + body.add("redirect_uri", redirectUri); +// body.add("state", ""); // 选填;填了会校验 + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 校验访问令牌,并返回它的基本信息 + * + * @param token 访问令牌 + * @return 访问令牌的基本信息 + */ + public CommonResult checkToken(String token) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/check-token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌 + */ + public CommonResult refreshToken(String refreshToken) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "refresh_token"); + body.add("refresh_token", refreshToken); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 删除访问令牌 + * + * @param token 访问令牌 + * @return 成功 + */ + public CommonResult revokeToken(String token) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.DELETE, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + private static void addClientHeader(HttpHeaders headers) { + // client 拼接,需要 BASE64 编码 + String client = CLIENT_ID + ":" + CLIENT_SECRET; + client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8)); + headers.add("Authorization", "Basic " + client); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java new file mode 100644 index 000000000..666bd3ee4 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.ssodemo.client; + +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO; +import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO; +import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +/** + * 用户 User 信息的客户端 + * + * 对应调用 OAuth2UserController 接口 + */ +@Component +public class UserClient { + + private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user"; + + // @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 + private final RestTemplate restTemplate = new RestTemplate(); + + public CommonResult getUser() { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); + addTokenHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/get", + HttpMethod.GET, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + public CommonResult updateUser(UserUpdateReqDTO updateReqDTO) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); + addTokenHeader(headers); + // 1.2 构建请求参数 + // 使用 updateReqDTO 即可 + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/update", + HttpMethod.PUT, + new HttpEntity<>(updateReqDTO, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + + private static void addTokenHeader(HttpHeaders headers) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + Assert.notNull(loginUser, "登录用户不能为空"); + headers.add("Authorization", "Bearer " + loginUser.getAccessToken()); + } +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java new file mode 100644 index 000000000..548fe51e4 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.ssodemo.client.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + */ + private String msg; + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java new file mode 100644 index 000000000..6a5369a20 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.ssodemo.client.dto.oauth2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 访问令牌 Response DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2AccessTokenRespDTO { + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 刷新令牌 + */ + @JsonProperty("refresh_token") + private String refreshToken; + + /** + * 令牌类型 + */ + @JsonProperty("token_type") + private String tokenType; + + /** + * 过期时间;单位:秒 + */ + @JsonProperty("expires_in") + private Long expiresIn; + + /** + * 授权范围;如果多个授权范围,使用空格分隔 + */ + private String scope; + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java new file mode 100644 index 000000000..862bcf04b --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.ssodemo.client.dto.oauth2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 校验令牌 Response DTO + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2CheckTokenRespDTO { + + /** + * 用户编号 + */ + @JsonProperty("user_id") + private Long userId; + /** + * 用户类型 + */ + @JsonProperty("user_type") + private Integer userType; + /** + * 租户编号 + */ + @JsonProperty("tenant_id") + private Long tenantId; + + /** + * 客户端编号 + */ + @JsonProperty("client_id") + private String clientId; + /** + * 授权范围 + */ + private List scopes; + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 过期时间 + * + * 时间戳 / 1000,即单位:秒 + */ + private Long exp; + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java new file mode 100644 index 000000000..e81bea9eb --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.ssodemo.client.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 获得用户基本信息 Response dto + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserInfoRespDTO { + + /** + * 用户编号 + */ + private Long id; + + /** + * 用户账号 + */ + private String username; + + /** + * 用户昵称 + */ + private String nickname; + + /** + * 用户邮箱 + */ + private String email; + /** + * 手机号码 + */ + private String mobile; + + /** + * 用户性别 + */ + private Integer sex; + + /** + * 用户头像 + */ + private String avatar; + + /** + * 所在部门 + */ + private Dept dept; + + /** + * 所属岗位数组 + */ + private List posts; + + /** + * 部门 + */ + @Data + public static class Dept { + + /** + * 部门编号 + */ + private Long id; + + /** + * 部门名称 + */ + private String name; + + } + + /** + * 岗位 + */ + @Data + public static class Post { + + /** + * 岗位编号 + */ + private Long id; + + /** + * 岗位名称 + */ + private String name; + + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java new file mode 100644 index 000000000..e711d7311 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.ssodemo.client.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 更新用户基本信息 Request DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserUpdateReqDTO { + + /** + * 用户昵称 + */ + private String nickname; + + /** + * 用户邮箱 + */ + private String email; + + /** + * 手机号码 + */ + private String mobile; + + /** + * 用户性别 + */ + private Integer sex; + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java new file mode 100644 index 000000000..fe64a1547 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.ssodemo.controller; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.ssodemo.client.OAuth2Client; +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Resource + private OAuth2Client oauth2Client; + + /** + * 使用 code 访问令牌,获得访问令牌 + * + * @param code 授权码 + * @param redirectUri 重定向 URI + * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @PostMapping("/login-by-code") + public CommonResult loginByCode(@RequestParam("code") String code, + @RequestParam("redirectUri") String redirectUri) { + return oauth2Client.postAccessToken(code, redirectUri); + } + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @PostMapping("/refresh-token") + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return oauth2Client.refreshToken(refreshToken); + } + + /** + * 退出登录 + * + * @param request 请求 + * @return 成功 + */ + @PostMapping("/logout") + public CommonResult logout(HttpServletRequest request) { + String token = SecurityUtils.obtainAuthorization(request, "Authorization"); + if (StrUtil.isNotBlank(token)) { + return oauth2Client.revokeToken(token); + } + // 返回成功 + return new CommonResult<>(); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java new file mode 100644 index 000000000..819c752bc --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.ssodemo.controller; + +import cn.iocoder.yudao.ssodemo.client.UserClient; +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO; +import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("/user") +public class UserController { + + @Resource + private UserClient userClient; + + /** + * 获得当前登录用户的基本信息 + * + * @return 用户信息;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @GetMapping("/get") + public CommonResult getUser() { + return userClient.getUser(); + } + + /** + * 更新当前登录用户的昵称 + * + * @param nickname 昵称 + * @return 成功 + */ + @PutMapping("/update") + public CommonResult updateUser(@RequestParam("nickname") String nickname) { + UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null); + return userClient.updateUser(updateReqDTO); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java new file mode 100644 index 000000000..1d19fcfdb --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.ssodemo.framework.config; + +import cn.iocoder.yudao.ssodemo.framework.core.filter.TokenAuthenticationFilter; +import cn.iocoder.yudao.ssodemo.framework.core.handler.AccessDeniedHandlerImpl; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.annotation.Resource; + +@Configuration +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Resource + private TokenAuthenticationFilter tokenAuthenticationFilter; + + @Resource + private AccessDeniedHandlerImpl accessDeniedHandler; + @Resource + private AuthenticationEntryPoint authenticationEntryPoint; + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + // 设置 URL 安全权限 + httpSecurity.csrf().disable() // 禁用 CSRF 保护 + .authorizeRequests() + // 1. 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() + // 2. 登录相关的接口,可匿名访问 + .antMatchers("/auth/login-by-code").permitAll() + .antMatchers("/auth/refresh-token").permitAll() + .antMatchers("/auth/logout").permitAll() + // last. 兜底规则,必须认证 + .and().authorizeRequests() + .anyRequest().authenticated(); + + // 设置处理器 + httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler) + .authenticationEntryPoint(authenticationEntryPoint); + + // 添加 Token Filter + httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java new file mode 100644 index 000000000..44f3edf54 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.ssodemo.framework.core; + +import lombok.Data; + +import java.util.List; + +/** + * 登录用户信息 + * + * @author 芋道源码 + */ +@Data +public class LoginUser { + + /** + * 用户编号 + */ + private Long id; + /** + * 用户类型 + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围 + */ + private List scopes; + + /** + * 访问令牌 + */ + private String accessToken; + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 000000000..70bb40eaa --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.ssodemo.framework.core.filter; + +import cn.iocoder.yudao.ssodemo.client.OAuth2Client; +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; +import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.Resource; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Token 过滤器,验证 token 的有效性 + * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 + * + * @author 芋道源码 + */ +@Component +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + @Resource + private OAuth2Client oauth2Client; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + // 1. 获得访问令牌 + String token = SecurityUtils.obtainAuthorization(request, "Authorization"); + if (StringUtils.hasText(token)) { + // 2. 基于 token 构建登录用户 + LoginUser loginUser = buildLoginUserByToken(token); + // 3. 设置当前用户 + if (loginUser != null) { + SecurityUtils.setLoginUser(loginUser, request); + } + } + + // 继续过滤链 + filterChain.doFilter(request, response); + } + + private LoginUser buildLoginUserByToken(String token) { + try { + CommonResult accessTokenResult = oauth2Client.checkToken(token); + OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); + if (accessToken == null) { + return null; + } + // 构建登录用户 + return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) + .setAccessToken(accessToken.getAccessToken()); + } catch (Exception exception) { + // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 + return null; + } + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 000000000..dccc35eb1 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.ssodemo.framework.core.handler; + +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; +import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 + * + * @author 芋道源码 + */ +@Component +@SuppressWarnings("JavadocReference") +@Slf4j +public class AccessDeniedHandlerImpl implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) + throws IOException, ServletException { + // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 + log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), + SecurityUtils.getLoginUserId(), e); + // 返回 403 + CommonResult result = new CommonResult<>(); + result.setCode(HttpStatus.FORBIDDEN.value()); + result.setMsg("没有该操作权限"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 000000000..241ff56f5 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.ssodemo.framework.core.handler; + +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 + */ +@Component +@Slf4j +@SuppressWarnings("JavadocReference") // 忽略文档引用报错 +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); + // 返回 401 + CommonResult result = new CommonResult<>(); + result.setCode(HttpStatus.UNAUTHORIZED.value()); + result.setMsg("账号未登录"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java new file mode 100644 index 000000000..0352a4785 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.ssodemo.framework.core.util; + +import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; + +/** + * 安全服务工具类 + * + * @author 芋道源码 + */ +public class SecurityUtils { + + public static final String AUTHORIZATION_BEARER = "Bearer"; + + private SecurityUtils() {} + + /** + * 从请求中,获得认证 Token + * + * @param request 请求 + * @param header 认证 Token 对应的 Header 名字 + * @return 认证 Token + */ + public static String obtainAuthorization(HttpServletRequest request, String header) { + String authorization = request.getHeader(header); + if (!StringUtils.hasText(authorization)) { + return null; + } + int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); + if (index == -1) { // 未找到 + return null; + } + return authorization.substring(index + 7).trim(); + } + + /** + * 获得当前认证信息 + * + * @return 认证信息 + */ + public static Authentication getAuthentication() { + SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return null; + } + return context.getAuthentication(); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + @Nullable + public static LoginUser getLoginUser() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; + } + + /** + * 获得当前用户的编号,从上下文中 + * + * @return 用户编号 + */ + @Nullable + public static Long getLoginUserId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 设置当前用户 + * + * @param loginUser 登录用户 + * @param request 请求 + */ + public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { + // 创建 Authentication,并设置到上下文 + Authentication authentication = buildAuthentication(loginUser, request); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { + // 创建 UsernamePasswordAuthenticationToken 对象 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginUser, null, Collections.emptyList()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + return authenticationToken; + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java new file mode 100644 index 000000000..50f1836f7 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.ssodemo.framework.core.util; + +import cn.hutool.extra.servlet.ServletUtil; +import cn.hutool.json.JSONUtil; +import org.springframework.http.MediaType; + +import javax.servlet.http.HttpServletResponse; + +/** + * 客户端工具类 + * + * @author 芋道源码 + */ +public class ServletUtils { + + /** + * 返回 JSON 字符串 + * + * @param response 响应 + * @param object 对象,会序列化成 JSON 字符串 + */ + @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 + public static void writeJSON(HttpServletResponse response, Object object) { + String content = JSONUtil.toJsonStr(object); + ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/resources/application.yaml b/yudao-example/yudao-sso-demo-by-code/src/main/resources/application.yaml new file mode 100644 index 000000000..a62cf97dc --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/resources/application.yaml @@ -0,0 +1,2 @@ +server: + port: 18080 diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/callback.html b/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/callback.html new file mode 100644 index 000000000..123a1af9b --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/callback.html @@ -0,0 +1,61 @@ + + + + + SSO 授权后的回调页 + + + + + + + + +正在使用 code 授权码,进行 accessToken 访问令牌的获取 + + diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html b/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html new file mode 100644 index 000000000..a4f858363 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html @@ -0,0 +1,159 @@ + + + + + 首页 + + + + + + + + + + + + + + diff --git a/yudao-example/yudao-sso-demo-by-password/pom.xml b/yudao-example/yudao-sso-demo-by-password/pom.xml new file mode 100644 index 000000000..be86d771c --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + + cn.iocoder.boot + yudao-sso-demo-by-password + 1.0.0-snapshot + jar + + ${project.artifactId} + 基于密码模式,如何实现 SSO 单点登录? + https://github.com/YunaiV/ruoyi-vue-pro + + + + 8 + 8 + UTF-8 + + 2.6.10 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + cn.hutool + hutool-all + 5.8.8 + + + + org.projectlombok + lombok + true + + + + diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java new file mode 100644 index 000000000..f6b160745 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.ssodemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SSODemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SSODemoApplication.class, args); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java new file mode 100644 index 000000000..4f6b63481 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java @@ -0,0 +1,127 @@ +package cn.iocoder.yudao.ssodemo.client; + +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.Base64Utils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; + +/** + * OAuth 2.0 客户端 + * + * 对应调用 OAuth2OpenController 接口 + */ +@Component +public class OAuth2Client { + + private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2"; + + /** + * 租户编号 + * + * 默认使用 1;如果使用别的租户,可以调整 + */ + public static final Long TENANT_ID = 1L; + + private static final String CLIENT_ID = "yudao-sso-demo-by-password"; + private static final String CLIENT_SECRET = "test"; + + +// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * 校验访问令牌,并返回它的基本信息 + * + * @param token 访问令牌 + * @return 访问令牌的基本信息 + */ + public CommonResult checkToken(String token) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/check-token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌 + */ + public CommonResult refreshToken(String refreshToken) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "refresh_token"); + body.add("refresh_token", refreshToken); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 删除访问令牌 + * + * @param token 访问令牌 + * @return 成功 + */ + public CommonResult revokeToken(String token) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.DELETE, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + private static void addClientHeader(HttpHeaders headers) { + // client 拼接,需要 BASE64 编码 + String client = CLIENT_ID + ":" + CLIENT_SECRET; + client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8)); + headers.add("Authorization", "Basic " + client); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java new file mode 100644 index 000000000..666bd3ee4 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.ssodemo.client; + +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO; +import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO; +import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +/** + * 用户 User 信息的客户端 + * + * 对应调用 OAuth2UserController 接口 + */ +@Component +public class UserClient { + + private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user"; + + // @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 + private final RestTemplate restTemplate = new RestTemplate(); + + public CommonResult getUser() { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); + addTokenHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/get", + HttpMethod.GET, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + public CommonResult updateUser(UserUpdateReqDTO updateReqDTO) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); + addTokenHeader(headers); + // 1.2 构建请求参数 + // 使用 updateReqDTO 即可 + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/update", + HttpMethod.PUT, + new HttpEntity<>(updateReqDTO, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + + private static void addTokenHeader(HttpHeaders headers) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + Assert.notNull(loginUser, "登录用户不能为空"); + headers.add("Authorization", "Bearer " + loginUser.getAccessToken()); + } +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java new file mode 100644 index 000000000..548fe51e4 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.ssodemo.client.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + */ + private String msg; + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java new file mode 100644 index 000000000..6a5369a20 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.ssodemo.client.dto.oauth2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 访问令牌 Response DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2AccessTokenRespDTO { + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 刷新令牌 + */ + @JsonProperty("refresh_token") + private String refreshToken; + + /** + * 令牌类型 + */ + @JsonProperty("token_type") + private String tokenType; + + /** + * 过期时间;单位:秒 + */ + @JsonProperty("expires_in") + private Long expiresIn; + + /** + * 授权范围;如果多个授权范围,使用空格分隔 + */ + private String scope; + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java new file mode 100644 index 000000000..862bcf04b --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.ssodemo.client.dto.oauth2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 校验令牌 Response DTO + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2CheckTokenRespDTO { + + /** + * 用户编号 + */ + @JsonProperty("user_id") + private Long userId; + /** + * 用户类型 + */ + @JsonProperty("user_type") + private Integer userType; + /** + * 租户编号 + */ + @JsonProperty("tenant_id") + private Long tenantId; + + /** + * 客户端编号 + */ + @JsonProperty("client_id") + private String clientId; + /** + * 授权范围 + */ + private List scopes; + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 过期时间 + * + * 时间戳 / 1000,即单位:秒 + */ + private Long exp; + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java new file mode 100644 index 000000000..e81bea9eb --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.ssodemo.client.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 获得用户基本信息 Response dto + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserInfoRespDTO { + + /** + * 用户编号 + */ + private Long id; + + /** + * 用户账号 + */ + private String username; + + /** + * 用户昵称 + */ + private String nickname; + + /** + * 用户邮箱 + */ + private String email; + /** + * 手机号码 + */ + private String mobile; + + /** + * 用户性别 + */ + private Integer sex; + + /** + * 用户头像 + */ + private String avatar; + + /** + * 所在部门 + */ + private Dept dept; + + /** + * 所属岗位数组 + */ + private List posts; + + /** + * 部门 + */ + @Data + public static class Dept { + + /** + * 部门编号 + */ + private Long id; + + /** + * 部门名称 + */ + private String name; + + } + + /** + * 岗位 + */ + @Data + public static class Post { + + /** + * 岗位编号 + */ + private Long id; + + /** + * 岗位名称 + */ + private String name; + + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java new file mode 100644 index 000000000..e711d7311 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.ssodemo.client.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 更新用户基本信息 Request DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserUpdateReqDTO { + + /** + * 用户昵称 + */ + private String nickname; + + /** + * 用户邮箱 + */ + private String email; + + /** + * 手机号码 + */ + private String mobile; + + /** + * 用户性别 + */ + private Integer sex; + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java new file mode 100644 index 000000000..6aedc3b24 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.ssodemo.controller; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.ssodemo.client.OAuth2Client; +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Resource + private OAuth2Client oauth2Client; + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @PostMapping("/refresh-token") + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return oauth2Client.refreshToken(refreshToken); + } + + /** + * 退出登录 + * + * @param request 请求 + * @return 成功 + */ + @PostMapping("/logout") + public CommonResult logout(HttpServletRequest request) { + String token = SecurityUtils.obtainAuthorization(request, "Authorization"); + if (StrUtil.isNotBlank(token)) { + return oauth2Client.revokeToken(token); + } + // 返回成功 + return new CommonResult<>(); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java new file mode 100644 index 000000000..819c752bc --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.ssodemo.controller; + +import cn.iocoder.yudao.ssodemo.client.UserClient; +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO; +import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("/user") +public class UserController { + + @Resource + private UserClient userClient; + + /** + * 获得当前登录用户的基本信息 + * + * @return 用户信息;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @GetMapping("/get") + public CommonResult getUser() { + return userClient.getUser(); + } + + /** + * 更新当前登录用户的昵称 + * + * @param nickname 昵称 + * @return 成功 + */ + @PutMapping("/update") + public CommonResult updateUser(@RequestParam("nickname") String nickname) { + UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null); + return userClient.updateUser(updateReqDTO); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java new file mode 100644 index 000000000..1d19fcfdb --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.ssodemo.framework.config; + +import cn.iocoder.yudao.ssodemo.framework.core.filter.TokenAuthenticationFilter; +import cn.iocoder.yudao.ssodemo.framework.core.handler.AccessDeniedHandlerImpl; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.annotation.Resource; + +@Configuration +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Resource + private TokenAuthenticationFilter tokenAuthenticationFilter; + + @Resource + private AccessDeniedHandlerImpl accessDeniedHandler; + @Resource + private AuthenticationEntryPoint authenticationEntryPoint; + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + // 设置 URL 安全权限 + httpSecurity.csrf().disable() // 禁用 CSRF 保护 + .authorizeRequests() + // 1. 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() + // 2. 登录相关的接口,可匿名访问 + .antMatchers("/auth/login-by-code").permitAll() + .antMatchers("/auth/refresh-token").permitAll() + .antMatchers("/auth/logout").permitAll() + // last. 兜底规则,必须认证 + .and().authorizeRequests() + .anyRequest().authenticated(); + + // 设置处理器 + httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler) + .authenticationEntryPoint(authenticationEntryPoint); + + // 添加 Token Filter + httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java new file mode 100644 index 000000000..44f3edf54 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.ssodemo.framework.core; + +import lombok.Data; + +import java.util.List; + +/** + * 登录用户信息 + * + * @author 芋道源码 + */ +@Data +public class LoginUser { + + /** + * 用户编号 + */ + private Long id; + /** + * 用户类型 + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围 + */ + private List scopes; + + /** + * 访问令牌 + */ + private String accessToken; + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 000000000..70bb40eaa --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.ssodemo.framework.core.filter; + +import cn.iocoder.yudao.ssodemo.client.OAuth2Client; +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; +import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.Resource; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Token 过滤器,验证 token 的有效性 + * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 + * + * @author 芋道源码 + */ +@Component +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + @Resource + private OAuth2Client oauth2Client; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + // 1. 获得访问令牌 + String token = SecurityUtils.obtainAuthorization(request, "Authorization"); + if (StringUtils.hasText(token)) { + // 2. 基于 token 构建登录用户 + LoginUser loginUser = buildLoginUserByToken(token); + // 3. 设置当前用户 + if (loginUser != null) { + SecurityUtils.setLoginUser(loginUser, request); + } + } + + // 继续过滤链 + filterChain.doFilter(request, response); + } + + private LoginUser buildLoginUserByToken(String token) { + try { + CommonResult accessTokenResult = oauth2Client.checkToken(token); + OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); + if (accessToken == null) { + return null; + } + // 构建登录用户 + return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) + .setAccessToken(accessToken.getAccessToken()); + } catch (Exception exception) { + // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 + return null; + } + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 000000000..dccc35eb1 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.ssodemo.framework.core.handler; + +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; +import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 + * + * @author 芋道源码 + */ +@Component +@SuppressWarnings("JavadocReference") +@Slf4j +public class AccessDeniedHandlerImpl implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) + throws IOException, ServletException { + // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 + log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), + SecurityUtils.getLoginUserId(), e); + // 返回 403 + CommonResult result = new CommonResult<>(); + result.setCode(HttpStatus.FORBIDDEN.value()); + result.setMsg("没有该操作权限"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 000000000..241ff56f5 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.ssodemo.framework.core.handler; + +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 + */ +@Component +@Slf4j +@SuppressWarnings("JavadocReference") // 忽略文档引用报错 +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); + // 返回 401 + CommonResult result = new CommonResult<>(); + result.setCode(HttpStatus.UNAUTHORIZED.value()); + result.setMsg("账号未登录"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java new file mode 100644 index 000000000..0352a4785 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.ssodemo.framework.core.util; + +import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; + +/** + * 安全服务工具类 + * + * @author 芋道源码 + */ +public class SecurityUtils { + + public static final String AUTHORIZATION_BEARER = "Bearer"; + + private SecurityUtils() {} + + /** + * 从请求中,获得认证 Token + * + * @param request 请求 + * @param header 认证 Token 对应的 Header 名字 + * @return 认证 Token + */ + public static String obtainAuthorization(HttpServletRequest request, String header) { + String authorization = request.getHeader(header); + if (!StringUtils.hasText(authorization)) { + return null; + } + int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); + if (index == -1) { // 未找到 + return null; + } + return authorization.substring(index + 7).trim(); + } + + /** + * 获得当前认证信息 + * + * @return 认证信息 + */ + public static Authentication getAuthentication() { + SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return null; + } + return context.getAuthentication(); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + @Nullable + public static LoginUser getLoginUser() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; + } + + /** + * 获得当前用户的编号,从上下文中 + * + * @return 用户编号 + */ + @Nullable + public static Long getLoginUserId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 设置当前用户 + * + * @param loginUser 登录用户 + * @param request 请求 + */ + public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { + // 创建 Authentication,并设置到上下文 + Authentication authentication = buildAuthentication(loginUser, request); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { + // 创建 UsernamePasswordAuthenticationToken 对象 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginUser, null, Collections.emptyList()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + return authenticationToken; + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java new file mode 100644 index 000000000..50f1836f7 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.ssodemo.framework.core.util; + +import cn.hutool.extra.servlet.ServletUtil; +import cn.hutool.json.JSONUtil; +import org.springframework.http.MediaType; + +import javax.servlet.http.HttpServletResponse; + +/** + * 客户端工具类 + * + * @author 芋道源码 + */ +public class ServletUtils { + + /** + * 返回 JSON 字符串 + * + * @param response 响应 + * @param object 对象,会序列化成 JSON 字符串 + */ + @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 + public static void writeJSON(HttpServletResponse response, Object object) { + String content = JSONUtil.toJsonStr(object); + ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/resources/application.yaml b/yudao-example/yudao-sso-demo-by-password/src/main/resources/application.yaml new file mode 100644 index 000000000..a62cf97dc --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/resources/application.yaml @@ -0,0 +1,2 @@ +server: + port: 18080 diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/resources/static/index.html b/yudao-example/yudao-sso-demo-by-password/src/main/resources/static/index.html new file mode 100644 index 000000000..8a5372901 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/resources/static/index.html @@ -0,0 +1,154 @@ + + + + + 首页 + + + + + + + + + + + + + + diff --git a/yudao-example/yudao-sso-demo-by-password/src/main/resources/static/login.html b/yudao-example/yudao-sso-demo-by-password/src/main/resources/static/login.html new file mode 100644 index 000000000..79b866bf2 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-password/src/main/resources/static/login.html @@ -0,0 +1,74 @@ + + + + + 登录 + + + + + + +账号:
+密码:
+ + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml index e6106b9d9..1c3a3bde1 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml @@ -52,7 +52,7 @@ com.alipay.sdk alipay-sdk-java - 4.31.72.ALL + 4.33.44.ALL org.bouncycastle @@ -63,7 +63,7 @@ com.github.binarywang weixin-java-pay - 4.3.8.B + 4.4.0 diff --git a/yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml index 2dc2fe688..8af0041a9 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml @@ -35,12 +35,12 @@ com.github.binarywang wx-java-mp-spring-boot-starter - 4.3.8.B + 4.4.0 com.github.binarywang wx-java-miniapp-spring-boot-starter - 4.3.8.B + 4.4.0 diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java index 3af543530..2cea08bf8 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/lock4j/core/DefaultLockFailureStrategy.java @@ -5,6 +5,8 @@ import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstant import com.baomidou.lock.LockFailureStrategy; import lombok.extern.slf4j.Slf4j; +import java.lang.reflect.Method; + /** * 自定义获取锁失败策略,抛出 {@link ServiceException} 异常 */ @@ -12,9 +14,8 @@ import lombok.extern.slf4j.Slf4j; public class DefaultLockFailureStrategy implements LockFailureStrategy { @Override - public void onLockFailure(String key, long acquireTimeout, int acquireCount) { - log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取超时时长:{} ms]", Thread.currentThread().getName(), key, acquireTimeout); + public void onLockFailure(String key, Method method, Object[] arguments) { + log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取失败:{} ]", Thread.currentThread().getName(), key, arguments); throw new ServiceException(GlobalErrorCodeConstants.LOCKED); } - } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/message/BpmMessageConvert.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/message/BpmMessageConvert.java index 92683f4c9..99b7bebf2 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/message/BpmMessageConvert.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/message/BpmMessageConvert.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.convert.message; import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; import java.util.Map; @@ -11,6 +12,10 @@ public interface BpmMessageConvert { BpmMessageConvert INSTANCE = Mappers.getMapper(BpmMessageConvert.class); + @Mapping(target = "mobile", ignore = true) + @Mapping(source = "userId", target = "userId") + @Mapping(source = "templateCode", target = "templateCode") + @Mapping(source = "templateParams", target = "templateParams") SmsSendSingleToUserReqDTO convert(Long userId, String templateCode, Map templateParams); } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm index c497a1801..ca4adc1a6 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm @@ -50,23 +50,29 @@ // 提交按钮 const submitForm = async () => { - actionLoading.value = true - // 提交请求 - try { - const data = unref(formRef)?.formModel as ${simpleClassName}VO - if (actionType.value === 'create') { - await ${simpleClassName}Api.create${simpleClassName}Api(data) - ElMessage.success(t('common.createSuccess')) - } else { - await ${simpleClassName}Api.update${simpleClassName}Api(data) - ElMessage.success(t('common.updateSuccess')) + const elForm = unref(formRef)?.getElFormRef() + if (!elForm) return + elForm.validate(async (valid) => { + if (valid) { + actionLoading.value = true + // 提交请求 + try { + const data = unref(formRef)?.formModel as ${simpleClassName}VO + if (actionType.value === 'create') { + await ${simpleClassName}Api.create${simpleClassName}Api(data) + ElMessage.success(t('common.createSuccess')) + } else { + await ${simpleClassName}Api.update${simpleClassName}Api(data) + ElMessage.success(t('common.updateSuccess')) + } + // 操作成功,重新加载列表 + dialogVisible.value = false + await getList() + } finally { + actionLoading.value = false + } } - // 操作成功,重新加载列表 - dialogVisible.value = false - await getList() - } finally { - actionLoading.value = false - } + }) } // ========== 详情相关 ========== diff --git a/yudao-module-mall/yudao-module-coupon-biz/pom.xml b/yudao-module-mall/yudao-module-coupon-biz/pom.xml index eda16bd16..225c24d4e 100644 --- a/yudao-module-mall/yudao-module-coupon-biz/pom.xml +++ b/yudao-module-mall/yudao-module-coupon-biz/pom.xml @@ -30,10 +30,6 @@ cn.iocoder.boot yudao-spring-boot-starter-biz-operatelog - - cn.iocoder.boot - yudao-spring-boot-starter-biz-tenant - @@ -42,7 +38,7 @@ cn.iocoder.boot - yudao-spring-boot-starter-excel + yudao-spring-boot-starter-security @@ -57,6 +53,12 @@ yudao-spring-boot-starter-test + + + cn.iocoder.boot + yudao-spring-boot-starter-excel + + - \ No newline at end of file + diff --git a/yudao-module-mall/yudao-module-coupon-biz/src/main/java/cn/iocoder/yudao/module/coupon/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-coupon-biz/src/main/java/cn/iocoder/yudao/module/coupon/service/coupon/CouponServiceImpl.java index 0a1737a31..14f3cacc4 100644 --- a/yudao-module-mall/yudao-module-coupon-biz/src/main/java/cn/iocoder/yudao/module/coupon/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-coupon-biz/src/main/java/cn/iocoder/yudao/module/coupon/service/coupon/CouponServiceImpl.java @@ -4,7 +4,6 @@ import cn.hutool.core.bean.BeanUtil; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.module.coupon.dal.dataobject.CouponTemplete.CouponTempleteDO; import cn.iocoder.yudao.module.coupon.dal.mysql.CouponTemplete.CouponTempleteMapper; -import io.micrometer.core.instrument.Counter; import org.springframework.stereotype.Service; import javax.annotation.Resource; import org.springframework.validation.annotation.Validated; diff --git a/yudao-module-mall/yudao-module-market-biz/pom.xml b/yudao-module-mall/yudao-module-market-biz/pom.xml index 669e9ed63..dc90257d8 100644 --- a/yudao-module-mall/yudao-module-market-biz/pom.xml +++ b/yudao-module-mall/yudao-module-market-biz/pom.xml @@ -39,16 +39,16 @@ cn.iocoder.boot yudao-spring-boot-starter-biz-weixin - - cn.iocoder.boot - yudao-spring-boot-starter-biz-tenant - cn.iocoder.boot yudao-spring-boot-starter-web + + cn.iocoder.boot + yudao-spring-boot-starter-security + @@ -61,6 +61,12 @@ cn.iocoder.boot yudao-spring-boot-starter-test + + + + cn.iocoder.boot + yudao-spring-boot-starter-excel + diff --git a/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/controller/app/banner/AppBannerController.java b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/controller/app/banner/AppBannerController.java index 3d9d58eb8..9af420a61 100644 --- a/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/controller/app/banner/AppBannerController.java +++ b/yudao-module-mall/yudao-module-market-biz/src/main/java/cn/iocoder/yudao/module/market/controller/app/banner/AppBannerController.java @@ -7,13 +7,13 @@ import cn.iocoder.yudao.module.market.dal.dataobject.banner.BannerDO; import cn.iocoder.yudao.module.market.service.banner.BannerService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import javax.annotation.Resource; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @@ -27,8 +27,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Validated public class AppBannerController { - // TODO @xia:使用 @Resource 哈 - @Autowired + @Resource private BannerService bannerService; // TODO @xia:新建一个 AppBannerRespVO,只返回必要的字段。status 要过滤下。然后 sort 下结果 diff --git a/yudao-module-mall/yudao-module-product-biz/pom.xml b/yudao-module-mall/yudao-module-product-biz/pom.xml index bb9a36f94..e89ed105e 100644 --- a/yudao-module-mall/yudao-module-product-biz/pom.xml +++ b/yudao-module-mall/yudao-module-product-biz/pom.xml @@ -29,10 +29,6 @@ cn.iocoder.boot yudao-spring-boot-starter-biz-operatelog - - cn.iocoder.boot - yudao-spring-boot-starter-biz-tenant - @@ -41,7 +37,7 @@ cn.iocoder.boot - yudao-spring-boot-starter-excel + yudao-spring-boot-starter-security @@ -56,6 +52,11 @@ yudao-spring-boot-starter-test + + + cn.iocoder.boot + yudao-spring-boot-starter-excel + diff --git a/yudao-module-mall/yudao-module-trade-biz/pom.xml b/yudao-module-mall/yudao-module-trade-biz/pom.xml index 79eb00955..4e95c09d2 100644 --- a/yudao-module-mall/yudao-module-trade-biz/pom.xml +++ b/yudao-module-mall/yudao-module-trade-biz/pom.xml @@ -47,10 +47,6 @@ cn.iocoder.boot yudao-spring-boot-starter-biz-operatelog - - cn.iocoder.boot - yudao-spring-boot-starter-biz-tenant - @@ -60,13 +56,13 @@ cn.iocoder.boot - yudao-spring-boot-starter-biz-pay + yudao-spring-boot-starter-security - + cn.iocoder.boot - yudao-spring-boot-starter-mybatis + yudao-spring-boot-starter-biz-pay @@ -80,6 +76,12 @@ cn.iocoder.boot yudao-spring-boot-starter-test + + + + cn.iocoder.boot + yudao-spring-boot-starter-excel + diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java index 67a5d93d2..ef77780de 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java @@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.validation.Mobile; import lombok.Data; import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; import java.util.Map; /** diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java index 8ab096587..608f5341a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java @@ -106,7 +106,7 @@ public class OAuth2OpenController { @RequestParam(value = "scope", required = false) String scope, // 密码模式 @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式 List scopes = OAuth2Utils.buildScopes(scope); - // 授权类型 + // 1.1 校验授权类型 OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType); if (grantTypeEnum == null) { throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType)); @@ -115,12 +115,12 @@ public class OAuth2OpenController { throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式"); } - // 校验客户端 + // 1.2 校验客户端 String[] clientIdAndSecret = obtainBasicAuthorization(request); OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], grantType, scopes, redirectUri); - // 根据授权模式,获取访问令牌 + // 2. 根据授权模式,获取访问令牌 OAuth2AccessTokenDO accessTokenDO; switch (grantTypeEnum) { case AUTHORIZATION_CODE: diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java index f23838718..e1639e9ac 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java @@ -26,6 +26,7 @@ public class OAuth2OpenCheckTokenRespVO { private Long tenantId; @ApiModelProperty(value = "客户端编号", required = true, example = "car") + @JsonProperty("client_id") private String clientId; @ApiModelProperty(value = "授权范围", required = true, example = "user_info") private List scopes; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java index ddd7aecc7..f22c14a9f 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java @@ -17,7 +17,7 @@ public class OAuth2UserInfoRespVO { @ApiModelProperty(value = "用户编号", required = true, example = "1") private Long id; - @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿") + @ApiModelProperty(value = "用户账号", required = true, example = "芋艿") private String username; @ApiModelProperty(value = "用户昵称", required = true, example = "芋道") diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java index e8e200911..9813d8c0a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java @@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.*; import cn.iocoder.yudao.module.system.convert.permission.MenuConvert; import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; import cn.iocoder.yudao.module.system.service.permission.MenuService; -import cn.iocoder.yudao.module.system.service.tenant.TenantService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiOperation; @@ -29,8 +28,6 @@ public class MenuController { @Resource private MenuService menuService; - @Resource - private TenantService tenantService; @PostMapping("/create") @ApiOperation("创建菜单") diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantPackageDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantPackageDO.java index 430c3a080..dba7569af 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantPackageDO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/tenant/TenantPackageDO.java @@ -34,7 +34,7 @@ public class TenantPackageDO extends BaseDO { */ private String name; /** - * 租户状态 + * 租户套餐状态 * * 枚举 {@link CommonStatusEnum} */ diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java index 6b7a8f56a..fef1d5e8d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java @@ -12,7 +12,7 @@ import java.util.List; * 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。 * * 问题:为什么自身也作为一个第三方应用,也走这套流程呢? - * 回复:当然可以这么做,采用 Implicit 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。 + * 回复:当然可以这么做,采用 password 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。 * * @author 芋道源码 */ diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 11cb1b347..36ab6a8b2 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -45,33 +45,33 @@ spring: datasource: master: name: ruoyi-vue-pro - url: jdbc:mysql://139.9.196.247:3306/${spring.datasource.dynamic.datasource.master.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 # url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例 # url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例 username: root - password: ${RUOYI_VUE_PRO} + password: 123456 # username: sa # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W slave: # 模拟从库,可根据自己需要修改 name: ruoyi-vue-pro - url: jdbc:mysql://139.9.196.247:3306/${spring.datasource.dynamic.datasource.slave.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 # url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例 # url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例 # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例 username: root - password: ${RUOYI_VUE_PRO} + password: 123456 # username: sa # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 redis: - host: 139.9.196.247 # 地址 + host: 127.0.0.1 # 地址 port: 6379 # 端口 database: 0 # 数据库索引 - password: 123456 # 密码,建议生产环境开启 +# password: 123456 # 密码,建议生产环境开启 jasypt: encryptor: @@ -198,7 +198,7 @@ wx: # 芋道配置项,设置当前项目所有自定义的配置 yudao: captcha: - enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试 + enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试; security: mock-enable: true xss: diff --git a/yudao-ui-admin-vue3/README.md b/yudao-ui-admin-vue3/README.md index 8379c7f7e..b9032638e 100644 --- a/yudao-ui-admin-vue3/README.md +++ b/yudao-ui-admin-vue3/README.md @@ -27,17 +27,17 @@ | 框架 | 说明 | 版本 | | --- | --- |--------| -| [Vue](https://staging-cn.vuejs.org/) | vue 框架 | 3.2.37 | -| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.0.4 | -| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.12 | -| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.7.4 | -| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.17 | -| [vueuse](https://vueuse.org//) | 常用工具集 | 9.0.2 | -| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.0 | -| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.3 | +| [Vue](https://staging-cn.vuejs.org/) | vue 框架 | 3.2.41 | +| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.1.8 | +| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.18 | +| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.8.4 | +| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.23 | +| [vueuse](https://vueuse.org//) | 常用工具集 | 9.3.1 | +| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 | +| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.5 | | [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 | -| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.1 | -| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.14 | +| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.0.0 | +| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.21 | ### 推荐 VScode 开发,插件如下 diff --git a/yudao-ui-admin-vue3/package.json b/yudao-ui-admin-vue3/package.json index 81c41cc42..ab056d30b 100644 --- a/yudao-ui-admin-vue3/package.json +++ b/yudao-ui-admin-vue3/package.json @@ -1,6 +1,6 @@ { "name": "ruoyi-vue-pro-vue3", - "version": "1.6.3.1611", + "version": "1.6.4.1811", "description": "基于vue3、element-plus、typesScript、vite3", "author": "xingyu", "private": false, @@ -10,7 +10,7 @@ "ts:check": "vue-tsc --noEmit", "build:pro": "vite build --mode pro", "build:dev": "vite build --mode dev", - "build:test": "pnpm run ts:check && vite build --mode test", + "build:test": "npm run ts:check && vite build --mode test", "serve:pro": "vite preview --mode pro", "serve:dev": "vite preview --mode dev", "serve:test": "vite preview --mode test", @@ -25,84 +25,84 @@ "p": "plop" }, "dependencies": { - "@iconify/iconify": "^2.2.1", - "@vueuse/core": "^9.1.0", - "@wangeditor/editor": "^5.1.14", + "@iconify/iconify": "^3.0.0", + "@vueuse/core": "^9.3.1", + "@wangeditor/editor": "^5.1.21", "@wangeditor/editor-for-vue": "^5.1.10", - "@zxcvbn-ts/core": "^2.0.4", + "@zxcvbn-ts/core": "^2.0.5", "animate.css": "^4.1.1", "axios": "^0.27.2", "crypto-js": "^4.1.1", - "dayjs": "^1.11.4", - "echarts": "^5.3.3", + "dayjs": "^1.11.5", + "echarts": "^5.4.0", "echarts-wordcloud": "^2.0.0", - "element-plus": "2.2.12", + "element-plus": "2.2.18", "intro.js": "^6.0.0", "jsencrypt": "^3.2.1", "lodash-es": "^4.17.21", "mitt": "^3.0.0", "nprogress": "^0.2.0", - "pinia": "^2.0.17", - "pinia-plugin-persist": "^1.0.0", + "pinia": "^2.0.23", "qrcode": "^1.5.1", "qs": "^6.11.0", "url": "^0.11.0", - "vue": "3.2.37", + "vue": "3.2.41", "vue-cropper": "^1.0.3", "vue-i18n": "9.2.2", - "vue-router": "^4.1.3", + "vue-router": "^4.1.5", "vue-types": "^4.2.1", "web-storage-cache": "^1.1.1" }, "devDependencies": { - "@commitlint/cli": "^17.0.3", - "@commitlint/config-conventional": "^17.0.3", - "@iconify/json": "^2.1.89", - "@intlify/vite-plugin-vue-i18n": "^6.0.0", + "@commitlint/cli": "^17.1.2", + "@commitlint/config-conventional": "^17.1.0", + "@iconify/json": "^2.1.122", + "@intlify/vite-plugin-vue-i18n": "^6.0.3", "@purge-icons/generated": "^0.9.0", "@types/intro.js": "^5.1.0", "@types/lodash-es": "^4.17.6", - "@types/node": "^18.6.5", + "@types/node": "^18.11.0", "@types/nprogress": "^0.2.0", - "@types/qrcode": "^1.4.2", + "@types/qrcode": "^1.5.0", "@types/qs": "^6.9.7", - "@typescript-eslint/eslint-plugin": "^5.33.0", - "@typescript-eslint/parser": "^5.33.0", - "@vitejs/plugin-vue": "^3.0.1", - "@vitejs/plugin-vue-jsx": "^2.0.0", - "autoprefixer": "^10.4.8", - "eslint": "^8.21.0", + "@typescript-eslint/eslint-plugin": "^5.40.0", + "@typescript-eslint/parser": "^5.40.0", + "@vitejs/plugin-vue": "^3.1.2", + "@vitejs/plugin-vue-jsx": "^2.0.1", + "autoprefixer": "^10.4.12", + "eslint": "^8.25.0", "eslint-config-prettier": "^8.5.0", - "eslint-define-config": "^1.6.0", + "eslint-define-config": "^1.7.0", "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-vue": "^9.3.0", + "eslint-plugin-vue": "^9.6.0", "less": "^4.1.3", "lint-staged": "^13.0.3", "plop": "^3.1.1", - "postcss": "^8.4.16", + "postcss": "^8.4.18", "postcss-html": "^1.5.0", "postcss-less": "^6.0.0", "prettier": "^2.7.1", "rimraf": "^3.0.2", - "rollup": "^2.77.2", - "stylelint": "^14.9.1", + "rollup": "^2.79.1", + "stylelint": "^14.14.0", "stylelint-config-html": "^1.1.0", "stylelint-config-prettier": "^9.0.3", - "stylelint-config-recommended": "^8.0.0", - "stylelint-config-standard": "^26.0.0", + "stylelint-config-recommended": "^9.0.0", + "stylelint-config-standard": "^29.0.0", "stylelint-order": "^5.0.0", - "typescript": "4.7.4", - "unplugin-vue-define-options": "^0.7.3", - "vite": "3.0.5", + "typescript": "4.8.4", + "unplugin-vue-macros": "^0.13.2", + "vite": "3.1.8", "vite-plugin-compression": "^0.5.1", - "vite-plugin-eslint": "^1.7.0", + "vite-plugin-eslint": "^1.8.1", "vite-plugin-html": "^3.2.0", - "vite-plugin-purge-icons": "^0.9.0", - "vite-plugin-style-import": "^2.0.0", + "vite-plugin-purge-icons": "^0.9.1", + "vite-plugin-style-import": "2.0.0", "vite-plugin-svg-icons": "^2.0.1", - "vite-plugin-windicss": "^1.8.7", - "vue-tsc": "^0.39.5", - "windicss": "^3.5.6" + "vite-plugin-windicss": "^1.8.8", + "vue-tsc": "^1.0.8", + "windicss": "^3.5.6", + "windicss-analysis": "^0.3.5" }, "engines": { "node": ">= 14.18.0" diff --git a/yudao-ui-admin-vue3/src/api/system/tenant/types.ts b/yudao-ui-admin-vue3/src/api/system/tenant/types.ts index 1fc58bd71..dc19b8e2c 100644 --- a/yudao-ui-admin-vue3/src/api/system/tenant/types.ts +++ b/yudao-ui-admin-vue3/src/api/system/tenant/types.ts @@ -4,6 +4,8 @@ export type TenantVO = { packageId: number contactName: string contactMobile: string + username: string + password: string accountCount: number expireTime: string domain: string diff --git a/yudao-ui-admin-vue3/src/components/ConfigGlobal/src/ConfigGlobal.vue b/yudao-ui-admin-vue3/src/components/ConfigGlobal/src/ConfigGlobal.vue index 985d5dfbb..da6f8c710 100644 --- a/yudao-ui-admin-vue3/src/components/ConfigGlobal/src/ConfigGlobal.vue +++ b/yudao-ui-admin-vue3/src/components/ConfigGlobal/src/ConfigGlobal.vue @@ -7,13 +7,14 @@ import { useWindowSize } from '@vueuse/core' import { useAppStore } from '@/store/modules/app' import { setCssVar } from '@/utils' import { useDesign } from '@/hooks/web/useDesign' +import { ElementPlusSize } from '@/types/elementPlus' const { variables } = useDesign() const appStore = useAppStore() const props = defineProps({ - size: propTypes.oneOf(['default', 'small', 'large']).def('default') + size: propTypes.oneOf(['default', 'small', 'large']).def('default') }) provide('configGlobal', props) diff --git a/yudao-ui-admin-vue3/src/components/ContextMenu/src/ContextMenu.vue b/yudao-ui-admin-vue3/src/components/ContextMenu/src/ContextMenu.vue index 500712154..bd53950e3 100644 --- a/yudao-ui-admin-vue3/src/components/ContextMenu/src/ContextMenu.vue +++ b/yudao-ui-admin-vue3/src/components/ContextMenu/src/ContextMenu.vue @@ -1,9 +1,10 @@