Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/db

# Conflicts:
#	sql/mysql/ruoyi-vue-pro.sql
This commit is contained in:
YunaiV 2024-05-01 08:05:18 +08:00
commit c0722b3a19
66 changed files with 251 additions and 4416 deletions

View File

@ -21,6 +21,18 @@
* 启动文档:<https://doc.iocoder.cn/quick-start/>
* 视频教程:<https://doc.iocoder.cn/video/>
## 🐰 版本说明
| 版本 | JDK 8 + Spring Boot 2.7 | JDK 17/21 + Spring Boot 3.2 |
|---------------------------------------------------------------------|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| 【完整版】[ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [`master`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master-jdk17/) 分支 |
| 【精简版】[yudao-boot-mini](https://gitee.com/yudaocode/yudao-boot-mini) | [`master`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master-jdk17/) 分支 |
* 【完整版】包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
* 【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
可参考 [《迁移文档》](https://doc.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
## 🐯 平台简介
**芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
@ -31,7 +43,7 @@
![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
* Java 后端:`master` 分支为 JDK 8 + Spring Boot 2.7.18`master-jdk21` 分支为 JDK21 + Spring Boot 3.2.0
* Java 后端:`master` 分支为 JDK 8 + Spring Boot 2.7`master-jdk17` 分支为 JDK 17/21 + Spring Boot 3.2
* 管理后台的电脑端Vue3 提供 `element-plus`、`vben(ant-design-vue)` 两个版本Vue2 提供 `element-ui` 版本
* 管理后台的移动端:采用 `uni-app` 方案,一份代码多终端适配,同时支持 APP、小程序、H5
* 后端采用 Spring Boot 多模块架构、MySQL + MyBatis Plus、Redis + Redisson
@ -72,28 +84,6 @@
| [yudao-ui-admin-uniapp](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-uniapp) | 基于 Vue2 + element-ui 实现的管理后台 |
| [yudao-ui-go-view](https://gitee.com/yudaocode/yudao-ui-go-view) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-go-view/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-go-view) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-go-view.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-go-view) | 基于 Vue3 + naive-ui 实现的大屏报表 |
## 🐰 分支说明
### ⬅️ 完整版
【完整版】包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM 等功能
* JDK 8 + Spring Boot 2.7.18 版本:<https://gitee.com/zhijiantianya/ruoyi-vue-pro>`master` 分支
* JDK 21 + Spring Boot 3.2.0 版本:<https://gitee.com/zhijiantianya/ruoyi-vue-pro>`master-jdk21` 分支
两个分支的功能是一致的,可以放心使用!
### ➡️️ 精简版
【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM 等功能
* JDK 8 + Spring Boot 2.7.18 版本:<https://gitee.com/yudaocode/yudao-boot-mini>`master` 分支
* JDK 21 + Spring Boot 3.2.0 版本:<https://gitee.com/yudaocode/yudao-boot-mini>`master-jdk21` 分支
如果你想把【完整版】的功能,迁移到【精简版】,可以参考 [《迁移功能到精简版》](https://doc.iocoder.cn/migrate-module/) 文档。
如果你想把【完整版】的功能,迁移到【精简版】,可以参考 [《迁移功能到精简版》](https://doc.iocoder.cn/migrate-module/) 文档。
## 😎 开源协议
**为什么推荐使用本项目?**
@ -120,16 +110,9 @@
![功能分层](/.image/common/ruoyi-vue-pro-biz.png)
* 系统功能
* 基础设施
* 工作流程
* 支付系统
* 会员中心
* 数据报表
* 商城系统
* 微信公众号
* ERP 系统
* CRM 系统
* 通用模块(必选):系统功能、基础设施
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
* 业务系统按需ERP 系统、CRM 系统、商城系统、微信公众号
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
>

View File

@ -23,8 +23,6 @@
<!-- <module>yudao-module-mall</module>-->
<!-- <module>yudao-module-crm</module>-->
<!-- <module>yudao-module-erp</module>-->
<!-- 示例项目 -->
<!-- <module>yudao-example</module>-->
</modules>
<name>${project.artifactId}</name>
@ -34,7 +32,7 @@
<properties>
<revision>2.0.1-snapshot</revision>
<!-- Maven 相关 -->
<java.version>21</java.version>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +0,0 @@
<?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>

View File

@ -1,65 +0,0 @@
<?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>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 统一依赖管理 -->
<spring.boot.version>3.2.0</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.22</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -1,13 +0,0 @@
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);
}
}

View File

@ -1,157 +0,0 @@
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);
}
}

View File

@ -1,73 +0,0 @@
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());
}
}

View File

@ -1,28 +0,0 @@
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;
}

View File

@ -1,45 +0,0 @@
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;
}

View File

@ -1,59 +0,0 @@
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;
}

View File

@ -1,97 +0,0 @@
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;
}
}

View File

@ -1,35 +0,0 @@
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;
}

View File

@ -1,63 +0,0 @@
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 jakarta.annotation.Resource;
import jakarta.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<>();
}
}

View File

@ -1,40 +0,0 @@
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 jakarta.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);
}
}

View File

@ -1,58 +0,0 @@
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.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import jakarta.annotation.Resource;
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfiguration{
@Resource
private TokenAuthenticationFilter tokenAuthenticationFilter;
@Resource
private AccessDeniedHandlerImpl accessDeniedHandler;
@Resource
private AuthenticationEntryPoint authenticationEntryPoint;
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// 设置 URL 安全权限
httpSecurity
// 开启跨域
.cors(Customizer.withDefaults())
// CSRF 禁用因为不使用 Session
.csrf(AbstractHttpConfigurer::disable)
// 一堆自定义的 Spring Security 处理器
.exceptionHandling(c -> c.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler));
// 设置每个请求的权限
httpSecurity.authorizeHttpRequests(c -> c
// 1. 静态资源可匿名访问
.requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll()
// 2. 登录相关的接口可匿名访问
.requestMatchers("/auth/login-by-code").permitAll()
.requestMatchers("/auth/refresh-token").permitAll()
.requestMatchers("/auth/logout").permitAll())
// 3. 兜底规则必须认证
.authorizeHttpRequests(c -> c.anyRequest().authenticated());
// 添加 Token Filter
httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
}

View File

@ -1,37 +0,0 @@
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;
}

View File

@ -1,66 +0,0 @@
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 jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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;
}
}
}

View File

@ -1,44 +0,0 @@
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 jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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);
}
}

View File

@ -1,36 +0,0 @@
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 jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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);
}
}

View File

@ -1,103 +0,0 @@
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 jakarta.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;
}
}

View File

@ -1,33 +0,0 @@
package cn.iocoder.yudao.ssodemo.framework.core.util;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.http.MediaType;
import jakarta.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);
JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
}
public static void write(HttpServletResponse response, String text, String contentType) {
JakartaServletUtil.write(response, text, contentType);
}
}

View File

@ -1,2 +0,0 @@
server:
port: 18080

View File

@ -1,61 +0,0 @@
<!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>

View File

@ -1,159 +0,0 @@
<!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授权码模式对应 code2简化模式对应 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>

View File

@ -1,65 +0,0 @@
<?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>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 统一依赖管理 -->
<spring.boot.version>3.2.0</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.22</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -1,13 +0,0 @@
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);
}
}

View File

@ -1,127 +0,0 @@
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);
}
}

View File

@ -1,73 +0,0 @@
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());
}
}

View File

@ -1,28 +0,0 @@
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;
}

View File

@ -1,45 +0,0 @@
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;
}

View File

@ -1,59 +0,0 @@
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;
}

View File

@ -1,97 +0,0 @@
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;
}
}

View File

@ -1,35 +0,0 @@
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;
}

View File

@ -1,50 +0,0 @@
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 jakarta.annotation.Resource;
import jakarta.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<>();
}
}

View File

@ -1,40 +0,0 @@
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 jakarta.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);
}
}

View File

@ -1,58 +0,0 @@
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.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import jakarta.annotation.Resource;
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfiguration {
@Resource
private TokenAuthenticationFilter tokenAuthenticationFilter;
@Resource
private AccessDeniedHandlerImpl accessDeniedHandler;
@Resource
private AuthenticationEntryPoint authenticationEntryPoint;
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// 设置 URL 安全权限
httpSecurity
// 开启跨域
.cors(Customizer.withDefaults())
// CSRF 禁用因为不使用 Session
.csrf(AbstractHttpConfigurer::disable)
// 一堆自定义的 Spring Security 处理器
.exceptionHandling(c -> c.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler));
// 设置每个请求的权限
httpSecurity.authorizeHttpRequests(c -> c
// 1. 静态资源可匿名访问
.requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll()
// 2. 登录相关的接口可匿名访问
.requestMatchers("/auth/login-by-code").permitAll()
.requestMatchers("/auth/refresh-token").permitAll()
.requestMatchers("/auth/logout").permitAll())
// 3. 兜底规则必须认证
.authorizeHttpRequests(c -> c.anyRequest().authenticated());
// 添加 Token Filter
httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
}

View File

@ -1,37 +0,0 @@
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;
}

View File

@ -1,66 +0,0 @@
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 jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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;
}
}
}

View File

@ -1,44 +0,0 @@
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 jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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);
}
}

View File

@ -1,36 +0,0 @@
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 jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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);
}
}

View File

@ -1,103 +0,0 @@
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 jakarta.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;
}
}

View File

@ -1,33 +0,0 @@
package cn.iocoder.yudao.ssodemo.framework.core.util;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.http.MediaType;
import jakarta.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);
JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
}
public static void write(HttpServletResponse response, String text, String contentType) {
JakartaServletUtil.write(response, text, contentType);
}
}

View File

@ -1,154 +0,0 @@
<!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>

View File

@ -1,74 +0,0 @@
<!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>

View File

@ -227,27 +227,27 @@ public class LocalDateTimeUtils {
// 2. 循环生成时间范围
List<LocalDateTime[]> timeRanges = new ArrayList<>();
switch (intervalEnum) {
case DateIntervalEnum.DAY:
case DAY:
while (startTime.isBefore(endTime)) {
timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)});
startTime = startTime.plusDays(1);
}
break;
case DateIntervalEnum.WEEK:
case WEEK:
while (startTime.isBefore(endTime)) {
LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1);
timeRanges.add(new LocalDateTime[]{startTime, endOfWeek});
startTime = endOfWeek.plusNanos(1);
}
break;
case DateIntervalEnum.MONTH:
case MONTH:
while (startTime.isBefore(endTime)) {
LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1);
timeRanges.add(new LocalDateTime[]{startTime, endOfMonth});
startTime = endOfMonth.plusNanos(1);
}
break;
case DateIntervalEnum.QUARTER:
case QUARTER:
while (startTime.isBefore(endTime)) {
int quarterOfYear = getQuarterOfYear(startTime);
LocalDateTime quarterEnd = quarterOfYear == 4
@ -257,7 +257,7 @@ public class LocalDateTimeUtils {
startTime = quarterEnd.plusNanos(1);
}
break;
case DateIntervalEnum.YEAR:
case YEAR:
while (startTime.isBefore(endTime)) {
LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1);
timeRanges.add(new LocalDateTime[]{startTime, endOfYear});
@ -290,16 +290,16 @@ public class LocalDateTimeUtils {
// 2. 循环生成时间范围
switch (intervalEnum) {
case DateIntervalEnum.DAY:
case DAY:
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN);
case DateIntervalEnum.WEEK:
case WEEK:
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN)
+ StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime));
case DateIntervalEnum.MONTH:
case MONTH:
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN);
case DateIntervalEnum.QUARTER:
case QUARTER:
return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime));
case DateIntervalEnum.YEAR:
case YEAR:
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN);
default:
throw new IllegalArgumentException("Invalid interval: " + interval);

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.mybatis.config;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.extension.incrementer.*;
@ -20,7 +21,7 @@ import org.springframework.core.env.ConfigurableEnvironment;
*
* @author 芋道源码
*/
@AutoConfiguration
@AutoConfiguration(before = MybatisPlusAutoConfiguration.class) // 目的先于 MyBatis Plus 自动配置避免 @MapperScan 可能扫描不到 Mapper 打印 warn 日志
@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class,
lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载目前仅用于单元测试
public class YudaoMybatisAutoConfiguration {

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.redis.config;
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.redisson.spring.starter.RedissonAutoConfigurationV2;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
@ -12,7 +13,7 @@ import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Redis 配置类
*/
@AutoConfiguration
@AutoConfiguration(before = RedissonAutoConfigurationV2.class) // 目的使用自己定义的 RedisTemplate Bean
public class YudaoRedisAutoConfiguration {
/**

View File

@ -10,8 +10,10 @@ import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.context.SecurityContextHolder;
@ -20,8 +22,6 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import jakarta.annotation.Resource;
/**
* Spring Security 自动配置类主要用于相关组件的配置
*
@ -31,6 +31,7 @@ import jakarta.annotation.Resource;
* @author 芋道源码
*/
@AutoConfiguration
@AutoConfigureOrder(-1) // 目的先于 Spring Security 自动配置避免一键改包后org.* 基础包无法生效
@EnableConfigurationProperties(SecurityProperties.class)
public class YudaoSecurityAutoConfiguration {

View File

@ -5,14 +5,16 @@ import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter
import cn.iocoder.yudao.framework.web.config.WebProperties;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@ -27,8 +29,6 @@ import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -39,6 +39,7 @@ import java.util.Set;
* @author 芋道源码
*/
@AutoConfiguration
@AutoConfigureOrder(-1) // 目的先于 Spring Security 自动配置避免一键改包后org.* 基础包无法生效
@EnableMethodSecurity(securedEnabled = true)
public class YudaoWebSecurityConfigurerAdapter {

View File

@ -43,7 +43,7 @@ public class BaseDbAndRedisUnitTest {
RedisTestConfiguration.class, // Redis 测试配置类用于启动 RedisServer
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedisAutoConfiguration.class, // Spring Redis 自动配置类
RedissonAutoConfiguration.class, // Redisson 自动配置类
RedissonAutoConfiguration.class, // Redisson 自动配置类
})
public static class Application {
}

View File

@ -24,7 +24,7 @@ public class BaseRedisUnitTest {
RedisTestConfiguration.class, // Redis 测试配置类用于启动 RedisServer
RedisAutoConfiguration.class, // Spring Redis 自动配置类
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedissonAutoConfiguration.class, // Redisson 自动配置类
RedissonAutoConfiguration.class, // Redisson 自动配置类
})
public static class Application {
}

View File

@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpHeaders;
import java.util.HashMap;
@ -91,6 +92,7 @@ public class YudaoSwaggerAutoConfiguration {
* 自定义 OpenAPI 处理器
*/
@Bean
@Primary // 目的以我们创建的 OpenAPIService Bean 为主避免一键改包后启动报错
public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties,
@ -98,7 +100,6 @@ public class YudaoSwaggerAutoConfiguration {
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
Optional<JavadocProvider> javadocProvider) {
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties,
propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
}

View File

@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@ -122,7 +123,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
* @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
*/
@Bean
@ConditionalOnMissingBean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
}

View File

@ -1,20 +1,20 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 示例联系人 DO
*
* @author 芋道源码
*/
@TableName("infra_demo01_contact")
@KeySequence("infra_demo01_contact_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@TableName("yudao_demo01_contact")
@KeySequence("yudao_demo01_contact_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

View File

@ -1,19 +1,18 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 示例分类 DO
*
* @author 芋道源码
*/
@TableName("infra_demo02_category")
@KeySequence("infra_demo02_category_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@TableName("yudao_demo02_category")
@KeySequence("yudao_demo02_category_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

View File

@ -1,19 +1,18 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 学生课程 DO
*
* @author 芋道源码
*/
@TableName("infra_demo03_course")
@KeySequence("infra_demo03_course_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@TableName("yudao_demo03_course")
@KeySequence("yudao_demo03_course_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

View File

@ -1,19 +1,18 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 学生班级 DO
*
* @author 芋道源码
*/
@TableName("infra_demo03_grade")
@KeySequence("infra_demo03_grade_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@TableName("yudao_demo03_grade")
@KeySequence("yudao_demo03_grade_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

View File

@ -1,20 +1,20 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 学生 DO
*
* @author 芋道源码
*/
@TableName("infra_demo03_student")
@KeySequence("infra_demo03_student_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@TableName("yudao_demo03_student")
@KeySequence("yudao_demo03_student_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

View File

@ -24,12 +24,6 @@ public class ProductSpuRespDTO {
* 商品名称
*/
private String name;
/**
* 单位
*
* 对应 product_unit 数据字典
*/
private Integer unit;
/**
* 商品分类编号

View File

@ -119,7 +119,7 @@ public class CouponServiceImpl implements CouponService {
Integer status = LocalDateTimeUtils.beforeNow(coupon.getValidEndTime())
? CouponStatusEnum.EXPIRE.getStatus() // 退还时可能已经过期了
: CouponStatusEnum.UNUSED.getStatus();
int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(),
int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.USED.getStatus(),
new CouponDO().setStatus(status));
if (updateCount == 0) {
throw exception(COUPON_STATUS_NOT_USED);

View File

@ -30,7 +30,7 @@ public class BaseDbAndRedisIntegrationTest {
// Redis 配置类
RedisAutoConfiguration.class, // Spring Redis 自动配置类
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedissonAutoConfiguration.class, // Redisson 自动配置类
RedissonAutoConfiguration.class, // Redisson 自动配置类
})
public static class Application {
}

View File

@ -15,7 +15,7 @@ public class BaseRedisIntegrationTest {
// Redis 配置类
RedisAutoConfiguration.class, // Spring Redis 自动配置类
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedissonAutoConfiguration.class, // Redisson 自动配置类
RedissonAutoConfiguration.class, // Redisson 自动配置类
})
public static class Application {
}

View File

@ -26,7 +26,7 @@ public class SmsCallbackController {
@PostMapping("/aliyun")
@PermitAll
@Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/document_detail/120998.html 文档")
@Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/zh/sms/developer-reference/configure-delivery-receipts-1 文档")
public CommonResult<Boolean> receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable {
String text = ServletUtils.getBody(request);
smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text);
@ -35,7 +35,7 @@ public class SmsCallbackController {
@PostMapping("/tencent")
@PermitAll
@Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/52077 文档")
@Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/59178 文档")
public CommonResult<Boolean> receiveTencentSmsStatus(HttpServletRequest request) throws Throwable {
String text = ServletUtils.getBody(request);
smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text);

View File

@ -34,7 +34,8 @@ public class ProjectReactor {
* 白名单文件不进行重写避免出问题
*/
private static final Set<String> WHITE_FILE_TYPES = SetUtils.asSet("gif", "jpg", "svg", "png", // 图片
"eot", "woff2", "ttf", "woff"); // 字体
"eot", "woff2", "ttf", "woff", // 字体
"xdb"); // IP
public static void main(String[] args) {
long start = System.currentTimeMillis();