mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-11-04 12:18:42 +08:00 
			
		
		
		
	【移除】移除 yudao-example 项目,统一到 https://gitee.com/yudaocode/yudao-demo
This commit is contained in:
		
							
								
								
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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<>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,2 +0,0 @@
 | 
			
		||||
server:
 | 
			
		||||
  port: 18080
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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)授权码模式,对应 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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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<>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,2 +0,0 @@
 | 
			
		||||
server:
 | 
			
		||||
  port: 18080
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user