mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2024-11-24 08:11:55 +08:00
Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro
This commit is contained in:
commit
87c40a4ab1
43
README.md
43
README.md
@ -23,6 +23,8 @@
|
|||||||
>
|
>
|
||||||
> 😜 给项目点点 Star 吧,这对我们真的很重要!
|
> 😜 给项目点点 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)
|
* 管理后台的 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!
|
* 管理后台的移动端采用 [uni-app](https://github.com/dcloudio/uni-app) 方案,一份代码多终端适配,同时支持 APP、小程序、H5!
|
||||||
* 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson
|
* 后端采用 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) |
|
| `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) |
|
| `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 修改,**重构优化**后端的代码,**美化**前端的界面。
|
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
|
||||||
>
|
>
|
||||||
@ -170,14 +175,14 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
|||||||
### 后端
|
### 后端
|
||||||
|
|
||||||
| 框架 | 说明 | 版本 | 学习指南 |
|
| 框架 | 说明 | 版本 | 学习指南 |
|
||||||
|---------------------------------------------------------------------------------------------|-----------------------|---------|----------------------------------------------------------------|
|
|---------------------------------------------------------------------------------------------|-----------------------|------------------|----------------------------------------------------------------|
|
||||||
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.6.10 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
|
| [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 | |
|
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | |
|
||||||
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.11 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
| [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) |
|
| [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) |
|
| [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 | |
|
| [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) |
|
| [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 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) |
|
| [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) |
|
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.3 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
|
||||||
@ -186,12 +191,12 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
|||||||
| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?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) |
|
| [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) |
|
| [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) |
|
| [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 | |
|
| [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) |
|
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.5.2.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
|
||||||
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?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 | - |
|
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - |
|
||||||
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.0.0 | - |
|
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.8.0 | - |
|
||||||
|
|
||||||
### [管理后台 Vue2 前端](./yudao-ui-admin)
|
### [管理后台 Vue2 前端](./yudao-ui-admin)
|
||||||
|
|
||||||
@ -204,14 +209,14 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
|||||||
|
|
||||||
| 框架 | 说明 | 版本 |
|
| 框架 | 说明 | 版本 |
|
||||||
|----------------------------------------------------------------------|-----------------|--------|
|
|----------------------------------------------------------------------|-----------------|--------|
|
||||||
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.37 |
|
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.41 |
|
||||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.0.4 |
|
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.1.8 |
|
||||||
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.12 |
|
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.18 |
|
||||||
| [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.7.4 |
|
| [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.8.4 |
|
||||||
| [pinia](https://pinia.vuejs.org/) | vuex5 | 2.0.17 |
|
| [pinia](https://pinia.vuejs.org/) | vuex5 | 2.0.23 |
|
||||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
|
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
|
||||||
| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
|
| [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)
|
### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp)
|
||||||
|
|
||||||
|
6
pom.xml
6
pom.xml
@ -20,6 +20,8 @@
|
|||||||
<module>yudao-module-pay</module>
|
<module>yudao-module-pay</module>
|
||||||
<module>yudao-module-mall</module>
|
<module>yudao-module-mall</module>
|
||||||
<module>yudao-module-visualization</module>
|
<module>yudao-module-visualization</module>
|
||||||
|
<!-- 示例项目 -->
|
||||||
|
<module>yudao-example</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<name>${project.artifactId}</name>
|
<name>${project.artifactId}</name>
|
||||||
@ -35,8 +37,8 @@
|
|||||||
<maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
|
<maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
|
||||||
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
|
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
|
||||||
<!-- 看看咋放到 bom 里 -->
|
<!-- 看看咋放到 bom 里 -->
|
||||||
<lombok.version>1.18.20</lombok.version>
|
<lombok.version>1.18.24</lombok.version>
|
||||||
<mapstruct.version>1.4.1.Final</mapstruct.version>
|
<mapstruct.version>1.5.2.Final</mapstruct.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
@ -16,57 +16,58 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<revision>1.6.4-snapshot</revision>
|
<revision>1.6.4-snapshot</revision>
|
||||||
<!-- 统一依赖管理 -->
|
<!-- 统一依赖管理 -->
|
||||||
<spring.boot.version>2.6.10</spring.boot.version>
|
<spring.boot.version>2.6.12</spring.boot.version>
|
||||||
<!-- Web 相关 -->
|
<!-- Web 相关 -->
|
||||||
<knife4j.version>3.0.3</knife4j.version>
|
<knife4j.version>3.0.3</knife4j.version>
|
||||||
<swagger-annotations.version>1.6.6</swagger-annotations.version>
|
<swagger-annotations.version>1.6.7</swagger-annotations.version>
|
||||||
<servlet.versoin>2.5</servlet.versoin>
|
<servlet.versoin>2.5</servlet.versoin>
|
||||||
<!-- DB 相关 -->
|
<!-- DB 相关 -->
|
||||||
<druid.version>1.2.11</druid.version>
|
<druid.version>1.2.13-SNSAPSHOT</druid.version>
|
||||||
<mybatis-plus.version>3.5.2</mybatis-plus.version>
|
<mybatis-plus.version>3.5.2</mybatis-plus.version>
|
||||||
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
|
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
|
||||||
<dynamic-datasource.version>3.5.0</dynamic-datasource.version>
|
<dynamic-datasource.version>3.5.2</dynamic-datasource.version>
|
||||||
<redisson.version>3.17.4</redisson.version>
|
<redisson.version>3.17.7</redisson.version>
|
||||||
<!-- Config 配置中心相关 -->
|
<!-- Config 配置中心相关 -->
|
||||||
<apollo.version>1.9.2</apollo.version>
|
<apollo.version>2.0.1</apollo.version>
|
||||||
<!-- 服务保障相关 -->
|
<!-- 服务保障相关 -->
|
||||||
<lock4j.version>2.2.0</lock4j.version>
|
<lock4j.version>2.2.2</lock4j.version>
|
||||||
<resilience4j.version>1.7.1</resilience4j.version>
|
<resilience4j.version>1.7.1</resilience4j.version>
|
||||||
<!-- 监控相关 -->
|
<!-- 监控相关 -->
|
||||||
<skywalking.version>8.7.0</skywalking.version>
|
<skywalking.version>8.12.0</skywalking.version>
|
||||||
<spring-boot-admin.version>2.6.7</spring-boot-admin.version>
|
<spring-boot-admin.version>2.6.9</spring-boot-admin.version>
|
||||||
<opentracing.version>0.31.0</opentracing.version>
|
<opentracing.version>0.33.0</opentracing.version>
|
||||||
<!-- Test 测试相关 -->
|
<!-- Test 测试相关 -->
|
||||||
<podam.version>7.2.6.RELEASE</podam.version>
|
<podam.version>7.2.9.RELEASE</podam.version>
|
||||||
<jedis-mock.version>0.1.16</jedis-mock.version>
|
<jedis-mock.version>1.0.4</jedis-mock.version>
|
||||||
<mockito-inline.version>4.0.0</mockito-inline.version>
|
<mockito-inline.version>4.8.0</mockito-inline.version>
|
||||||
<!-- Bpm 工作流相关 -->
|
<!-- Bpm 工作流相关 -->
|
||||||
<flowable.version>6.7.2</flowable.version>
|
<flowable.version>6.7.2</flowable.version>
|
||||||
<!-- 工具类相关 -->
|
<!-- 工具类相关 -->
|
||||||
<jasypt-spring-boot-starter.version>3.0.4</jasypt-spring-boot-starter.version>
|
<jasypt-spring-boot-starter.version>3.0.4</jasypt-spring-boot-starter.version>
|
||||||
<lombok.version>1.18.20</lombok.version>
|
<lombok.version>1.18.24</lombok.version>
|
||||||
<mapstruct.version>1.4.1.Final</mapstruct.version>
|
<mapstruct.version>1.5.2.Final</mapstruct.version>
|
||||||
<hutool.version>5.8.5</hutool.version>
|
<hutool.version>5.8.8</hutool.version>
|
||||||
<easyexcel.verion>3.1.1</easyexcel.verion>
|
<easyexcel.verion>3.1.1</easyexcel.verion>
|
||||||
<velocity.version>2.3</velocity.version>
|
<velocity.version>2.3</velocity.version>
|
||||||
<screw.version>1.0.5</screw.version>
|
<screw.version>1.0.5</screw.version>
|
||||||
<fastjson.version>1.2.83</fastjson.version>
|
<fastjson.version>1.2.83</fastjson.version>
|
||||||
<guava.version>30.1.1-jre</guava.version>
|
<guava.version>31.1-jre</guava.version>
|
||||||
<guice.version>5.1.0</guice.version>
|
<guice.version>5.1.0</guice.version>
|
||||||
<transmittable-thread-local.version>2.12.2</transmittable-thread-local.version>
|
<transmittable-thread-local.version>2.14.0</transmittable-thread-local.version>
|
||||||
<commons-net.version>3.8.0</commons-net.version>
|
<commons-net.version>3.8.0</commons-net.version>
|
||||||
<jsch.version>0.1.55</jsch.version>
|
<jsch.version>0.1.55</jsch.version>
|
||||||
<tika-core.version>2.4.1</tika-core.version>
|
<tika-core.version>2.5.0</tika-core.version>
|
||||||
<aj-captcha.version>1.3.0</aj-captcha.version>
|
<aj-captcha.version>1.3.0</aj-captcha.version>
|
||||||
|
<netty-all.version>4.1.82.Final</netty-all.version>
|
||||||
<!-- 三方云服务相关 -->
|
<!-- 三方云服务相关 -->
|
||||||
<minio.version>8.2.2</minio.version>
|
<minio.version>8.2.2</minio.version>
|
||||||
<aliyun-java-sdk-core.version>4.6.0</aliyun-java-sdk-core.version>
|
<aliyun-java-sdk-core.version>4.6.2</aliyun-java-sdk-core.version>
|
||||||
<aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
|
<aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
|
||||||
<tencentcloud-sdk-java.version>3.1.561</tencentcloud-sdk-java.version>
|
<tencentcloud-sdk-java.version>3.1.561</tencentcloud-sdk-java.version>
|
||||||
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
|
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
|
||||||
<justauth.version>1.4.0</justauth.version>
|
<justauth.version>1.4.0</justauth.version>
|
||||||
<jimureport.version>1.5.2</jimureport.version>
|
<jimureport.version>1.5.3</jimureport.version>
|
||||||
<xercesImpl.version>2.12.0</xercesImpl.version>
|
<xercesImpl.version>2.12.2</xercesImpl.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@ -521,6 +522,12 @@
|
|||||||
<version>${jsch.version}</version>
|
<version>${jsch.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.netty</groupId>
|
||||||
|
<artifactId>netty-all</artifactId>
|
||||||
|
<version>${netty-all.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 三方云服务相关 -->
|
<!-- 三方云服务相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
22
yudao-example/pom.xml
Normal file
22
yudao-example/pom.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent -->
|
||||||
|
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-example</artifactId>
|
||||||
|
<version>1.0.0-snapshot</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<modules>
|
||||||
|
<module>yudao-sso-demo-by-code</module>
|
||||||
|
<module>yudao-sso-demo-by-password</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<name>${project.artifactId}</name>
|
||||||
|
<description>提供各种示例,例如说:SSO 单点登录</description>
|
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||||
|
|
||||||
|
</project>
|
65
yudao-example/yudao-sso-demo-by-code/pom.xml
Normal file
65
yudao-example/yudao-sso-demo-by-code/pom.xml
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent -->
|
||||||
|
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-sso-demo-by-code</artifactId>
|
||||||
|
<version>1.0.0-snapshot</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>${project.artifactId}</name>
|
||||||
|
<description>基于授权码模式,如何实现 SSO 单点登录?</description>
|
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<!-- Maven 相关 -->
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<!-- 统一依赖管理 -->
|
||||||
|
<spring.boot.version>2.6.10</spring.boot.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<!-- 统一依赖管理 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Web 相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
<version>5.8.8</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<OAuth2AccessTokenRespDTO> 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<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("grant_type", "authorization_code");
|
||||||
|
body.add("code", code);
|
||||||
|
body.add("redirect_uri", redirectUri);
|
||||||
|
// body.add("state", ""); // 选填;填了会校验
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验访问令牌,并返回它的基本信息
|
||||||
|
*
|
||||||
|
* @param token 访问令牌
|
||||||
|
* @return 访问令牌的基本信息
|
||||||
|
*/
|
||||||
|
public CommonResult<OAuth2CheckTokenRespDTO> 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<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("token", token);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/check-token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用刷新令牌,获得(刷新)访问令牌
|
||||||
|
*
|
||||||
|
* @param refreshToken 刷新令牌
|
||||||
|
* @return 访问令牌
|
||||||
|
*/
|
||||||
|
public CommonResult<OAuth2AccessTokenRespDTO> 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<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("grant_type", "refresh_token");
|
||||||
|
body.add("refresh_token", refreshToken);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除访问令牌
|
||||||
|
*
|
||||||
|
* @param token 访问令牌
|
||||||
|
* @return 成功
|
||||||
|
*/
|
||||||
|
public CommonResult<Boolean> 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<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("token", token);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.DELETE,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<UserInfoRespDTO> 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<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/get",
|
||||||
|
HttpMethod.GET,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommonResult<Boolean> 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<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/update",
|
||||||
|
HttpMethod.PUT,
|
||||||
|
new HttpEntity<>(updateReqDTO, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 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());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.client.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用返回
|
||||||
|
*
|
||||||
|
* @param <T> 数据泛型
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CommonResult<T> implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误码
|
||||||
|
*/
|
||||||
|
private Integer code;
|
||||||
|
/**
|
||||||
|
* 返回数据
|
||||||
|
*/
|
||||||
|
private T data;
|
||||||
|
/**
|
||||||
|
* 错误提示,用户可阅读
|
||||||
|
*/
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -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<String> scopes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌
|
||||||
|
*/
|
||||||
|
@JsonProperty("access_token")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间
|
||||||
|
*
|
||||||
|
* 时间戳 / 1000,即单位:秒
|
||||||
|
*/
|
||||||
|
private Long exp;
|
||||||
|
|
||||||
|
}
|
@ -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<Post> posts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class Dept {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class Post {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -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<OAuth2AccessTokenRespDTO> loginByCode(@RequestParam("code") String code,
|
||||||
|
@RequestParam("redirectUri") String redirectUri) {
|
||||||
|
return oauth2Client.postAccessToken(code, redirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用刷新令牌,获得(刷新)访问令牌
|
||||||
|
*
|
||||||
|
* @param refreshToken 刷新令牌
|
||||||
|
* @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段
|
||||||
|
*/
|
||||||
|
@PostMapping("/refresh-token")
|
||||||
|
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
|
||||||
|
return oauth2Client.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*
|
||||||
|
* @param request 请求
|
||||||
|
* @return 成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public CommonResult<Boolean> logout(HttpServletRequest request) {
|
||||||
|
String token = SecurityUtils.obtainAuthorization(request, "Authorization");
|
||||||
|
if (StrUtil.isNotBlank(token)) {
|
||||||
|
return oauth2Client.revokeToken(token);
|
||||||
|
}
|
||||||
|
// 返回成功
|
||||||
|
return new CommonResult<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<UserInfoRespDTO> getUser() {
|
||||||
|
return userClient.getUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新当前登录用户的昵称
|
||||||
|
*
|
||||||
|
* @param nickname 昵称
|
||||||
|
* @return 成功
|
||||||
|
*/
|
||||||
|
@PutMapping("/update")
|
||||||
|
public CommonResult<Boolean> updateUser(@RequestParam("nickname") String nickname) {
|
||||||
|
UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null);
|
||||||
|
return userClient.updateUser(updateReqDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<String> scopes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌
|
||||||
|
*/
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
}
|
@ -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<OAuth2CheckTokenRespDTO> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<Object> result = new CommonResult<>();
|
||||||
|
result.setCode(HttpStatus.FORBIDDEN.value());
|
||||||
|
result.setMsg("没有该操作权限");
|
||||||
|
ServletUtils.writeJSON(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<Object> result = new CommonResult<>();
|
||||||
|
result.setCode(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
result.setMsg("账号未登录");
|
||||||
|
ServletUtils.writeJSON(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
server:
|
||||||
|
port: 18080
|
@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>SSO 授权后的回调页</title>
|
||||||
|
<!-- jQuery:操作 dom、发起请求等 -->
|
||||||
|
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
|
||||||
|
<!-- 工具类 -->
|
||||||
|
<script type="application/javascript">
|
||||||
|
(function ($) {
|
||||||
|
/**
|
||||||
|
* 获得 URL 的指定参数的值
|
||||||
|
*
|
||||||
|
* @param name 参数名
|
||||||
|
* @returns 参数值
|
||||||
|
*/
|
||||||
|
$.getUrlParam = function (name) {
|
||||||
|
const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||||
|
const r = window.location.search.substr(1).match(reg);
|
||||||
|
if (r != null) return unescape(r[2]); return null;
|
||||||
|
}
|
||||||
|
})(jQuery);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
$(function () {
|
||||||
|
// 获得 code 授权码
|
||||||
|
const code = $.getUrlParam('code');
|
||||||
|
if (!code) {
|
||||||
|
alert('获取不到 code 参数,请排查!')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
const redirectUri = 'http://127.0.0.1:18080/callback.html'; // 需要修改成,你回调的地址,就是在 index.html 拼接的 redirectUri
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:18080/auth/login-by-code?code=" + code
|
||||||
|
+ '&redirectUri=' + redirectUri,
|
||||||
|
method: 'POST',
|
||||||
|
success: function( result ) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('获得访问令牌失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert('获得访问令牌成功!点击确认,跳转回首页')
|
||||||
|
|
||||||
|
// 设置到 localStorage 中
|
||||||
|
localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
|
||||||
|
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
|
||||||
|
|
||||||
|
// 跳转回首页
|
||||||
|
window.location.href = '/index.html';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
正在使用 code 授权码,进行 accessToken 访问令牌的获取
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,159 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>首页</title>
|
||||||
|
<!-- jQuery:操作 dom、发起请求等 -->
|
||||||
|
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转单点登录
|
||||||
|
*/
|
||||||
|
function ssoLogin() {
|
||||||
|
const clientId = 'yudao-sso-demo-by-code'; // 可以改写成,你的 clientId
|
||||||
|
const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址
|
||||||
|
const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token
|
||||||
|
window.location.href = 'http://127.0.0.1:1024/sso?client_id=' + clientId
|
||||||
|
+ '&redirect_uri=' + redirectUri
|
||||||
|
+ '&response_type=' + responseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改昵称
|
||||||
|
*/
|
||||||
|
function updateNickname() {
|
||||||
|
const nickname = prompt("请输入新的昵称", "");
|
||||||
|
if (!nickname) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 更新用户的昵称
|
||||||
|
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:18080/user/update?nickname=" + nickname,
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + accessToken
|
||||||
|
},
|
||||||
|
success: function (result) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('更新昵称失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert('更新昵称成功!');
|
||||||
|
$('#nicknameSpan').html(nickname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新令牌
|
||||||
|
*/
|
||||||
|
function refreshToken() {
|
||||||
|
const refreshToken = localStorage.getItem('REFRESH-TOKEN');
|
||||||
|
if (!refreshToken) {
|
||||||
|
alert("获取不到刷新令牌");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken,
|
||||||
|
method: 'POST',
|
||||||
|
success: function (result) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('刷新访问令牌失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert('更新访问令牌成功!');
|
||||||
|
$('#accessTokenSpan').html(result.data.access_token);
|
||||||
|
|
||||||
|
// 设置到 localStorage 中
|
||||||
|
localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
|
||||||
|
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出,删除访问令牌
|
||||||
|
*/
|
||||||
|
function logout() {
|
||||||
|
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||||
|
if (!accessToken) {
|
||||||
|
location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:18080/auth/logout",
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + accessToken
|
||||||
|
},
|
||||||
|
success: function (result) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('退出登录失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert('退出登录成功!');
|
||||||
|
// 删除 localStorage 中
|
||||||
|
localStorage.removeItem('ACCESS-TOKEN');
|
||||||
|
localStorage.removeItem('REFRESH-TOKEN');
|
||||||
|
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||||
|
// 情况一:未登录
|
||||||
|
if (!accessToken) {
|
||||||
|
$('#noLoginDiv').css("display", "block");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况二:已登录
|
||||||
|
$('#yesLoginDiv').css("display", "block");
|
||||||
|
$('#accessTokenSpan').html(accessToken);
|
||||||
|
// 获得登录用户的信息
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:18080/user/get",
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + accessToken
|
||||||
|
},
|
||||||
|
success: function (result) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('获得个人信息失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$('#nicknameSpan').html(result.data.nickname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
|
||||||
|
<div id="noLoginDiv" style="display: none">
|
||||||
|
您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
|
||||||
|
<div id="yesLoginDiv" style="display: none">
|
||||||
|
您已登录!<button onclick="logout()">退出登录</button> <br />
|
||||||
|
昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br />
|
||||||
|
访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br />
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
body { /** 页面居中 */
|
||||||
|
border-radius: 20px;
|
||||||
|
height: 350px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</html>
|
65
yudao-example/yudao-sso-demo-by-password/pom.xml
Normal file
65
yudao-example/yudao-sso-demo-by-password/pom.xml
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent -->
|
||||||
|
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-sso-demo-by-password</artifactId>
|
||||||
|
<version>1.0.0-snapshot</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>${project.artifactId}</name>
|
||||||
|
<description>基于密码模式,如何实现 SSO 单点登录?</description>
|
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<!-- Maven 相关 -->
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<!-- 统一依赖管理 -->
|
||||||
|
<spring.boot.version>2.6.10</spring.boot.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<!-- 统一依赖管理 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Web 相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
<version>5.8.8</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<OAuth2CheckTokenRespDTO> 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<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("token", token);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/check-token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用刷新令牌,获得(刷新)访问令牌
|
||||||
|
*
|
||||||
|
* @param refreshToken 刷新令牌
|
||||||
|
* @return 访问令牌
|
||||||
|
*/
|
||||||
|
public CommonResult<OAuth2AccessTokenRespDTO> 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<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("grant_type", "refresh_token");
|
||||||
|
body.add("refresh_token", refreshToken);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除访问令牌
|
||||||
|
*
|
||||||
|
* @param token 访问令牌
|
||||||
|
* @return 成功
|
||||||
|
*/
|
||||||
|
public CommonResult<Boolean> 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<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("token", token);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.DELETE,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<UserInfoRespDTO> 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<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/get",
|
||||||
|
HttpMethod.GET,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommonResult<Boolean> 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<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/update",
|
||||||
|
HttpMethod.PUT,
|
||||||
|
new HttpEntity<>(updateReqDTO, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 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());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.client.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用返回
|
||||||
|
*
|
||||||
|
* @param <T> 数据泛型
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CommonResult<T> implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误码
|
||||||
|
*/
|
||||||
|
private Integer code;
|
||||||
|
/**
|
||||||
|
* 返回数据
|
||||||
|
*/
|
||||||
|
private T data;
|
||||||
|
/**
|
||||||
|
* 错误提示,用户可阅读
|
||||||
|
*/
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -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<String> scopes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌
|
||||||
|
*/
|
||||||
|
@JsonProperty("access_token")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间
|
||||||
|
*
|
||||||
|
* 时间戳 / 1000,即单位:秒
|
||||||
|
*/
|
||||||
|
private Long exp;
|
||||||
|
|
||||||
|
}
|
@ -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<Post> posts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class Dept {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class Post {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -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<OAuth2AccessTokenRespDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
|
||||||
|
return oauth2Client.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*
|
||||||
|
* @param request 请求
|
||||||
|
* @return 成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public CommonResult<Boolean> logout(HttpServletRequest request) {
|
||||||
|
String token = SecurityUtils.obtainAuthorization(request, "Authorization");
|
||||||
|
if (StrUtil.isNotBlank(token)) {
|
||||||
|
return oauth2Client.revokeToken(token);
|
||||||
|
}
|
||||||
|
// 返回成功
|
||||||
|
return new CommonResult<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<UserInfoRespDTO> getUser() {
|
||||||
|
return userClient.getUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新当前登录用户的昵称
|
||||||
|
*
|
||||||
|
* @param nickname 昵称
|
||||||
|
* @return 成功
|
||||||
|
*/
|
||||||
|
@PutMapping("/update")
|
||||||
|
public CommonResult<Boolean> updateUser(@RequestParam("nickname") String nickname) {
|
||||||
|
UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null);
|
||||||
|
return userClient.updateUser(updateReqDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<String> scopes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌
|
||||||
|
*/
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
}
|
@ -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<OAuth2CheckTokenRespDTO> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<Object> result = new CommonResult<>();
|
||||||
|
result.setCode(HttpStatus.FORBIDDEN.value());
|
||||||
|
result.setMsg("没有该操作权限");
|
||||||
|
ServletUtils.writeJSON(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<Object> result = new CommonResult<>();
|
||||||
|
result.setCode(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
result.setMsg("账号未登录");
|
||||||
|
ServletUtils.writeJSON(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
server:
|
||||||
|
port: 18080
|
@ -0,0 +1,154 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>首页</title>
|
||||||
|
<!-- jQuery:操作 dom、发起请求等 -->
|
||||||
|
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转单点登录
|
||||||
|
*/
|
||||||
|
function passwordLogin() {
|
||||||
|
window.location.href = '/login.html'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改昵称
|
||||||
|
*/
|
||||||
|
function updateNickname() {
|
||||||
|
const nickname = prompt("请输入新的昵称", "");
|
||||||
|
if (!nickname) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 更新用户的昵称
|
||||||
|
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:18080/user/update?nickname=" + nickname,
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + accessToken
|
||||||
|
},
|
||||||
|
success: function (result) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('更新昵称失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert('更新昵称成功!');
|
||||||
|
$('#nicknameSpan').html(nickname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新令牌
|
||||||
|
*/
|
||||||
|
function refreshToken() {
|
||||||
|
const refreshToken = localStorage.getItem('REFRESH-TOKEN');
|
||||||
|
if (!refreshToken) {
|
||||||
|
alert("获取不到刷新令牌");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken,
|
||||||
|
method: 'POST',
|
||||||
|
success: function (result) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('刷新访问令牌失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert('更新访问令牌成功!');
|
||||||
|
$('#accessTokenSpan').html(result.data.access_token);
|
||||||
|
|
||||||
|
// 设置到 localStorage 中
|
||||||
|
localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
|
||||||
|
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出,删除访问令牌
|
||||||
|
*/
|
||||||
|
function logout() {
|
||||||
|
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||||
|
if (!accessToken) {
|
||||||
|
location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:18080/auth/logout",
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + accessToken
|
||||||
|
},
|
||||||
|
success: function (result) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('退出登录失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert('退出登录成功!');
|
||||||
|
// 删除 localStorage 中
|
||||||
|
localStorage.removeItem('ACCESS-TOKEN');
|
||||||
|
localStorage.removeItem('REFRESH-TOKEN');
|
||||||
|
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||||
|
// 情况一:未登录
|
||||||
|
if (!accessToken) {
|
||||||
|
$('#noLoginDiv').css("display", "block");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况二:已登录
|
||||||
|
$('#yesLoginDiv').css("display", "block");
|
||||||
|
$('#accessTokenSpan').html(accessToken);
|
||||||
|
// 获得登录用户的信息
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:18080/user/get",
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + accessToken
|
||||||
|
},
|
||||||
|
success: function (result) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('获得个人信息失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$('#nicknameSpan').html(result.data.nickname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
|
||||||
|
<div id="noLoginDiv" style="display: none">
|
||||||
|
您未登录,点击 <a href="#" onclick="passwordLogin()">跳转 </a> 账号密码登录
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
|
||||||
|
<div id="yesLoginDiv" style="display: none">
|
||||||
|
您已登录!<button onclick="logout()">退出登录</button> <br />
|
||||||
|
昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br />
|
||||||
|
访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br />
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
body { /** 页面居中 */
|
||||||
|
border-radius: 20px;
|
||||||
|
height: 350px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</html>
|
@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>登录</title>
|
||||||
|
<!-- jQuery:操作 dom、发起请求等 -->
|
||||||
|
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号密码登录
|
||||||
|
*/
|
||||||
|
function login() {
|
||||||
|
const clientId = 'yudao-sso-demo-by-password'; // 可以改写成,你的 clientId
|
||||||
|
const clientSecret = 'test'; // 可以改写成,你的 clientSecret
|
||||||
|
const grantType = 'password'; // 密码模式
|
||||||
|
|
||||||
|
// 账号 + 密码
|
||||||
|
const username = $('#username').val();
|
||||||
|
const password = $('#password').val();
|
||||||
|
if (username.length === 0 || password.length === 0) {
|
||||||
|
alert('账号或密码未输入');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:48080/admin-api/system/oauth2/token?"
|
||||||
|
// 客户端
|
||||||
|
+ "client_id=" + clientId
|
||||||
|
+ "&client_secret=" + clientSecret
|
||||||
|
// 密码模式的参数
|
||||||
|
+ "&grant_type=" + grantType
|
||||||
|
+ "&username=" + username
|
||||||
|
+ "&password=" + password
|
||||||
|
+ '&scope=user.read user.write',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'tenant-id': '1', // 多租户编号,写死
|
||||||
|
},
|
||||||
|
success: function (result) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('登录失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 设置到 localStorage 中
|
||||||
|
localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
|
||||||
|
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
|
||||||
|
|
||||||
|
// 提示登录成功
|
||||||
|
alert('登录成功!点击确认,跳转回首页');
|
||||||
|
window.location.href = '/index.html';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
账号:<input id="username" value="admin" /> <br />
|
||||||
|
密码:<input id="password" value="admin123" > <br />
|
||||||
|
<button style="float: right; margin-top: 5px;" onclick="login()">登录</button>
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
body { /** 页面居中 */
|
||||||
|
border-radius: 20px;
|
||||||
|
height: 350px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</html>
|
@ -52,7 +52,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alipay.sdk</groupId>
|
<groupId>com.alipay.sdk</groupId>
|
||||||
<artifactId>alipay-sdk-java</artifactId>
|
<artifactId>alipay-sdk-java</artifactId>
|
||||||
<version>4.31.72.ALL</version>
|
<version>4.33.44.ALL</version>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>org.bouncycastle</groupId>
|
<groupId>org.bouncycastle</groupId>
|
||||||
@ -63,7 +63,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.binarywang</groupId>
|
<groupId>com.github.binarywang</groupId>
|
||||||
<artifactId>weixin-java-pay</artifactId>
|
<artifactId>weixin-java-pay</artifactId>
|
||||||
<version>4.3.8.B</version>
|
<version>4.4.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- TODO 芋艿:清理 -->
|
<!-- TODO 芋艿:清理 -->
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
|
|||||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator;
|
import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.mq.TenantRedisMessageInterceptor;
|
import cn.iocoder.yudao.framework.tenant.core.mq.TenantRedisMessageInterceptor;
|
||||||
|
import cn.iocoder.yudao.framework.tenant.core.redis.TenantRedisCacheManager;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
|
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
|
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
|
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
|
||||||
@ -25,6 +26,14 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
|||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheWriter;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
|
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
|
||||||
@ -110,4 +119,15 @@ public class YudaoTenantAutoConfiguration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary // 引入租户时,tenantRedisCacheManager 为主 Bean
|
||||||
|
public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate,
|
||||||
|
RedisCacheConfiguration redisCacheConfiguration) {
|
||||||
|
// 创建 RedisCacheWriter 对象
|
||||||
|
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
|
||||||
|
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
|
||||||
|
// 创建 TenantRedisCacheManager 对象
|
||||||
|
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
package cn.iocoder.yudao.framework.tenant.core.redis;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.cache.Cache;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多租户的 {@link RedisCacheManager} 实现类
|
||||||
|
*
|
||||||
|
* 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId
|
||||||
|
*
|
||||||
|
* @author airhead
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class TenantRedisCacheManager extends RedisCacheManager {
|
||||||
|
|
||||||
|
public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
|
||||||
|
RedisCacheConfiguration defaultCacheConfiguration) {
|
||||||
|
super(cacheWriter, defaultCacheConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cache getCache(@NotNull String name) {
|
||||||
|
// 如果开启多租户,则 name 拼接租户后缀
|
||||||
|
if (!TenantContextHolder.isIgnore()
|
||||||
|
&& TenantContextHolder.getTenantId() != null) {
|
||||||
|
name = name + ":" + TenantContextHolder.getTenantId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续基于父方法
|
||||||
|
return super.getCache(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -35,12 +35,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.binarywang</groupId>
|
<groupId>com.github.binarywang</groupId>
|
||||||
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
|
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
|
||||||
<version>4.3.8.B</version>
|
<version>4.4.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.binarywang</groupId>
|
<groupId>com.github.binarywang</groupId>
|
||||||
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
|
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
|
||||||
<version>4.3.8.B</version>
|
<version>4.4.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- TODO 芋艿:清理 -->
|
<!-- TODO 芋艿:清理 -->
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -5,6 +5,8 @@ import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstant
|
|||||||
import com.baomidou.lock.LockFailureStrategy;
|
import com.baomidou.lock.LockFailureStrategy;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义获取锁失败策略,抛出 {@link ServiceException} 异常
|
* 自定义获取锁失败策略,抛出 {@link ServiceException} 异常
|
||||||
*/
|
*/
|
||||||
@ -12,9 +14,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
public class DefaultLockFailureStrategy implements LockFailureStrategy {
|
public class DefaultLockFailureStrategy implements LockFailureStrategy {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLockFailure(String key, long acquireTimeout, int acquireCount) {
|
public void onLockFailure(String key, Method method, Object[] arguments) {
|
||||||
log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取超时时长:{} ms]", Thread.currentThread().getName(), key, acquireTimeout);
|
log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取失败:{} ]", Thread.currentThread().getName(), key, arguments);
|
||||||
throw new ServiceException(GlobalErrorCodeConstants.LOCKED);
|
throw new ServiceException(GlobalErrorCodeConstants.LOCKED);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,11 @@
|
|||||||
<artifactId>spring-boot-starter-cache</artifactId> <!-- 实现对 Caches 的自动化配置 -->
|
<artifactId>spring-boot-starter-cache</artifactId> <!-- 实现对 Caches 的自动化配置 -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.netty</groupId>
|
||||||
|
<artifactId>netty-all</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package cn.iocoder.yudao.framework.redis.config;
|
package cn.iocoder.yudao.framework.redis.config;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.cache.CacheProperties;
|
import org.springframework.boot.autoconfigure.cache.CacheProperties;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@ -13,6 +14,7 @@ import org.springframework.data.redis.serializer.RedisSerializer;
|
|||||||
* Cache 配置类,基于 Redis 实现
|
* Cache 配置类,基于 Redis 实现
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@EnableConfigurationProperties({CacheProperties.class})
|
||||||
@EnableCaching
|
@EnableCaching
|
||||||
public class YudaoCacheAutoConfiguration {
|
public class YudaoCacheAutoConfiguration {
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.convert.message;
|
|||||||
|
|
||||||
import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
|
import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
import org.mapstruct.factory.Mappers;
|
import org.mapstruct.factory.Mappers;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -11,6 +12,10 @@ public interface BpmMessageConvert {
|
|||||||
|
|
||||||
BpmMessageConvert INSTANCE = Mappers.getMapper(BpmMessageConvert.class);
|
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<String, Object> templateParams);
|
SmsSendSingleToUserReqDTO convert(Long userId, String templateCode, Map<String, Object> templateParams);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ export type ${simpleClassName}VO = {
|
|||||||
#if ($column.createOperation || $column.updateOperation)
|
#if ($column.createOperation || $column.updateOperation)
|
||||||
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer")
|
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer")
|
||||||
${column.javaField}: number
|
${column.javaField}: number
|
||||||
|
#elseif(${column.javaType.toLowerCase()} == "date")
|
||||||
|
${column.javaField}: string
|
||||||
#else
|
#else
|
||||||
${column.javaField}: ${column.javaType.toLowerCase()}
|
${column.javaField}: ${column.javaType.toLowerCase()}
|
||||||
#end
|
#end
|
||||||
@ -15,6 +17,8 @@ export type ${simpleClassName}PageReqVO = {
|
|||||||
#if (${column.listOperation})##查询操作
|
#if (${column.listOperation})##查询操作
|
||||||
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer")
|
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer")
|
||||||
${column.javaField}: number
|
${column.javaField}: number
|
||||||
|
#elseif(${column.javaType.toLowerCase()} == "date")
|
||||||
|
${column.javaField}: string
|
||||||
#else
|
#else
|
||||||
${column.javaField}: ${column.javaType.toLowerCase()}
|
${column.javaField}: ${column.javaType.toLowerCase()}
|
||||||
#end
|
#end
|
||||||
@ -27,6 +31,8 @@ export type ${simpleClassName}ExcelReqVO = {
|
|||||||
#if (${column.listOperation})##查询操作
|
#if (${column.listOperation})##查询操作
|
||||||
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer")
|
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer")
|
||||||
${column.javaField}: number
|
${column.javaField}: number
|
||||||
|
#elseif(${column.javaType.toLowerCase()} == "date")
|
||||||
|
${column.javaField}: string
|
||||||
#else
|
#else
|
||||||
${column.javaField}: ${column.javaType.toLowerCase()}
|
${column.javaField}: ${column.javaType.toLowerCase()}
|
||||||
#end
|
#end
|
||||||
|
@ -8,7 +8,7 @@ export const rules = reactive({
|
|||||||
#foreach ($column in $columns)
|
#foreach ($column in $columns)
|
||||||
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
|
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
|
||||||
#set($comment=$column.columnComment)
|
#set($comment=$column.columnComment)
|
||||||
$column.javaField: [{ required: true, message: "${comment}不能为空", trigger: #if($column.htmlType == "select")"change"#else"blur"#end }],
|
$column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == "select")'change'#else'blur'#end }],
|
||||||
#end
|
#end
|
||||||
#end
|
#end
|
||||||
})
|
})
|
||||||
@ -34,7 +34,7 @@ const crudSchemas = reactive<CrudSchema[]>([
|
|||||||
#else
|
#else
|
||||||
#if (!$column.createOperation && !$column.updateOperation)
|
#if (!$column.createOperation && !$column.updateOperation)
|
||||||
form: {
|
form: {
|
||||||
false
|
show: false
|
||||||
},
|
},
|
||||||
#elseif(!("" != $column.dictType))
|
#elseif(!("" != $column.dictType))
|
||||||
form: {
|
form: {
|
||||||
|
@ -50,6 +50,10 @@
|
|||||||
|
|
||||||
// 提交按钮
|
// 提交按钮
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
|
const elForm = unref(formRef)?.getElFormRef()
|
||||||
|
if (!elForm) return
|
||||||
|
elForm.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
actionLoading.value = true
|
actionLoading.value = true
|
||||||
// 提交请求
|
// 提交请求
|
||||||
try {
|
try {
|
||||||
@ -68,6 +72,8 @@
|
|||||||
actionLoading.value = false
|
actionLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 详情相关 ==========
|
// ========== 详情相关 ==========
|
||||||
const detailRef = ref() // 详情 Ref
|
const detailRef = ref() // 详情 Ref
|
||||||
|
@ -30,10 +30,6 @@
|
|||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
|
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
|
||||||
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Web 相关 -->
|
<!-- Web 相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -42,7 +38,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- DB 相关 -->
|
<!-- DB 相关 -->
|
||||||
@ -57,6 +53,12 @@
|
|||||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 工具类相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
@ -4,7 +4,6 @@ import cn.hutool.core.bean.BeanUtil;
|
|||||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
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.dataobject.CouponTemplete.CouponTempleteDO;
|
||||||
import cn.iocoder.yudao.module.coupon.dal.mysql.CouponTemplete.CouponTempleteMapper;
|
import cn.iocoder.yudao.module.coupon.dal.mysql.CouponTemplete.CouponTempleteMapper;
|
||||||
import io.micrometer.core.instrument.Counter;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
@ -34,16 +34,16 @@
|
|||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
|
<artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
|
||||||
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Web 相关 -->
|
<!-- Web 相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-web</artifactId>
|
<artifactId>yudao-spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- DB 相关 -->
|
<!-- DB 相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -56,6 +56,12 @@
|
|||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 工具类相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -7,13 +7,13 @@ import cn.iocoder.yudao.module.market.dal.dataobject.banner.BannerDO;
|
|||||||
import cn.iocoder.yudao.module.market.service.banner.BannerService;
|
import cn.iocoder.yudao.module.market.service.banner.BannerService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
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
|
@Validated
|
||||||
public class AppBannerController {
|
public class AppBannerController {
|
||||||
|
|
||||||
// TODO @xia:使用 @Resource 哈
|
@Resource
|
||||||
@Autowired
|
|
||||||
private BannerService bannerService;
|
private BannerService bannerService;
|
||||||
|
|
||||||
// TODO @xia:新建一个 AppBannerRespVO,只返回必要的字段。status 要过滤下。然后 sort 下结果
|
// TODO @xia:新建一个 AppBannerRespVO,只返回必要的字段。status 要过滤下。然后 sort 下结果
|
||||||
|
@ -29,10 +29,6 @@
|
|||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
|
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
|
||||||
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Web 相关 -->
|
<!-- Web 相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -41,7 +37,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- DB 相关 -->
|
<!-- DB 相关 -->
|
||||||
@ -56,6 +52,11 @@
|
|||||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 工具类相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -47,10 +47,6 @@
|
|||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
|
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
|
||||||
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Web 相关 -->
|
<!-- Web 相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -59,7 +55,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- DB 相关 -->
|
<!-- DB 相关 -->
|
||||||
@ -73,6 +69,12 @@
|
|||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 工具类相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.validation.Mobile;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.validation.constraints.NotEmpty;
|
import javax.validation.constraints.NotEmpty;
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -106,7 +106,7 @@ public class OAuth2OpenController {
|
|||||||
@RequestParam(value = "scope", required = false) String scope, // 密码模式
|
@RequestParam(value = "scope", required = false) String scope, // 密码模式
|
||||||
@RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式
|
@RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式
|
||||||
List<String> scopes = OAuth2Utils.buildScopes(scope);
|
List<String> scopes = OAuth2Utils.buildScopes(scope);
|
||||||
// 授权类型
|
// 1.1 校验授权类型
|
||||||
OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType);
|
OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType);
|
||||||
if (grantTypeEnum == null) {
|
if (grantTypeEnum == null) {
|
||||||
throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType));
|
throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType));
|
||||||
@ -115,12 +115,12 @@ public class OAuth2OpenController {
|
|||||||
throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式");
|
throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验客户端
|
// 1.2 校验客户端
|
||||||
String[] clientIdAndSecret = obtainBasicAuthorization(request);
|
String[] clientIdAndSecret = obtainBasicAuthorization(request);
|
||||||
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
|
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
|
||||||
grantType, scopes, redirectUri);
|
grantType, scopes, redirectUri);
|
||||||
|
|
||||||
// 根据授权模式,获取访问令牌
|
// 2. 根据授权模式,获取访问令牌
|
||||||
OAuth2AccessTokenDO accessTokenDO;
|
OAuth2AccessTokenDO accessTokenDO;
|
||||||
switch (grantTypeEnum) {
|
switch (grantTypeEnum) {
|
||||||
case AUTHORIZATION_CODE:
|
case AUTHORIZATION_CODE:
|
||||||
|
@ -26,6 +26,7 @@ public class OAuth2OpenCheckTokenRespVO {
|
|||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
@ApiModelProperty(value = "客户端编号", required = true, example = "car")
|
@ApiModelProperty(value = "客户端编号", required = true, example = "car")
|
||||||
|
@JsonProperty("client_id")
|
||||||
private String clientId;
|
private String clientId;
|
||||||
@ApiModelProperty(value = "授权范围", required = true, example = "user_info")
|
@ApiModelProperty(value = "授权范围", required = true, example = "user_info")
|
||||||
private List<String> scopes;
|
private List<String> scopes;
|
||||||
|
@ -17,7 +17,7 @@ public class OAuth2UserInfoRespVO {
|
|||||||
@ApiModelProperty(value = "用户编号", required = true, example = "1")
|
@ApiModelProperty(value = "用户编号", required = true, example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
|
@ApiModelProperty(value = "用户账号", required = true, example = "芋艿")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@ApiModelProperty(value = "用户昵称", required = true, example = "芋道")
|
@ApiModelProperty(value = "用户昵称", required = true, example = "芋道")
|
||||||
|
@ -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.convert.permission.MenuConvert;
|
||||||
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
|
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.permission.MenuService;
|
||||||
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
|
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiImplicitParam;
|
import io.swagger.annotations.ApiImplicitParam;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
@ -29,8 +28,6 @@ public class MenuController {
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private MenuService menuService;
|
private MenuService menuService;
|
||||||
@Resource
|
|
||||||
private TenantService tenantService;
|
|
||||||
|
|
||||||
@PostMapping("/create")
|
@PostMapping("/create")
|
||||||
@ApiOperation("创建菜单")
|
@ApiOperation("创建菜单")
|
||||||
|
@ -34,7 +34,7 @@ public class TenantPackageDO extends BaseDO {
|
|||||||
*/
|
*/
|
||||||
private String name;
|
private String name;
|
||||||
/**
|
/**
|
||||||
* 租户状态
|
* 租户套餐状态
|
||||||
*
|
*
|
||||||
* 枚举 {@link CommonStatusEnum}
|
* 枚举 {@link CommonStatusEnum}
|
||||||
*/
|
*/
|
||||||
|
@ -12,7 +12,7 @@ import java.util.List;
|
|||||||
* 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。
|
* 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。
|
||||||
*
|
*
|
||||||
* 问题:为什么自身也作为一个第三方应用,也走这套流程呢?
|
* 问题:为什么自身也作为一个第三方应用,也走这套流程呢?
|
||||||
* 回复:当然可以这么做,采用 Implicit 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。
|
* 回复:当然可以这么做,采用 password 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。
|
||||||
*
|
*
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
|
@ -45,7 +45,7 @@ spring:
|
|||||||
datasource:
|
datasource:
|
||||||
master:
|
master:
|
||||||
name: ruoyi-vue-pro
|
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: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: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:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||||
@ -56,7 +56,7 @@ spring:
|
|||||||
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
|
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
|
||||||
slave: # 模拟从库,可根据自己需要修改
|
slave: # 模拟从库,可根据自己需要修改
|
||||||
name: ruoyi-vue-pro
|
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: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: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:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||||
@ -68,10 +68,10 @@ spring:
|
|||||||
|
|
||||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||||
redis:
|
redis:
|
||||||
host: 139.9.196.247 # 地址
|
host: 127.0.0.1 # 地址
|
||||||
port: 6379 # 端口
|
port: 6379 # 端口
|
||||||
database: 0 # 数据库索引
|
database: 0 # 数据库索引
|
||||||
password: 123456 # 密码,建议生产环境开启
|
# password: 123456 # 密码,建议生产环境开启
|
||||||
|
|
||||||
jasypt:
|
jasypt:
|
||||||
encryptor:
|
encryptor:
|
||||||
@ -197,7 +197,7 @@ wx:
|
|||||||
# 芋道配置项,设置当前项目所有自定义的配置
|
# 芋道配置项,设置当前项目所有自定义的配置
|
||||||
yudao:
|
yudao:
|
||||||
captcha:
|
captcha:
|
||||||
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试
|
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
|
||||||
security:
|
security:
|
||||||
mock-enable: true
|
mock-enable: true
|
||||||
xss:
|
xss:
|
||||||
|
3
yudao-ui-admin-vue3/.vscode/settings.json
vendored
3
yudao-ui-admin-vue3/.vscode/settings.json
vendored
@ -39,5 +39,6 @@
|
|||||||
"i18n-ally.sourceLanguage": "en",
|
"i18n-ally.sourceLanguage": "en",
|
||||||
"i18n-ally.displayLanguage": "zh-CN",
|
"i18n-ally.displayLanguage": "zh-CN",
|
||||||
"i18n-ally.enabledFrameworks": ["vue", "react"],
|
"i18n-ally.enabledFrameworks": ["vue", "react"],
|
||||||
"god.tsconfig": "./tsconfig.json"
|
"god.tsconfig": "./tsconfig.json",
|
||||||
|
"vue-i18n.i18nPaths": "src/locales"
|
||||||
}
|
}
|
||||||
|
@ -27,17 +27,17 @@
|
|||||||
|
|
||||||
| 框架 | 说明 | 版本 |
|
| 框架 | 说明 | 版本 |
|
||||||
| --- | --- |--------|
|
| --- | --- |--------|
|
||||||
| [Vue](https://staging-cn.vuejs.org/) | vue 框架 | 3.2.37 |
|
| [Vue](https://staging-cn.vuejs.org/) | vue 框架 | 3.2.41 |
|
||||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.0.4 |
|
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.1.8 |
|
||||||
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.12 |
|
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.18 |
|
||||||
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.7.4 |
|
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.8.4 |
|
||||||
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.17 |
|
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.23 |
|
||||||
| [vueuse](https://vueuse.org//) | 常用工具集 | 9.0.2 |
|
| [vueuse](https://vueuse.org//) | 常用工具集 | 9.3.1 |
|
||||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.0 |
|
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
|
||||||
| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.3 |
|
| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.5 |
|
||||||
| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
|
| [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 |
|
||||||
| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.14 |
|
| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.21 |
|
||||||
|
|
||||||
### 推荐 VScode 开发,插件如下
|
### 推荐 VScode 开发,插件如下
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ruoyi-vue-pro-vue3",
|
"name": "ruoyi-vue-pro-vue3",
|
||||||
"version": "1.6.3.1611",
|
"version": "1.6.4.1811",
|
||||||
"description": "基于vue3、element-plus、typesScript、vite3",
|
"description": "基于vue3、element-plus、typesScript、vite3",
|
||||||
"author": "xingyu",
|
"author": "xingyu",
|
||||||
"private": false,
|
"private": false,
|
||||||
@ -10,7 +10,7 @@
|
|||||||
"ts:check": "vue-tsc --noEmit",
|
"ts:check": "vue-tsc --noEmit",
|
||||||
"build:pro": "vite build --mode pro",
|
"build:pro": "vite build --mode pro",
|
||||||
"build:dev": "vite build --mode dev",
|
"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:pro": "vite preview --mode pro",
|
||||||
"serve:dev": "vite preview --mode dev",
|
"serve:dev": "vite preview --mode dev",
|
||||||
"serve:test": "vite preview --mode test",
|
"serve:test": "vite preview --mode test",
|
||||||
@ -25,84 +25,85 @@
|
|||||||
"p": "plop"
|
"p": "plop"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/iconify": "^2.2.1",
|
"@iconify/iconify": "^3.0.0",
|
||||||
"@vueuse/core": "^9.1.0",
|
"@vueuse/core": "^9.3.1",
|
||||||
"@wangeditor/editor": "^5.1.14",
|
"@wangeditor/editor": "^5.1.21",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.10",
|
"@wangeditor/editor-for-vue": "^5.1.10",
|
||||||
"@zxcvbn-ts/core": "^2.0.4",
|
"@zxcvbn-ts/core": "^2.0.5",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dayjs": "^1.11.4",
|
"dayjs": "^1.11.5",
|
||||||
"echarts": "^5.3.3",
|
"echarts": "^5.4.0",
|
||||||
"echarts-wordcloud": "^2.0.0",
|
"echarts-wordcloud": "^2.0.0",
|
||||||
"element-plus": "2.2.12",
|
"element-plus": "2.2.18",
|
||||||
"intro.js": "^6.0.0",
|
"intro.js": "^6.0.0",
|
||||||
|
"js-cookie": "^3.0.1",
|
||||||
"jsencrypt": "^3.2.1",
|
"jsencrypt": "^3.2.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mitt": "^3.0.0",
|
"mitt": "^3.0.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.0.17",
|
"pinia": "^2.0.23",
|
||||||
"pinia-plugin-persist": "^1.0.0",
|
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"vue": "3.2.37",
|
"vue": "3.2.41",
|
||||||
"vue-cropper": "^1.0.3",
|
"vue-cropper": "^1.0.3",
|
||||||
"vue-i18n": "9.2.2",
|
"vue-i18n": "9.2.2",
|
||||||
"vue-router": "^4.1.3",
|
"vue-router": "^4.1.5",
|
||||||
"vue-types": "^4.2.1",
|
"vue-types": "^4.2.1",
|
||||||
"web-storage-cache": "^1.1.1"
|
"web-storage-cache": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.0.3",
|
"@commitlint/cli": "^17.1.2",
|
||||||
"@commitlint/config-conventional": "^17.0.3",
|
"@commitlint/config-conventional": "^17.1.0",
|
||||||
"@iconify/json": "^2.1.89",
|
"@iconify/json": "^2.1.122",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^6.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^6.0.3",
|
||||||
"@purge-icons/generated": "^0.9.0",
|
"@purge-icons/generated": "^0.9.0",
|
||||||
"@types/intro.js": "^5.1.0",
|
"@types/intro.js": "^5.1.0",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/node": "^18.6.5",
|
"@types/node": "^18.11.0",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/qrcode": "^1.4.2",
|
"@types/qrcode": "^1.5.0",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||||
"@typescript-eslint/parser": "^5.33.0",
|
"@typescript-eslint/parser": "^5.40.0",
|
||||||
"@vitejs/plugin-vue": "^3.0.1",
|
"@vitejs/plugin-vue": "^3.1.2",
|
||||||
"@vitejs/plugin-vue-jsx": "^2.0.0",
|
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.12",
|
||||||
"eslint": "^8.21.0",
|
"eslint": "^8.25.0",
|
||||||
"eslint-config-prettier": "^8.5.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-prettier": "^4.2.1",
|
||||||
"eslint-plugin-vue": "^9.3.0",
|
"eslint-plugin-vue": "^9.6.0",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"plop": "^3.1.1",
|
"plop": "^3.1.1",
|
||||||
"postcss": "^8.4.16",
|
"postcss": "^8.4.18",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup": "^2.77.2",
|
"rollup": "^2.79.1",
|
||||||
"stylelint": "^14.9.1",
|
"stylelint": "^14.14.0",
|
||||||
"stylelint-config-html": "^1.1.0",
|
"stylelint-config-html": "^1.1.0",
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
"stylelint-config-recommended": "^8.0.0",
|
"stylelint-config-recommended": "^9.0.0",
|
||||||
"stylelint-config-standard": "^26.0.0",
|
"stylelint-config-standard": "^29.0.0",
|
||||||
"stylelint-order": "^5.0.0",
|
"stylelint-order": "^5.0.0",
|
||||||
"typescript": "4.7.4",
|
"typescript": "4.8.4",
|
||||||
"unplugin-vue-define-options": "^0.7.3",
|
"unplugin-vue-macros": "^0.13.2",
|
||||||
"vite": "3.0.5",
|
"vite": "3.1.8",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"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-html": "^3.2.0",
|
||||||
"vite-plugin-purge-icons": "^0.9.0",
|
"vite-plugin-purge-icons": "^0.9.1",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "2.0.0",
|
||||||
"vite-plugin-svg-icons": "^2.0.1",
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
"vite-plugin-windicss": "^1.8.7",
|
"vite-plugin-windicss": "^1.8.8",
|
||||||
"vue-tsc": "^0.39.5",
|
"vue-tsc": "^1.0.8",
|
||||||
"windicss": "^3.5.6"
|
"windicss": "^3.5.6",
|
||||||
|
"windicss-analysis": "^0.3.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18.0"
|
"node": ">= 14.18.0"
|
||||||
|
@ -4,7 +4,7 @@ import { useAppStore } from '@/store/modules/app'
|
|||||||
import { ConfigGlobal } from '@/components/ConfigGlobal'
|
import { ConfigGlobal } from '@/components/ConfigGlobal'
|
||||||
import { isDark } from '@/utils/is'
|
import { isDark } from '@/utils/is'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { useCache } from '@/hooks/web/useCache'
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
@ -16,18 +16,19 @@ const currentSize = computed(() => appStore.getCurrentSize)
|
|||||||
|
|
||||||
const greyMode = computed(() => appStore.getGreyMode)
|
const greyMode = computed(() => appStore.getGreyMode)
|
||||||
|
|
||||||
const { wsCache } = useCache()
|
|
||||||
|
|
||||||
// 根据浏览器当前主题设置系统主题色
|
// 根据浏览器当前主题设置系统主题色
|
||||||
const setDefaultTheme = () => {
|
const setDefaultTheme = () => {
|
||||||
if (wsCache.get('isDark')) {
|
if (Cookies.get('isDark')) {
|
||||||
appStore.setIsDark(wsCache.get('isDark'))
|
if (Cookies.get('isDark') === 'true') {
|
||||||
|
appStore.setIsDark(true)
|
||||||
|
} else {
|
||||||
|
appStore.setIsDark(false)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const isDarkTheme = isDark()
|
const isDarkTheme = isDark()
|
||||||
appStore.setIsDark(isDarkTheme)
|
appStore.setIsDark(isDarkTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultTheme()
|
setDefaultTheme()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ export type TenantVO = {
|
|||||||
packageId: number
|
packageId: number
|
||||||
contactName: string
|
contactName: string
|
||||||
contactMobile: string
|
contactMobile: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
accountCount: number
|
accountCount: number
|
||||||
expireTime: string
|
expireTime: string
|
||||||
domain: string
|
domain: string
|
||||||
|
@ -16,7 +16,7 @@ export const updateUserProfileApi = (params) => {
|
|||||||
export const updateUserPwdApi = (oldPassword: string, newPassword: string) => {
|
export const updateUserPwdApi = (oldPassword: string, newPassword: string) => {
|
||||||
return request.put({
|
return request.put({
|
||||||
url: '/system/user/profile/update-password',
|
url: '/system/user/profile/update-password',
|
||||||
params: {
|
data: {
|
||||||
oldPassword: oldPassword,
|
oldPassword: oldPassword,
|
||||||
newPassword: newPassword
|
newPassword: newPassword
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,14 @@ import { useWindowSize } from '@vueuse/core'
|
|||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { setCssVar } from '@/utils'
|
import { setCssVar } from '@/utils'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
|
import { ElementPlusSize } from '@/types/elementPlus'
|
||||||
|
|
||||||
const { variables } = useDesign()
|
const { variables } = useDesign()
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
size: propTypes.oneOf<ElememtPlusSize[]>(['default', 'small', 'large']).def('default')
|
size: propTypes.oneOf<ElementPlusSize[]>(['default', 'small', 'large']).def('default')
|
||||||
})
|
})
|
||||||
|
|
||||||
provide('configGlobal', props)
|
provide('configGlobal', props)
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||||
import { PropType, ref } from 'vue'
|
import { PropType, ref, defineEmits } from 'vue'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
|
import { contextMenuSchema } from '../../../types/contextMenu'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, computed, watch, onMounted, unref, toRef, PropType } from 'vue'
|
import { reactive, computed, watch, onMounted, unref, toRef, PropType, defineEmits } from 'vue'
|
||||||
import { isNumber } from '@/utils/is'
|
import { isNumber } from '@/utils/is'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
|
@ -24,7 +24,7 @@ import {
|
|||||||
ElRadioButton,
|
ElRadioButton,
|
||||||
ElMessage
|
ElMessage
|
||||||
} from 'element-plus'
|
} from 'element-plus'
|
||||||
import { ref, computed, onMounted, reactive, watch, PropType } from 'vue'
|
import { ref, computed, onMounted, reactive, watch, PropType, defineEmits } from 'vue'
|
||||||
interface shortcutsType {
|
interface shortcutsType {
|
||||||
text: string
|
text: string
|
||||||
value: string
|
value: string
|
||||||
|
@ -4,6 +4,7 @@ import { useDesign } from '@/hooks/web/useDesign'
|
|||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { ref, unref, PropType, computed, useAttrs } from 'vue'
|
import { ref, unref, PropType, computed, useAttrs } from 'vue'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
import { DescriptionsSchema } from '@/types/descriptions'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ const dialogStyle = computed(() => {
|
|||||||
</slot>
|
</slot>
|
||||||
<Icon
|
<Icon
|
||||||
v-if="fullscreen"
|
v-if="fullscreen"
|
||||||
class="mr-22px cursor-pointer is-hover mt-2px"
|
class="mr-22px cursor-pointer is-hover mt-2px z-10"
|
||||||
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
|
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
|
||||||
color="var(--el-color-info)"
|
color="var(--el-color-info)"
|
||||||
@click="toggleFull"
|
@click="toggleFull"
|
||||||
|
@ -177,7 +177,7 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="border-1 border-solid border-[var(--tags-view-border-color)]">
|
<div class="border-1 border-solid border-[var(--tags-view-border-color)] z-3000">
|
||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<Toolbar
|
<Toolbar
|
||||||
:editor="editorRef"
|
:editor="editorRef"
|
||||||
|
@ -4,6 +4,7 @@ import networkError from '@/assets/svgs/500.svg'
|
|||||||
import noPermission from '@/assets/svgs/403.svg'
|
import noPermission from '@/assets/svgs/403.svg'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
|
import { defineEmits } from 'vue'
|
||||||
|
|
||||||
interface ErrorMap {
|
interface ErrorMap {
|
||||||
url: string
|
url: string
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Form from './src/Form.vue'
|
import Form from './src/Form.vue'
|
||||||
import { ElForm } from 'element-plus'
|
import { ElForm } from 'element-plus'
|
||||||
|
import { FormSchema, FormSetPropsType } from '@/types/form'
|
||||||
|
|
||||||
export interface FormExpose {
|
export interface FormExpose {
|
||||||
setValues: (data: Recordable) => void
|
setValues: (data: Recordable) => void
|
||||||
|
@ -20,6 +20,7 @@ import { findIndex } from '@/utils'
|
|||||||
import { set } from 'lodash-es'
|
import { set } from 'lodash-es'
|
||||||
import { FormProps } from './types'
|
import { FormProps } from './types'
|
||||||
import { Icon } from '@/components/Icon'
|
import { Icon } from '@/components/Icon'
|
||||||
|
import { FormSchema, FormSetPropsType } from '@/types/form'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
} from 'element-plus'
|
} from 'element-plus'
|
||||||
import { InputPassword } from '@/components/InputPassword'
|
import { InputPassword } from '@/components/InputPassword'
|
||||||
import { Editor } from '@/components/Editor'
|
import { Editor } from '@/components/Editor'
|
||||||
|
import { ComponentName } from '@/types/components'
|
||||||
|
|
||||||
const componentMap: Recordable<Component, ComponentName> = {
|
const componentMap: Recordable<Component, ComponentName> = {
|
||||||
Radio: ElRadioGroup,
|
Radio: ElRadioGroup,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { FormSchema } from '@/types/form'
|
||||||
import { ElCheckbox, ElCheckboxButton } from 'element-plus'
|
import { ElCheckbox, ElCheckboxButton } from 'element-plus'
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { FormSchema } from '@/types/form'
|
||||||
import { ElRadio, ElRadioButton } from 'element-plus'
|
import { ElRadio, ElRadioButton } from 'element-plus'
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { FormSchema } from '@/types/form'
|
||||||
|
import { ComponentOptions } from '@/types/components'
|
||||||
import { ElOption, ElOptionGroup } from 'element-plus'
|
import { ElOption, ElOptionGroup } from 'element-plus'
|
||||||
import { getSlot } from '@/utils/tsxHelper'
|
import { getSlot } from '@/utils/tsxHelper'
|
||||||
import { Slots } from 'vue'
|
import { Slots } from 'vue'
|
||||||
@ -29,8 +31,15 @@ export const useRenderSelect = (slots: Slots) => {
|
|||||||
// 如果有别名,就取别名
|
// 如果有别名,就取别名
|
||||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField
|
const labelAlias = item?.componentProps?.optionsAlias?.labelField
|
||||||
const valueAlias = item?.componentProps?.optionsAlias?.valueField
|
const valueAlias = item?.componentProps?.optionsAlias?.valueField
|
||||||
|
|
||||||
|
const { label, value, ...other } = option
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ElOption label={option[labelAlias || 'label']} value={option[valueAlias || 'value']}>
|
<ElOption
|
||||||
|
label={labelAlias ? option[labelAlias] : label}
|
||||||
|
value={valueAlias ? option[valueAlias] : value}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
default: () =>
|
default: () =>
|
||||||
// option 插槽名规则,{field}-option
|
// option 插槽名规则,{field}-option
|
||||||
|
@ -2,6 +2,8 @@ import { useI18n } from '@/hooks/web/useI18n'
|
|||||||
import type { Slots } from 'vue'
|
import type { Slots } from 'vue'
|
||||||
import { getSlot } from '@/utils/tsxHelper'
|
import { getSlot } from '@/utils/tsxHelper'
|
||||||
import { PlaceholderMoel } from './types'
|
import { PlaceholderMoel } from './types'
|
||||||
|
import { FormSchema } from '@/types/form'
|
||||||
|
import { ColProps } from '@/types/components'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { FormSchema } from '@/types/form'
|
||||||
|
|
||||||
export interface PlaceholderMoel {
|
export interface PlaceholderMoel {
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
startPlaceholder?: string
|
startPlaceholder?: string
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import { ref, computed, CSSProperties, toRef, watch } from 'vue'
|
import { ref, computed, CSSProperties, toRef, watch, defineEmits } from 'vue'
|
||||||
import {
|
import {
|
||||||
ElInput,
|
ElInput,
|
||||||
ElPopover,
|
ElPopover,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from 'vue'
|
import { PropType, defineEmits } from 'vue'
|
||||||
import { Highlight } from '@//components/Highlight'
|
import { Highlight } from '@/components/Highlight'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
import { TipSchema } from '@/types/infoTip'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, unref, computed, watch } from 'vue'
|
import { ref, unref, computed, watch, defineEmits } from 'vue'
|
||||||
import { ElInput } from 'element-plus'
|
import { ElInput } from 'element-plus'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useConfigGlobal } from '@/hooks/web/useConfigGlobal'
|
import { useConfigGlobal } from '@/hooks/web/useConfigGlobal'
|
||||||
|
@ -3,11 +3,11 @@ import { computed, defineComponent, unref, PropType } from 'vue'
|
|||||||
import { ElMenu, ElScrollbar } from 'element-plus'
|
import { ElMenu, ElScrollbar } from 'element-plus'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { usePermissionStore } from '@/store/modules/permission'
|
import { usePermissionStore } from '@/store/modules/permission'
|
||||||
import type { LayoutType } from '@/config/app'
|
|
||||||
import { useRenderMenuItem } from './components/useRenderMenuItem'
|
import { useRenderMenuItem } from './components/useRenderMenuItem'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { isUrl } from '@/utils/is'
|
import { isUrl } from '@/utils/is'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
|
import { LayoutType } from '@/types/layout'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType, nextTick, ref, watch, computed, unref } from 'vue'
|
import { PropType, nextTick, ref, watch, computed, unref, defineEmits } from 'vue'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
import { QRCodeRenderersOptions } from 'qrcode'
|
import { QRCodeRenderersOptions } from 'qrcode'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { isString } from '@/utils/is'
|
import { isString } from '@/utils/is'
|
||||||
|
import { QrcodeLogo } from '@/types/qrcode'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// img 或者 canvas,img不支持logo嵌套
|
// img 或者 canvas,img不支持logo嵌套
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Form } from '@/components/Form'
|
import { Form } from '@/components/Form'
|
||||||
import { PropType, computed, unref, ref } from 'vue'
|
import { PropType, computed, unref, ref, defineEmits } from 'vue'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { ElButton } from 'element-plus'
|
import { ElButton } from 'element-plus'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
import { useForm } from '@/hooks/web/useForm'
|
import { useForm } from '@/hooks/web/useForm'
|
||||||
import { findIndex } from '@/utils'
|
import { findIndex } from '@/utils'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
import { FormSchema } from '@/types/form'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ const clear = () => {
|
|||||||
<Icon icon="ep:setting" color="#fff" />
|
<Icon icon="ep:setting" color="#fff" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ElDrawer v-model="drawer" direction="rtl" size="350px">
|
<ElDrawer v-model="drawer" direction="rtl" size="350px" :z-index="4000">
|
||||||
<template #header>
|
<template #header>
|
||||||
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
|
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user