mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2024-11-26 01:01:52 +08:00
Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/1.8.0-uniapp
Conflicts: yudao-module-mall/yudao-module-trade-biz/pom.xml
This commit is contained in:
commit
2bd2cb56a1
69
README.md
69
README.md
@ -23,6 +23,8 @@
|
||||
>
|
||||
> 😜 给项目点点 Star 吧,这对我们真的很重要!
|
||||
|
||||
![架构图](https://static.iocoder.cn/ruoyi-vue-pro-architecture.png)
|
||||
|
||||
* 管理后台的 Vue3 版本采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) ,Vue2 版本采用 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
||||
* 管理后台的移动端采用 [uni-app](https://github.com/dcloudio/uni-app) 方案,一份代码多终端适配,同时支持 APP、小程序、H5!
|
||||
* 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson
|
||||
@ -42,16 +44,19 @@
|
||||
| `yudao-cloud` | Spring Cloud 微服务 | **[Gitee](https://gitee.com/zhijiantianya/yudao-cloud)** [Github](https://github.com/YunaiV/yudao-cloud) |
|
||||
| `Spring-Boot-Labs` | Spring Boot & Cloud 入门 | **[Gitee](https://gitee.com/zhijiantianya/SpringBoot-Labs)** [Github](https://github.com/YunaiV/SpringBoot-Labs) |
|
||||
|
||||
|
||||
## 🐼 内置功能
|
||||
|
||||
分成多种内置功能:
|
||||
系统内置多种多种业务功能,可以用于快速你的业务系统:
|
||||
|
||||
![功能分层](https://static.iocoder.cn/ruoyi-vue-pro-biz.png)
|
||||
|
||||
* 系统功能
|
||||
* 基础设施
|
||||
* 工作流程
|
||||
* 支付系统
|
||||
* 商城系统
|
||||
* 会员中心
|
||||
* 数据报表
|
||||
* 商城系统
|
||||
|
||||
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
|
||||
>
|
||||
@ -169,29 +174,29 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
||||
|
||||
### 后端
|
||||
|
||||
| 框架 | 说明 | 版本 | 学习指南 |
|
||||
|---------------------------------------------------------------------------------------------|-----------------------|---------|----------------------------------------------------------------|
|
||||
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.6.10 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
|
||||
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 | |
|
||||
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.11 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
||||
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.2 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
|
||||
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
||||
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 | |
|
||||
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.17.4 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
|
||||
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.20 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
|
||||
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.6.5 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
|
||||
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.3 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
|
||||
| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.7.2 | [文档](https://doc.iocoder.cn/bpm/) |
|
||||
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
|
||||
| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
|
||||
| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
|
||||
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
|
||||
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.6.7 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
|
||||
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.3 | |
|
||||
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
|
||||
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
|
||||
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - |
|
||||
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.0.0 | - |
|
||||
| 框架 | 说明 | 版本 | 学习指南 |
|
||||
|---------------------------------------------------------------------------------------------|-----------------------|------------------|----------------------------------------------------------------|
|
||||
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.6.12 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
|
||||
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | |
|
||||
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.13-SNSAPSHOT | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
||||
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.2 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
|
||||
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.5.2 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
||||
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 | |
|
||||
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.17.7 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
|
||||
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.20 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
|
||||
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.6.5 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
|
||||
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.3 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
|
||||
| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.7.2 | [文档](https://doc.iocoder.cn/bpm/) |
|
||||
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
|
||||
| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
|
||||
| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
|
||||
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
|
||||
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.6.9 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
|
||||
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.3 | |
|
||||
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.5.2.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
|
||||
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.24 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
|
||||
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - |
|
||||
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.8.0 | - |
|
||||
|
||||
### [管理后台 Vue2 前端](./yudao-ui-admin)
|
||||
|
||||
@ -204,14 +209,14 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
||||
|
||||
| 框架 | 说明 | 版本 |
|
||||
|----------------------------------------------------------------------|-----------------|--------|
|
||||
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.37 |
|
||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.0.4 |
|
||||
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.12 |
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.7.4 |
|
||||
| [pinia](https://pinia.vuejs.org/) | vuex5 | 2.0.17 |
|
||||
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.41 |
|
||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.1.8 |
|
||||
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.18 |
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.8.4 |
|
||||
| [pinia](https://pinia.vuejs.org/) | vuex5 | 2.0.23 |
|
||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
|
||||
| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
|
||||
| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.1 |
|
||||
| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.0.0 |
|
||||
|
||||
### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp)
|
||||
|
||||
|
8
pom.xml
8
pom.xml
@ -12,7 +12,7 @@
|
||||
<module>yudao-framework</module>
|
||||
<!-- Server 主项目 -->
|
||||
<module>yudao-server</module>
|
||||
<!-- 各种 module 拓展 -->
|
||||
<!-- 各种 module 拓展 -->
|
||||
<module>yudao-module-member</module>
|
||||
<module>yudao-module-bpm</module>
|
||||
<module>yudao-module-system</module>
|
||||
@ -20,6 +20,8 @@
|
||||
<module>yudao-module-pay</module>
|
||||
<module>yudao-module-mall</module>
|
||||
<module>yudao-module-visualization</module>
|
||||
<!-- 示例项目 -->
|
||||
<module>yudao-example</module>
|
||||
</modules>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
@ -35,8 +37,8 @@
|
||||
<maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
|
||||
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
|
||||
<!-- 看看咋放到 bom 里 -->
|
||||
<lombok.version>1.18.20</lombok.version>
|
||||
<mapstruct.version>1.4.1.Final</mapstruct.version>
|
||||
<lombok.version>1.18.24</lombok.version>
|
||||
<mapstruct.version>1.5.2.Final</mapstruct.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
|
@ -16,58 +16,58 @@
|
||||
<properties>
|
||||
<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 相关 -->
|
||||
<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>
|
||||
<!-- 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-generator.version>3.5.2</mybatis-plus-generator.version>
|
||||
<dynamic-datasource.version>3.5.0</dynamic-datasource.version>
|
||||
<redisson.version>3.17.4</redisson.version>
|
||||
<dynamic-datasource.version>3.5.2</dynamic-datasource.version>
|
||||
<redisson.version>3.17.7</redisson.version>
|
||||
<!-- 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>
|
||||
<!-- 监控相关 -->
|
||||
<skywalking.version>8.7.0</skywalking.version>
|
||||
<spring-boot-admin.version>2.6.7</spring-boot-admin.version>
|
||||
<opentracing.version>0.31.0</opentracing.version>
|
||||
<skywalking.version>8.12.0</skywalking.version>
|
||||
<spring-boot-admin.version>2.6.9</spring-boot-admin.version>
|
||||
<opentracing.version>0.33.0</opentracing.version>
|
||||
<!-- Test 测试相关 -->
|
||||
<podam.version>7.2.6.RELEASE</podam.version>
|
||||
<jedis-mock.version>0.1.16</jedis-mock.version>
|
||||
<mockito-inline.version>4.0.0</mockito-inline.version>
|
||||
<podam.version>7.2.9.RELEASE</podam.version>
|
||||
<jedis-mock.version>1.0.4</jedis-mock.version>
|
||||
<mockito-inline.version>4.8.0</mockito-inline.version>
|
||||
<!-- Bpm 工作流相关 -->
|
||||
<flowable.version>6.7.2</flowable.version>
|
||||
<!-- 工具类相关 -->
|
||||
<jasypt-spring-boot-starter.version>3.0.4</jasypt-spring-boot-starter.version>
|
||||
<lombok.version>1.18.20</lombok.version>
|
||||
<mapstruct.version>1.4.1.Final</mapstruct.version>
|
||||
<hutool.version>5.8.5</hutool.version>
|
||||
<lombok.version>1.18.24</lombok.version>
|
||||
<mapstruct.version>1.5.2.Final</mapstruct.version>
|
||||
<hutool.version>5.8.8</hutool.version>
|
||||
<easyexcel.verion>3.1.1</easyexcel.verion>
|
||||
<velocity.version>2.3</velocity.version>
|
||||
<screw.version>1.0.5</screw.version>
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<guava.version>30.1.1-jre</guava.version>
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<guava.version>31.1-jre</guava.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>
|
||||
<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>
|
||||
<netty-all.version>4.1.82.Final</netty-all.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>
|
||||
<tencentcloud-sdk-java.version>3.1.561</tencentcloud-sdk-java.version>
|
||||
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
|
||||
<justauth.version>1.4.0</justauth.version>
|
||||
<jimureport.version>1.5.2</jimureport.version>
|
||||
<xercesImpl.version>2.12.0</xercesImpl.version>
|
||||
<jimureport.version>1.5.3</jimureport.version>
|
||||
<xercesImpl.version>2.12.2</xercesImpl.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
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>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.31.72.ALL</version>
|
||||
<version>4.33.44.ALL</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
@ -63,7 +63,7 @@
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-pay</artifactId>
|
||||
<version>4.3.8.B</version>
|
||||
<version>4.4.0</version>
|
||||
</dependency>
|
||||
<!-- TODO 芋艿:清理 -->
|
||||
|
||||
|
@ -35,12 +35,12 @@
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
|
||||
<version>4.3.8.B</version>
|
||||
<version>4.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
|
||||
<version>4.3.8.B</version>
|
||||
<version>4.4.0</version>
|
||||
</dependency>
|
||||
<!-- TODO 芋艿:清理 -->
|
||||
</dependencies>
|
||||
|
@ -5,6 +5,8 @@ import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstant
|
||||
import com.baomidou.lock.LockFailureStrategy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* 自定义获取锁失败策略,抛出 {@link ServiceException} 异常
|
||||
*/
|
||||
@ -12,9 +14,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class DefaultLockFailureStrategy implements LockFailureStrategy {
|
||||
|
||||
@Override
|
||||
public void onLockFailure(String key, long acquireTimeout, int acquireCount) {
|
||||
log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取超时时长:{} ms]", Thread.currentThread().getName(), key, acquireTimeout);
|
||||
public void onLockFailure(String key, Method method, Object[] arguments) {
|
||||
log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取失败:{} ]", Thread.currentThread().getName(), key, arguments);
|
||||
throw new ServiceException(GlobalErrorCodeConstants.LOCKED);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.convert.message;
|
||||
|
||||
import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.Map;
|
||||
@ -11,6 +12,10 @@ public interface BpmMessageConvert {
|
||||
|
||||
BpmMessageConvert INSTANCE = Mappers.getMapper(BpmMessageConvert.class);
|
||||
|
||||
@Mapping(target = "mobile", ignore = true)
|
||||
@Mapping(source = "userId", target = "userId")
|
||||
@Mapping(source = "templateCode", target = "templateCode")
|
||||
@Mapping(source = "templateParams", target = "templateParams")
|
||||
SmsSendSingleToUserReqDTO convert(Long userId, String templateCode, Map<String, Object> templateParams);
|
||||
|
||||
}
|
||||
|
@ -50,23 +50,29 @@
|
||||
|
||||
// 提交按钮
|
||||
const submitForm = async () => {
|
||||
actionLoading.value = true
|
||||
// 提交请求
|
||||
try {
|
||||
const data = unref(formRef)?.formModel as ${simpleClassName}VO
|
||||
if (actionType.value === 'create') {
|
||||
await ${simpleClassName}Api.create${simpleClassName}Api(data)
|
||||
ElMessage.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await ${simpleClassName}Api.update${simpleClassName}Api(data)
|
||||
ElMessage.success(t('common.updateSuccess'))
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
if (!elForm) return
|
||||
elForm.validate(async (valid) => {
|
||||
if (valid) {
|
||||
actionLoading.value = true
|
||||
// 提交请求
|
||||
try {
|
||||
const data = unref(formRef)?.formModel as ${simpleClassName}VO
|
||||
if (actionType.value === 'create') {
|
||||
await ${simpleClassName}Api.create${simpleClassName}Api(data)
|
||||
ElMessage.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await ${simpleClassName}Api.update${simpleClassName}Api(data)
|
||||
ElMessage.success(t('common.updateSuccess'))
|
||||
}
|
||||
// 操作成功,重新加载列表
|
||||
dialogVisible.value = false
|
||||
await getList()
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
}
|
||||
// 操作成功,重新加载列表
|
||||
dialogVisible.value = false
|
||||
await getList()
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 详情相关 ==========
|
||||
|
@ -30,10 +30,6 @@
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
@ -42,7 +38,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
@ -57,6 +53,12 @@
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -4,7 +4,6 @@ import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.module.coupon.dal.dataobject.CouponTemplete.CouponTempleteDO;
|
||||
import cn.iocoder.yudao.module.coupon.dal.mysql.CouponTemplete.CouponTempleteMapper;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import org.springframework.stereotype.Service;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
@ -39,16 +39,16 @@
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<dependency>
|
||||
@ -61,6 +61,12 @@
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</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 io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
@ -27,8 +27,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
@Validated
|
||||
public class AppBannerController {
|
||||
|
||||
// TODO @xia:使用 @Resource 哈
|
||||
@Autowired
|
||||
@Resource
|
||||
private BannerService bannerService;
|
||||
|
||||
// TODO @xia:新建一个 AppBannerRespVO,只返回必要的字段。status 要过滤下。然后 sort 下结果
|
||||
|
@ -29,10 +29,6 @@
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
@ -41,7 +37,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
@ -56,6 +52,11 @@
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -47,10 +47,6 @@
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
@ -60,13 +56,13 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<!-- TODO 芋艿:引入依赖要优化下: -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
@ -80,6 +76,12 @@
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.common.validation.Mobile;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
@ -106,7 +106,7 @@ public class OAuth2OpenController {
|
||||
@RequestParam(value = "scope", required = false) String scope, // 密码模式
|
||||
@RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式
|
||||
List<String> scopes = OAuth2Utils.buildScopes(scope);
|
||||
// 授权类型
|
||||
// 1.1 校验授权类型
|
||||
OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType);
|
||||
if (grantTypeEnum == null) {
|
||||
throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType));
|
||||
@ -115,12 +115,12 @@ public class OAuth2OpenController {
|
||||
throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式");
|
||||
}
|
||||
|
||||
// 校验客户端
|
||||
// 1.2 校验客户端
|
||||
String[] clientIdAndSecret = obtainBasicAuthorization(request);
|
||||
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
|
||||
grantType, scopes, redirectUri);
|
||||
|
||||
// 根据授权模式,获取访问令牌
|
||||
// 2. 根据授权模式,获取访问令牌
|
||||
OAuth2AccessTokenDO accessTokenDO;
|
||||
switch (grantTypeEnum) {
|
||||
case AUTHORIZATION_CODE:
|
||||
|
@ -26,6 +26,7 @@ public class OAuth2OpenCheckTokenRespVO {
|
||||
private Long tenantId;
|
||||
|
||||
@ApiModelProperty(value = "客户端编号", required = true, example = "car")
|
||||
@JsonProperty("client_id")
|
||||
private String clientId;
|
||||
@ApiModelProperty(value = "授权范围", required = true, example = "user_info")
|
||||
private List<String> scopes;
|
||||
|
@ -17,7 +17,7 @@ public class OAuth2UserInfoRespVO {
|
||||
@ApiModelProperty(value = "用户编号", required = true, example = "1")
|
||||
private Long id;
|
||||
|
||||
@ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
|
||||
@ApiModelProperty(value = "用户账号", required = true, example = "芋艿")
|
||||
private String username;
|
||||
|
||||
@ApiModelProperty(value = "用户昵称", required = true, example = "芋道")
|
||||
|
@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.*;
|
||||
import cn.iocoder.yudao.module.system.convert.permission.MenuConvert;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
|
||||
import cn.iocoder.yudao.module.system.service.permission.MenuService;
|
||||
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
@ -29,8 +28,6 @@ public class MenuController {
|
||||
|
||||
@Resource
|
||||
private MenuService menuService;
|
||||
@Resource
|
||||
private TenantService tenantService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@ApiOperation("创建菜单")
|
||||
|
@ -34,7 +34,7 @@ public class TenantPackageDO extends BaseDO {
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 租户状态
|
||||
* 租户套餐状态
|
||||
*
|
||||
* 枚举 {@link CommonStatusEnum}
|
||||
*/
|
||||
|
@ -12,7 +12,7 @@ import java.util.List;
|
||||
* 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。
|
||||
*
|
||||
* 问题:为什么自身也作为一个第三方应用,也走这套流程呢?
|
||||
* 回复:当然可以这么做,采用 Implicit 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。
|
||||
* 回复:当然可以这么做,采用 password 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
|
@ -45,33 +45,33 @@ spring:
|
||||
datasource:
|
||||
master:
|
||||
name: ruoyi-vue-pro
|
||||
url: jdbc:mysql://139.9.196.247:3306/${spring.datasource.dynamic.datasource.master.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
|
||||
url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
|
||||
# url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例
|
||||
# url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例
|
||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例
|
||||
username: root
|
||||
password: ${RUOYI_VUE_PRO}
|
||||
password: 123456
|
||||
# username: sa
|
||||
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
|
||||
slave: # 模拟从库,可根据自己需要修改
|
||||
name: ruoyi-vue-pro
|
||||
url: jdbc:mysql://139.9.196.247:3306/${spring.datasource.dynamic.datasource.slave.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
|
||||
url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
|
||||
# url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例
|
||||
# url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例
|
||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例
|
||||
username: root
|
||||
password: ${RUOYI_VUE_PRO}
|
||||
password: 123456
|
||||
# username: sa
|
||||
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W
|
||||
|
||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||
redis:
|
||||
host: 139.9.196.247 # 地址
|
||||
host: 127.0.0.1 # 地址
|
||||
port: 6379 # 端口
|
||||
database: 0 # 数据库索引
|
||||
password: 123456 # 密码,建议生产环境开启
|
||||
# password: 123456 # 密码,建议生产环境开启
|
||||
|
||||
jasypt:
|
||||
encryptor:
|
||||
@ -198,7 +198,7 @@ wx:
|
||||
# 芋道配置项,设置当前项目所有自定义的配置
|
||||
yudao:
|
||||
captcha:
|
||||
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试
|
||||
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
|
||||
security:
|
||||
mock-enable: true
|
||||
xss:
|
||||
|
@ -27,17 +27,17 @@
|
||||
|
||||
| 框架 | 说明 | 版本 |
|
||||
| --- | --- |--------|
|
||||
| [Vue](https://staging-cn.vuejs.org/) | vue 框架 | 3.2.37 |
|
||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.0.4 |
|
||||
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.12 |
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.7.4 |
|
||||
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.17 |
|
||||
| [vueuse](https://vueuse.org//) | 常用工具集 | 9.0.2 |
|
||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.0 |
|
||||
| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.3 |
|
||||
| [Vue](https://staging-cn.vuejs.org/) | vue 框架 | 3.2.41 |
|
||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 3.1.8 |
|
||||
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.18 |
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.8.4 |
|
||||
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.23 |
|
||||
| [vueuse](https://vueuse.org//) | 常用工具集 | 9.3.1 |
|
||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
|
||||
| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.5 |
|
||||
| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
|
||||
| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.1 |
|
||||
| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.14 |
|
||||
| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.0.0 |
|
||||
| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.21 |
|
||||
|
||||
### 推荐 VScode 开发,插件如下
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ruoyi-vue-pro-vue3",
|
||||
"version": "1.6.3.1611",
|
||||
"version": "1.6.4.1811",
|
||||
"description": "基于vue3、element-plus、typesScript、vite3",
|
||||
"author": "xingyu",
|
||||
"private": false,
|
||||
@ -10,7 +10,7 @@
|
||||
"ts:check": "vue-tsc --noEmit",
|
||||
"build:pro": "vite build --mode pro",
|
||||
"build:dev": "vite build --mode dev",
|
||||
"build:test": "pnpm run ts:check && vite build --mode test",
|
||||
"build:test": "npm run ts:check && vite build --mode test",
|
||||
"serve:pro": "vite preview --mode pro",
|
||||
"serve:dev": "vite preview --mode dev",
|
||||
"serve:test": "vite preview --mode test",
|
||||
@ -25,84 +25,84 @@
|
||||
"p": "plop"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/iconify": "^2.2.1",
|
||||
"@vueuse/core": "^9.1.0",
|
||||
"@wangeditor/editor": "^5.1.14",
|
||||
"@iconify/iconify": "^3.0.0",
|
||||
"@vueuse/core": "^9.3.1",
|
||||
"@wangeditor/editor": "^5.1.21",
|
||||
"@wangeditor/editor-for-vue": "^5.1.10",
|
||||
"@zxcvbn-ts/core": "^2.0.4",
|
||||
"@zxcvbn-ts/core": "^2.0.5",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^0.27.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.11.4",
|
||||
"echarts": "^5.3.3",
|
||||
"dayjs": "^1.11.5",
|
||||
"echarts": "^5.4.0",
|
||||
"echarts-wordcloud": "^2.0.0",
|
||||
"element-plus": "2.2.12",
|
||||
"element-plus": "2.2.18",
|
||||
"intro.js": "^6.0.0",
|
||||
"jsencrypt": "^3.2.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.17",
|
||||
"pinia-plugin-persist": "^1.0.0",
|
||||
"pinia": "^2.0.23",
|
||||
"qrcode": "^1.5.1",
|
||||
"qs": "^6.11.0",
|
||||
"url": "^0.11.0",
|
||||
"vue": "3.2.37",
|
||||
"vue": "3.2.41",
|
||||
"vue-cropper": "^1.0.3",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "^4.1.3",
|
||||
"vue-router": "^4.1.5",
|
||||
"vue-types": "^4.2.1",
|
||||
"web-storage-cache": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.3",
|
||||
"@commitlint/config-conventional": "^17.0.3",
|
||||
"@iconify/json": "^2.1.89",
|
||||
"@intlify/vite-plugin-vue-i18n": "^6.0.0",
|
||||
"@commitlint/cli": "^17.1.2",
|
||||
"@commitlint/config-conventional": "^17.1.0",
|
||||
"@iconify/json": "^2.1.122",
|
||||
"@intlify/vite-plugin-vue-i18n": "^6.0.3",
|
||||
"@purge-icons/generated": "^0.9.0",
|
||||
"@types/intro.js": "^5.1.0",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/node": "^18.6.5",
|
||||
"@types/node": "^18.11.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/qrcode": "^1.4.2",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||
"@typescript-eslint/parser": "^5.33.0",
|
||||
"@vitejs/plugin-vue": "^3.0.1",
|
||||
"@vitejs/plugin-vue-jsx": "^2.0.0",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"eslint": "^8.21.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"@vitejs/plugin-vue": "^3.1.2",
|
||||
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-define-config": "^1.6.0",
|
||||
"eslint-define-config": "^1.7.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"eslint-plugin-vue": "^9.6.0",
|
||||
"less": "^4.1.3",
|
||||
"lint-staged": "^13.0.3",
|
||||
"plop": "^3.1.1",
|
||||
"postcss": "^8.4.16",
|
||||
"postcss": "^8.4.18",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-less": "^6.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.77.2",
|
||||
"stylelint": "^14.9.1",
|
||||
"rollup": "^2.79.1",
|
||||
"stylelint": "^14.14.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-recommended": "^8.0.0",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"stylelint-config-recommended": "^9.0.0",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-order": "^5.0.0",
|
||||
"typescript": "4.7.4",
|
||||
"unplugin-vue-define-options": "^0.7.3",
|
||||
"vite": "3.0.5",
|
||||
"typescript": "4.8.4",
|
||||
"unplugin-vue-macros": "^0.13.2",
|
||||
"vite": "3.1.8",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-eslint": "^1.7.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-purge-icons": "^0.9.0",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vite-plugin-purge-icons": "^0.9.1",
|
||||
"vite-plugin-style-import": "2.0.0",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-windicss": "^1.8.7",
|
||||
"vue-tsc": "^0.39.5",
|
||||
"windicss": "^3.5.6"
|
||||
"vite-plugin-windicss": "^1.8.8",
|
||||
"vue-tsc": "^1.0.8",
|
||||
"windicss": "^3.5.6",
|
||||
"windicss-analysis": "^0.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
|
@ -4,6 +4,8 @@ export type TenantVO = {
|
||||
packageId: number
|
||||
contactName: string
|
||||
contactMobile: string
|
||||
username: string
|
||||
password: string
|
||||
accountCount: number
|
||||
expireTime: string
|
||||
domain: string
|
||||
|
@ -7,13 +7,14 @@ import { useWindowSize } from '@vueuse/core'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { setCssVar } from '@/utils'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { ElementPlusSize } from '@/types/elementPlus'
|
||||
|
||||
const { variables } = useDesign()
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const props = defineProps({
|
||||
size: propTypes.oneOf<ElememtPlusSize[]>(['default', 'small', 'large']).def('default')
|
||||
size: propTypes.oneOf<ElementPlusSize[]>(['default', 'small', 'large']).def('default')
|
||||
})
|
||||
|
||||
provide('configGlobal', props)
|
||||
|
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
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 { useDesign } from '@/hooks/web/useDesign'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { contextMenuSchema } from '../../../types/contextMenu'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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 { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
ElRadioButton,
|
||||
ElMessage
|
||||
} from 'element-plus'
|
||||
import { ref, computed, onMounted, reactive, watch, PropType } from 'vue'
|
||||
import { ref, computed, onMounted, reactive, watch, PropType, defineEmits } from 'vue'
|
||||
interface shortcutsType {
|
||||
text: string
|
||||
value: string
|
||||
|
@ -4,6 +4,7 @@ import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { ref, unref, PropType, computed, useAttrs } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { DescriptionsSchema } from '@/types/descriptions'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
|
@ -74,7 +74,7 @@ const dialogStyle = computed(() => {
|
||||
</slot>
|
||||
<Icon
|
||||
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'"
|
||||
color="var(--el-color-info)"
|
||||
@click="toggleFull"
|
||||
|
@ -177,7 +177,7 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<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
|
||||
:editor="editorRef"
|
||||
|
@ -4,6 +4,7 @@ import networkError from '@/assets/svgs/500.svg'
|
||||
import noPermission from '@/assets/svgs/403.svg'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { defineEmits } from 'vue'
|
||||
|
||||
interface ErrorMap {
|
||||
url: string
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Form from './src/Form.vue'
|
||||
import { ElForm } from 'element-plus'
|
||||
import { FormSchema, FormSetPropsType } from '@/types/form'
|
||||
|
||||
export interface FormExpose {
|
||||
setValues: (data: Recordable) => void
|
||||
|
@ -20,6 +20,7 @@ import { findIndex } from '@/utils'
|
||||
import { set } from 'lodash-es'
|
||||
import { FormProps } from './types'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { FormSchema, FormSetPropsType } from '@/types/form'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
} from 'element-plus'
|
||||
import { InputPassword } from '@/components/InputPassword'
|
||||
import { Editor } from '@/components/Editor'
|
||||
import { ComponentName } from '@/types/components'
|
||||
|
||||
const componentMap: Recordable<Component, ComponentName> = {
|
||||
Radio: ElRadioGroup,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { FormSchema } from '@/types/form'
|
||||
import { ElCheckbox, ElCheckboxButton } from 'element-plus'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { FormSchema } from '@/types/form'
|
||||
import { ElRadio, ElRadioButton } from 'element-plus'
|
||||
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 { getSlot } from '@/utils/tsxHelper'
|
||||
import { Slots } from 'vue'
|
||||
@ -29,8 +31,15 @@ export const useRenderSelect = (slots: Slots) => {
|
||||
// 如果有别名,就取别名
|
||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField
|
||||
const valueAlias = item?.componentProps?.optionsAlias?.valueField
|
||||
|
||||
const { label, value, ...other } = option
|
||||
|
||||
return (
|
||||
<ElOption label={option[labelAlias || 'label']} value={option[valueAlias || 'value']}>
|
||||
<ElOption
|
||||
label={labelAlias ? option[labelAlias] : label}
|
||||
value={valueAlias ? option[valueAlias] : value}
|
||||
{...other}
|
||||
>
|
||||
{{
|
||||
default: () =>
|
||||
// option 插槽名规则,{field}-option
|
||||
|
@ -2,6 +2,8 @@ import { useI18n } from '@/hooks/web/useI18n'
|
||||
import type { Slots } from 'vue'
|
||||
import { getSlot } from '@/utils/tsxHelper'
|
||||
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 {
|
||||
placeholder?: string
|
||||
startPlaceholder?: string
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { ref, computed, CSSProperties, toRef, watch } from 'vue'
|
||||
import { ref, computed, CSSProperties, toRef, watch, defineEmits } from 'vue'
|
||||
import {
|
||||
ElInput,
|
||||
ElPopover,
|
||||
|
@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
import { Highlight } from '@//components/Highlight'
|
||||
import { PropType, defineEmits } from 'vue'
|
||||
import { Highlight } from '@/components/Highlight'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { TipSchema } from '@/types/infoTip'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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 { propTypes } from '@/utils/propTypes'
|
||||
import { useConfigGlobal } from '@/hooks/web/useConfigGlobal'
|
||||
|
@ -3,11 +3,11 @@ import { computed, defineComponent, unref, PropType } from 'vue'
|
||||
import { ElMenu, ElScrollbar } from 'element-plus'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import type { LayoutType } from '@/config/app'
|
||||
import { useRenderMenuItem } from './components/useRenderMenuItem'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { isUrl } from '@/utils/is'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { LayoutType } from '@/types/layout'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
<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 { QRCodeRenderersOptions } from 'qrcode'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { isString } from '@/utils/is'
|
||||
import { QrcodeLogo } from '@/types/qrcode'
|
||||
|
||||
const props = defineProps({
|
||||
// img 或者 canvas,img不支持logo嵌套
|
||||
|
@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
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 { ElButton } from 'element-plus'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { findIndex } from '@/utils'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { FormSchema } from '@/types/form'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
@ -208,7 +208,7 @@ const clear = () => {
|
||||
<Icon icon="ep:setting" color="#fff" />
|
||||
</div>
|
||||
|
||||
<ElDrawer v-model="drawer" direction="rtl" size="350px">
|
||||
<ElDrawer v-model="drawer" direction="rtl" size="350px" :z-index="4000">
|
||||
<template #header>
|
||||
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
|
||||
</template>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { PropType, watch, unref, ref } from 'vue'
|
||||
import { PropType, watch, unref, ref, defineEmits } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ElSwitch } from 'element-plus'
|
||||
import { ElSwitch, ElMessage } from 'element-plus'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
@ -107,6 +107,21 @@ const greyModeChange = (show: boolean) => {
|
||||
appStore.setGreyMode(show)
|
||||
}
|
||||
|
||||
// 动态路由
|
||||
const dynamicRouter = ref(appStore.getDynamicRouter)
|
||||
|
||||
const dynamicRouterChange = (show: boolean) => {
|
||||
ElMessage.info(t('setting.reExperienced'))
|
||||
appStore.setDynamicRouter(show)
|
||||
}
|
||||
|
||||
// 固定菜单
|
||||
const fixedMenu = ref(appStore.getFixedMenu)
|
||||
|
||||
const fixedMenuChange = (show: boolean) => {
|
||||
appStore.setFixedMenu(show)
|
||||
}
|
||||
|
||||
const layout = computed(() => appStore.getLayout)
|
||||
|
||||
watch(
|
||||
@ -185,5 +200,15 @@ watch(
|
||||
<span class="text-14px">{{ t('setting.greyMode') }}</span>
|
||||
<ElSwitch v-model="greyMode" @change="greyModeChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.dynamicRouter') }}</span>
|
||||
<ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.fixedMenu') }}</span>
|
||||
<ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -5,6 +5,7 @@ import { useAppStore } from '@/store/modules/app'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { ElementPlusSize } from '@/types/elementPlus'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
@ -20,7 +21,7 @@ const appStore = useAppStore()
|
||||
|
||||
const sizeMap = computed(() => appStore.sizeMap)
|
||||
|
||||
const setCurrentSize = (size: ElememtPlusSize) => {
|
||||
const setCurrentSize = (size: ElementPlusSize) => {
|
||||
appStore.setCurrentSize(size)
|
||||
}
|
||||
</script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="tsx">
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { computed, unref, defineComponent, watch, ref } from 'vue'
|
||||
import { computed, unref, defineComponent, watch, ref, onMounted } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { ElScrollbar } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
@ -28,6 +28,8 @@ export default defineComponent({
|
||||
|
||||
const collapse = computed(() => appStore.getCollapse)
|
||||
|
||||
const fixedMenu = computed(() => appStore.getFixedMenu)
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const routers = computed(() => permissionStore.getRouters)
|
||||
@ -38,6 +40,27 @@ export default defineComponent({
|
||||
appStore.setCollapse(!unref(collapse))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (unref(fixedMenu)) {
|
||||
const path = `/${unref(currentRoute).path.split('/')[1]}`
|
||||
const children = unref(tabRouters).find(
|
||||
(v) =>
|
||||
(v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)) &&
|
||||
v.path === path
|
||||
)?.children
|
||||
|
||||
tabActive.value = path
|
||||
if (children) {
|
||||
permissionStore.setMenuTabRouters(
|
||||
cloneDeep(children).map((v) => {
|
||||
v.path = pathResolve(unref(tabActive), v.path)
|
||||
return v
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => routers.value,
|
||||
(routers: AppRouteRecordRaw[]) => {
|
||||
@ -66,7 +89,7 @@ export default defineComponent({
|
||||
)
|
||||
|
||||
// 是否显示菜单
|
||||
const showMenu = ref(false)
|
||||
const showMenu = ref(unref(fixedMenu) ? true : false)
|
||||
|
||||
// tab高亮
|
||||
const tabActive = ref('')
|
||||
@ -77,9 +100,13 @@ export default defineComponent({
|
||||
window.open(item.path)
|
||||
return
|
||||
}
|
||||
const newPath = item.children ? item.path : item.path.split('/')[0]
|
||||
const oldPath = unref(tabActive)
|
||||
tabActive.value = item.children ? item.path : item.path.split('/')[0]
|
||||
if (item.children) {
|
||||
showMenu.value = !unref(showMenu)
|
||||
if (newPath === oldPath || !unref(showMenu)) {
|
||||
showMenu.value = unref(fixedMenu) ? true : !unref(showMenu)
|
||||
}
|
||||
if (unref(showMenu)) {
|
||||
permissionStore.setMenuTabRouters(
|
||||
cloneDeep(item.children).map((v) => {
|
||||
@ -96,7 +123,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// 设置高亮
|
||||
const isActice = (currentPath: string) => {
|
||||
const isActive = (currentPath: string) => {
|
||||
const { path } = unref(currentRoute)
|
||||
if (tabPathMap[currentPath].includes(path)) {
|
||||
return true
|
||||
@ -105,7 +132,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
const mouseleave = () => {
|
||||
if (!unref(showMenu)) return
|
||||
if (!unref(showMenu) || unref(fixedMenu)) return
|
||||
showMenu.value = false
|
||||
}
|
||||
|
||||
@ -114,7 +141,7 @@ export default defineComponent({
|
||||
id={`${variables.namespace}-menu`}
|
||||
class={[
|
||||
prefixCls,
|
||||
'relative bg-[var(--left-menu-bg-color)] top-1px z-999',
|
||||
'relative bg-[var(--left-menu-bg-color)] top-1px z-3000',
|
||||
{
|
||||
'w-[var(--tab-menu-max-width)]': !unref(collapse),
|
||||
'w-[var(--tab-menu-min-width)]': unref(collapse)
|
||||
@ -140,7 +167,7 @@ export default defineComponent({
|
||||
`${prefixCls}__item`,
|
||||
'text-center text-12px relative py-12px cursor-pointer',
|
||||
{
|
||||
'is-active': isActice(v.path)
|
||||
'is-active': isActive(v.path)
|
||||
}
|
||||
]}
|
||||
onClick={() => {
|
||||
@ -174,8 +201,8 @@ export default defineComponent({
|
||||
{
|
||||
'!left-[var(--tab-menu-min-width)]': unref(collapse),
|
||||
'!left-[var(--tab-menu-max-width)]': !unref(collapse),
|
||||
'!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu),
|
||||
'!w-0': !unref(showMenu)
|
||||
'!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu),
|
||||
'!w-0': !unref(showMenu) && !unref(fixedMenu)
|
||||
}
|
||||
]}
|
||||
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
|
||||
|
@ -28,9 +28,9 @@ export const filterMenusPath = (
|
||||
let data: Nullable<AppRouteRecordRaw> = null
|
||||
const meta = (v.meta ?? {}) as RouteMeta
|
||||
if (!meta.hidden || meta.canTo) {
|
||||
const allParentPaht = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
|
||||
const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
|
||||
|
||||
const fullPath = isUrl(v.path) ? v.path : allParentPaht.join('/')
|
||||
const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/')
|
||||
|
||||
data = cloneDeep(v)
|
||||
data.path = fullPath
|
||||
@ -42,8 +42,8 @@ export const filterMenusPath = (
|
||||
res.push(data)
|
||||
}
|
||||
|
||||
if (allParentPaht.length && Reflect.has(tabPathMap, allParentPaht[0])) {
|
||||
tabPathMap[allParentPaht[0]].push(fullPath)
|
||||
if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) {
|
||||
tabPathMap[allParentPath[0]].push(fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import Table from './src/Table.vue'
|
||||
import { ElTable } from 'element-plus'
|
||||
import { TableSetPropsType } from '@/types/table'
|
||||
|
||||
export interface TableExpose {
|
||||
setProps: (props: Recordable) => void
|
||||
setColumn: (columnProps: TableSetPropsType[]) => void
|
||||
selections: Recordable[]
|
||||
elTableRef: ComponentRef<typeof ElTable>
|
||||
}
|
||||
|
||||
export { Table }
|
||||
|
@ -6,6 +6,7 @@ import { setIndex } from './helper'
|
||||
import { getSlot } from '@/utils/tsxHelper'
|
||||
import type { TableProps } from './types'
|
||||
import { set } from 'lodash-es'
|
||||
import { Pagination, TableColumn, TableSetPropsType, TableSlotDefault } from '@/types/table'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Table',
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Pagination, TableColumn } from '@/types/table'
|
||||
|
||||
export type TableProps = {
|
||||
pageSize?: number
|
||||
currentPage?: number
|
||||
|
@ -36,8 +36,8 @@ const loginOut = () => {
|
||||
})
|
||||
.then(async () => {
|
||||
userStore.loginOut()
|
||||
tagsViewStore.delAllViews
|
||||
replace('/login')
|
||||
tagsViewStore.delAllViews()
|
||||
replace('/login?redirect=/index')
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user