mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-10-31 10:18:42 +08:00 
			
		
		
		
	!272 增加“基于授权码模式,实现 SSO 单点登录“示例
Merge pull request !272 from 芋道源码/feature/sso-example
This commit is contained in:
		
							
								
								
									
										1
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								pom.xml
									
									
									
									
									
								
							| @@ -20,6 +20,7 @@ | |||||||
|         <module>yudao-module-pay</module> |         <module>yudao-module-pay</module> | ||||||
|         <module>yudao-module-mall</module> |         <module>yudao-module-mall</module> | ||||||
|         <module>yudao-module-visualization</module> |         <module>yudao-module-visualization</module> | ||||||
|  |         <module>yudao-example</module> | ||||||
|     </modules> |     </modules> | ||||||
|  |  | ||||||
|     <name>${project.artifactId}</name> |     <name>${project.artifactId}</name> | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								yudao-example/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								yudao-example/pom.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | <?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> | ||||||
|  |     </modules> | ||||||
|  |  | ||||||
|  |     <name>${project.artifactId}</name> | ||||||
|  |     <description>提供各种示例,例如说:SSO 单点登录</description> | ||||||
|  |     <url>https://github.com/YunaiV/ruoyi-vue-pro</url> | ||||||
|  |  | ||||||
|  | </project> | ||||||
							
								
								
									
										65
									
								
								yudao-example/yudao-sso-demo-by-code/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								yudao-example/yudao-sso-demo-by-code/pom.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project xmlns="http://maven.apache.org/POM/4.0.0" | ||||||
|  |          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||||
|  |          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||||
|  |     <modelVersion>4.0.0</modelVersion> | ||||||
|  |  | ||||||
|  |     <!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent --> | ||||||
|  |  | ||||||
|  |     <groupId>cn.iocoder.boot</groupId> | ||||||
|  |     <artifactId>yudao-sso-demo-by-code</artifactId> | ||||||
|  |     <version>1.0.0-snapshot</version> | ||||||
|  |     <packaging>jar</packaging> | ||||||
|  |  | ||||||
|  |     <name>${project.artifactId}</name> | ||||||
|  |     <description>基于授权码模式,如何实现 SSO 单点登录?</description> | ||||||
|  |     <url>https://github.com/YunaiV/ruoyi-vue-pro</url> | ||||||
|  |  | ||||||
|  |     <properties> | ||||||
|  |         <!-- Maven 相关 --> | ||||||
|  |         <maven.compiler.source>8</maven.compiler.source> | ||||||
|  |         <maven.compiler.target>8</maven.compiler.target> | ||||||
|  |         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||||||
|  |         <!-- 统一依赖管理 --> | ||||||
|  |         <spring.boot.version>2.6.10</spring.boot.version> | ||||||
|  |     </properties> | ||||||
|  |  | ||||||
|  |     <dependencyManagement> | ||||||
|  |         <dependencies> | ||||||
|  |             <!-- 统一依赖管理 --> | ||||||
|  |             <dependency> | ||||||
|  |                 <groupId>org.springframework.boot</groupId> | ||||||
|  |                 <artifactId>spring-boot-dependencies</artifactId> | ||||||
|  |                 <version>${spring.boot.version}</version> | ||||||
|  |                 <type>pom</type> | ||||||
|  |                 <scope>import</scope> | ||||||
|  |             </dependency> | ||||||
|  |         </dependencies> | ||||||
|  |     </dependencyManagement> | ||||||
|  |  | ||||||
|  |     <dependencies> | ||||||
|  |         <!-- Web 相关 --> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>org.springframework.boot</groupId> | ||||||
|  |             <artifactId>spring-boot-starter-web</artifactId> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>org.springframework.boot</groupId> | ||||||
|  |             <artifactId>spring-boot-starter-security</artifactId> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>cn.hutool</groupId> | ||||||
|  |             <artifactId>hutool-all</artifactId> | ||||||
|  |             <version>5.8.5</version> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>org.projectlombok</groupId> | ||||||
|  |             <artifactId>lombok</artifactId> | ||||||
|  |             <optional>true</optional> | ||||||
|  |         </dependency> | ||||||
|  |     </dependencies> | ||||||
|  |  | ||||||
|  | </project> | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo; | ||||||
|  |  | ||||||
|  | import org.springframework.boot.SpringApplication; | ||||||
|  | import org.springframework.boot.autoconfigure.SpringBootApplication; | ||||||
|  |  | ||||||
|  | @SpringBootApplication | ||||||
|  | public class SSODemoApplication { | ||||||
|  |  | ||||||
|  |     public static void main(String[] args) { | ||||||
|  |         SpringApplication.run(SSODemoApplication.class, args); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,157 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.client; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; | ||||||
|  | import org.springframework.core.ParameterizedTypeReference; | ||||||
|  | import org.springframework.http.*; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.util.Base64Utils; | ||||||
|  | import org.springframework.util.LinkedMultiValueMap; | ||||||
|  | import org.springframework.util.MultiValueMap; | ||||||
|  | import org.springframework.web.client.RestTemplate; | ||||||
|  |  | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * OAuth 2.0 客户端 | ||||||
|  |  * | ||||||
|  |  * 对应调用 OAuth2OpenController 接口 | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | public class OAuth2Client { | ||||||
|  |  | ||||||
|  |     private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 租户编号 | ||||||
|  |      * | ||||||
|  |      * 默认使用 1;如果使用别的租户,可以调整 | ||||||
|  |      */ | ||||||
|  |     public static final Long TENANT_ID = 1L; | ||||||
|  |  | ||||||
|  |     private static final String CLIENT_ID = "yudao-sso-demo-by-code"; | ||||||
|  |     private static final String CLIENT_SECRET = "test"; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | //    @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 | ||||||
|  |     private final RestTemplate restTemplate = new RestTemplate(); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 使用 code 授权码,获得访问令牌 | ||||||
|  |      * | ||||||
|  |      * @param code        授权码 | ||||||
|  |      * @param redirectUri 重定向 URI | ||||||
|  |      * @return 访问令牌 | ||||||
|  |      */ | ||||||
|  |     public CommonResult<OAuth2AccessTokenRespDTO> postAccessToken(String code, String redirectUri) { | ||||||
|  |         // 1.1 构建请求头 | ||||||
|  |         HttpHeaders headers = new HttpHeaders(); | ||||||
|  |         headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||||||
|  |         headers.set("tenant-id", TENANT_ID.toString()); | ||||||
|  |         addClientHeader(headers); | ||||||
|  |         // 1.2 构建请求参数 | ||||||
|  |         MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); | ||||||
|  |         body.add("grant_type", "authorization_code"); | ||||||
|  |         body.add("code", code); | ||||||
|  |         body.add("redirect_uri", redirectUri); | ||||||
|  | //        body.add("state", ""); // 选填;填了会校验 | ||||||
|  |  | ||||||
|  |         // 2. 执行请求 | ||||||
|  |         ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange( | ||||||
|  |                 BASE_URL + "/token", | ||||||
|  |                 HttpMethod.POST, | ||||||
|  |                 new HttpEntity<>(body, headers), | ||||||
|  |                 new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失 | ||||||
|  |         Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); | ||||||
|  |         return exchange.getBody(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 校验访问令牌,并返回它的基本信息 | ||||||
|  |      * | ||||||
|  |      * @param token 访问令牌 | ||||||
|  |      * @return 访问令牌的基本信息 | ||||||
|  |      */ | ||||||
|  |     public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) { | ||||||
|  |         // 1.1 构建请求头 | ||||||
|  |         HttpHeaders headers = new HttpHeaders(); | ||||||
|  |         headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||||||
|  |         headers.set("tenant-id", TENANT_ID.toString()); | ||||||
|  |         addClientHeader(headers); | ||||||
|  |         // 1.2 构建请求参数 | ||||||
|  |         MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); | ||||||
|  |         body.add("token", token); | ||||||
|  |  | ||||||
|  |         // 2. 执行请求 | ||||||
|  |         ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange( | ||||||
|  |                 BASE_URL + "/check-token", | ||||||
|  |                 HttpMethod.POST, | ||||||
|  |                 new HttpEntity<>(body, headers), | ||||||
|  |                 new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失 | ||||||
|  |         Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); | ||||||
|  |         return exchange.getBody(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 使用刷新令牌,获得(刷新)访问令牌 | ||||||
|  |      * | ||||||
|  |      * @param refreshToken 刷新令牌 | ||||||
|  |      * @return 访问令牌 | ||||||
|  |      */ | ||||||
|  |     public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(String refreshToken) { | ||||||
|  |         // 1.1 构建请求头 | ||||||
|  |         HttpHeaders headers = new HttpHeaders(); | ||||||
|  |         headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||||||
|  |         headers.set("tenant-id", TENANT_ID.toString()); | ||||||
|  |         addClientHeader(headers); | ||||||
|  |         // 1.2 构建请求参数 | ||||||
|  |         MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); | ||||||
|  |         body.add("grant_type", "refresh_token"); | ||||||
|  |         body.add("refresh_token", refreshToken); | ||||||
|  |  | ||||||
|  |         // 2. 执行请求 | ||||||
|  |         ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange( | ||||||
|  |                 BASE_URL + "/token", | ||||||
|  |                 HttpMethod.POST, | ||||||
|  |                 new HttpEntity<>(body, headers), | ||||||
|  |                 new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失 | ||||||
|  |         Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); | ||||||
|  |         return exchange.getBody(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 删除访问令牌 | ||||||
|  |      * | ||||||
|  |      * @param token 访问令牌 | ||||||
|  |      * @return 成功 | ||||||
|  |      */ | ||||||
|  |     public CommonResult<Boolean> revokeToken(String token) { | ||||||
|  |         // 1.1 构建请求头 | ||||||
|  |         HttpHeaders headers = new HttpHeaders(); | ||||||
|  |         headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||||||
|  |         headers.set("tenant-id", TENANT_ID.toString()); | ||||||
|  |         addClientHeader(headers); | ||||||
|  |         // 1.2 构建请求参数 | ||||||
|  |         MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); | ||||||
|  |         body.add("token", token); | ||||||
|  |  | ||||||
|  |         // 2. 执行请求 | ||||||
|  |         ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange( | ||||||
|  |                 BASE_URL + "/token", | ||||||
|  |                 HttpMethod.DELETE, | ||||||
|  |                 new HttpEntity<>(body, headers), | ||||||
|  |                 new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失 | ||||||
|  |         Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); | ||||||
|  |         return exchange.getBody(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static void addClientHeader(HttpHeaders headers) { | ||||||
|  |         // client 拼接,需要 BASE64 编码 | ||||||
|  |         String client = CLIENT_ID + ":" + CLIENT_SECRET; | ||||||
|  |         client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8)); | ||||||
|  |         headers.add("Authorization", "Basic " + client); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,73 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.client; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO; | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; | ||||||
|  | import org.springframework.core.ParameterizedTypeReference; | ||||||
|  | import org.springframework.http.*; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.util.LinkedMultiValueMap; | ||||||
|  | import org.springframework.util.MultiValueMap; | ||||||
|  | import org.springframework.web.client.RestTemplate; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 用户 User 信息的客户端 | ||||||
|  |  * | ||||||
|  |  * 对应调用 OAuth2UserController 接口 | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | public class UserClient { | ||||||
|  |  | ||||||
|  |     private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user"; | ||||||
|  |  | ||||||
|  |     //    @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 | ||||||
|  |     private final RestTemplate restTemplate = new RestTemplate(); | ||||||
|  |  | ||||||
|  |     public CommonResult<UserInfoRespDTO> getUser() { | ||||||
|  |         // 1.1 构建请求头 | ||||||
|  |         HttpHeaders headers = new HttpHeaders(); | ||||||
|  |         headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||||||
|  |         headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); | ||||||
|  |         addTokenHeader(headers); | ||||||
|  |         // 1.2 构建请求参数 | ||||||
|  |         MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); | ||||||
|  |  | ||||||
|  |         // 2. 执行请求 | ||||||
|  |         ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange( | ||||||
|  |                 BASE_URL + "/get", | ||||||
|  |                 HttpMethod.GET, | ||||||
|  |                 new HttpEntity<>(body, headers), | ||||||
|  |                 new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失 | ||||||
|  |         Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); | ||||||
|  |         return exchange.getBody(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public CommonResult<Boolean> updateUser(UserUpdateReqDTO updateReqDTO) { | ||||||
|  |         // 1.1 构建请求头 | ||||||
|  |         HttpHeaders headers = new HttpHeaders(); | ||||||
|  |         headers.setContentType(MediaType.APPLICATION_JSON); | ||||||
|  |         headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); | ||||||
|  |         addTokenHeader(headers); | ||||||
|  |         // 1.2 构建请求参数 | ||||||
|  |         // 使用 updateReqDTO 即可 | ||||||
|  |  | ||||||
|  |         // 2. 执行请求 | ||||||
|  |         ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange( | ||||||
|  |                 BASE_URL + "/update", | ||||||
|  |                 HttpMethod.PUT, | ||||||
|  |                 new HttpEntity<>(updateReqDTO, headers), | ||||||
|  |                 new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失 | ||||||
|  |         Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); | ||||||
|  |         return exchange.getBody(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private static void addTokenHeader(HttpHeaders headers) { | ||||||
|  |         LoginUser loginUser = SecurityUtils.getLoginUser(); | ||||||
|  |         Assert.notNull(loginUser, "登录用户不能为空"); | ||||||
|  |         headers.add("Authorization", "Bearer " + loginUser.getAccessToken()); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.client.dto; | ||||||
|  |  | ||||||
|  | import lombok.Data; | ||||||
|  |  | ||||||
|  | import java.io.Serializable; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 通用返回 | ||||||
|  |  * | ||||||
|  |  * @param <T> 数据泛型 | ||||||
|  |  */ | ||||||
|  | @Data | ||||||
|  | public class CommonResult<T> implements Serializable { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 错误码 | ||||||
|  |      */ | ||||||
|  |     private Integer code; | ||||||
|  |     /** | ||||||
|  |      * 返回数据 | ||||||
|  |      */ | ||||||
|  |     private T data; | ||||||
|  |     /** | ||||||
|  |      * 错误提示,用户可阅读 | ||||||
|  |      */ | ||||||
|  |     private String msg; | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,45 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.client.dto.oauth2; | ||||||
|  |  | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty; | ||||||
|  | import lombok.AllArgsConstructor; | ||||||
|  | import lombok.Data; | ||||||
|  | import lombok.NoArgsConstructor; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 访问令牌 Response DTO | ||||||
|  |  */ | ||||||
|  | @Data | ||||||
|  | @NoArgsConstructor | ||||||
|  | @AllArgsConstructor | ||||||
|  | public class OAuth2AccessTokenRespDTO { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 访问令牌 | ||||||
|  |      */ | ||||||
|  |     @JsonProperty("access_token") | ||||||
|  |     private String accessToken; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 刷新令牌 | ||||||
|  |      */ | ||||||
|  |     @JsonProperty("refresh_token") | ||||||
|  |     private String refreshToken; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 令牌类型 | ||||||
|  |      */ | ||||||
|  |     @JsonProperty("token_type") | ||||||
|  |     private String tokenType; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 过期时间;单位:秒 | ||||||
|  |      */ | ||||||
|  |     @JsonProperty("expires_in") | ||||||
|  |     private Long expiresIn; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 授权范围;如果多个授权范围,使用空格分隔 | ||||||
|  |      */ | ||||||
|  |     private String scope; | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,59 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.client.dto.oauth2; | ||||||
|  |  | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty; | ||||||
|  | import lombok.AllArgsConstructor; | ||||||
|  | import lombok.Data; | ||||||
|  | import lombok.NoArgsConstructor; | ||||||
|  |  | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 校验令牌 Response DTO | ||||||
|  |  * | ||||||
|  |  * @author 芋道源码 | ||||||
|  |  */ | ||||||
|  | @Data | ||||||
|  | @NoArgsConstructor | ||||||
|  | @AllArgsConstructor | ||||||
|  | public class OAuth2CheckTokenRespDTO { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户编号 | ||||||
|  |      */ | ||||||
|  |     @JsonProperty("user_id") | ||||||
|  |     private Long userId; | ||||||
|  |     /** | ||||||
|  |      * 用户类型 | ||||||
|  |      */ | ||||||
|  |     @JsonProperty("user_type") | ||||||
|  |     private Integer userType; | ||||||
|  |     /** | ||||||
|  |      * 租户编号 | ||||||
|  |      */ | ||||||
|  |     @JsonProperty("tenant_id") | ||||||
|  |     private Long tenantId; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 客户端编号 | ||||||
|  |      */ | ||||||
|  |     @JsonProperty("client_id") | ||||||
|  |     private String clientId; | ||||||
|  |     /** | ||||||
|  |      * 授权范围 | ||||||
|  |      */ | ||||||
|  |     private List<String> scopes; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 访问令牌 | ||||||
|  |      */ | ||||||
|  |     @JsonProperty("access_token") | ||||||
|  |     private String accessToken; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 过期时间 | ||||||
|  |      * | ||||||
|  |      * 时间戳 / 1000,即单位:秒 | ||||||
|  |      */ | ||||||
|  |     private Long exp; | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,97 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.client.dto.user; | ||||||
|  |  | ||||||
|  | import lombok.AllArgsConstructor; | ||||||
|  | import lombok.Data; | ||||||
|  | import lombok.NoArgsConstructor; | ||||||
|  |  | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获得用户基本信息 Response dto | ||||||
|  |  */ | ||||||
|  | @Data | ||||||
|  | @NoArgsConstructor | ||||||
|  | @AllArgsConstructor | ||||||
|  | public class UserInfoRespDTO { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户编号 | ||||||
|  |      */ | ||||||
|  |     private Long id; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户账号 | ||||||
|  |      */ | ||||||
|  |     private String username; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户昵称 | ||||||
|  |      */ | ||||||
|  |     private String nickname; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户邮箱 | ||||||
|  |      */ | ||||||
|  |     private String email; | ||||||
|  |     /** | ||||||
|  |      * 手机号码 | ||||||
|  |      */ | ||||||
|  |     private String mobile; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户性别 | ||||||
|  |      */ | ||||||
|  |     private Integer sex; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户头像 | ||||||
|  |      */ | ||||||
|  |     private String avatar; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 所在部门 | ||||||
|  |      */ | ||||||
|  |     private Dept dept; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 所属岗位数组 | ||||||
|  |      */ | ||||||
|  |     private List<Post> posts; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 部门 | ||||||
|  |      */ | ||||||
|  |     @Data | ||||||
|  |     public static class Dept { | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 部门编号 | ||||||
|  |          */ | ||||||
|  |         private Long id; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 部门名称 | ||||||
|  |          */ | ||||||
|  |         private String name; | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 岗位 | ||||||
|  |      */ | ||||||
|  |     @Data | ||||||
|  |     public static class Post { | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 岗位编号 | ||||||
|  |          */ | ||||||
|  |         private Long id; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 岗位名称 | ||||||
|  |          */ | ||||||
|  |         private String name; | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,35 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.client.dto.user; | ||||||
|  |  | ||||||
|  | import lombok.AllArgsConstructor; | ||||||
|  | import lombok.Data; | ||||||
|  | import lombok.NoArgsConstructor; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 更新用户基本信息 Request DTO | ||||||
|  |  */ | ||||||
|  | @Data | ||||||
|  | @NoArgsConstructor | ||||||
|  | @AllArgsConstructor | ||||||
|  | public class UserUpdateReqDTO { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户昵称 | ||||||
|  |      */ | ||||||
|  |     private String nickname; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户邮箱 | ||||||
|  |      */ | ||||||
|  |     private String email; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 手机号码 | ||||||
|  |      */ | ||||||
|  |     private String mobile; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户性别 | ||||||
|  |      */ | ||||||
|  |     private Integer sex; | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,63 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.controller; | ||||||
|  |  | ||||||
|  | import cn.hutool.core.util.StrUtil; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.OAuth2Client; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; | ||||||
|  | import org.springframework.web.bind.annotation.PostMapping; | ||||||
|  | import org.springframework.web.bind.annotation.RequestMapping; | ||||||
|  | import org.springframework.web.bind.annotation.RequestParam; | ||||||
|  | import org.springframework.web.bind.annotation.RestController; | ||||||
|  |  | ||||||
|  | import javax.annotation.Resource; | ||||||
|  | import javax.servlet.http.HttpServletRequest; | ||||||
|  |  | ||||||
|  | @RestController | ||||||
|  | @RequestMapping("/auth") | ||||||
|  | public class AuthController { | ||||||
|  |  | ||||||
|  |     @Resource | ||||||
|  |     private OAuth2Client oauth2Client; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 使用 code 访问令牌,获得访问令牌 | ||||||
|  |      * | ||||||
|  |      * @param code 授权码 | ||||||
|  |      * @param redirectUri 重定向 URI | ||||||
|  |      * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 | ||||||
|  |      */ | ||||||
|  |     @PostMapping("/login-by-code") | ||||||
|  |     public CommonResult<OAuth2AccessTokenRespDTO> loginByCode(@RequestParam("code") String code, | ||||||
|  |                                                               @RequestParam("redirectUri") String redirectUri) { | ||||||
|  |         return oauth2Client.postAccessToken(code, redirectUri); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 使用刷新令牌,获得(刷新)访问令牌 | ||||||
|  |      * | ||||||
|  |      * @param refreshToken 刷新令牌 | ||||||
|  |      * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 | ||||||
|  |      */ | ||||||
|  |     @PostMapping("/refresh-token") | ||||||
|  |     public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) { | ||||||
|  |         return oauth2Client.refreshToken(refreshToken); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 退出登录 | ||||||
|  |      * | ||||||
|  |      * @param request 请求 | ||||||
|  |      * @return 成功 | ||||||
|  |      */ | ||||||
|  |     @PostMapping("/logout") | ||||||
|  |     public CommonResult<Boolean> logout(HttpServletRequest request) { | ||||||
|  |         String token = SecurityUtils.obtainAuthorization(request, "Authentication"); | ||||||
|  |         if (StrUtil.isNotBlank(token)) { | ||||||
|  |             return oauth2Client.revokeToken(token); | ||||||
|  |         } | ||||||
|  |         // 返回成功 | ||||||
|  |         return new CommonResult<>(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,40 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.controller; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.UserClient; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO; | ||||||
|  | import org.springframework.web.bind.annotation.*; | ||||||
|  |  | ||||||
|  | import javax.annotation.Resource; | ||||||
|  |  | ||||||
|  | @RestController | ||||||
|  | @RequestMapping("/user") | ||||||
|  | public class UserController { | ||||||
|  |  | ||||||
|  |     @Resource | ||||||
|  |     private UserClient userClient; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获得当前登录用户的基本信息 | ||||||
|  |      * | ||||||
|  |      * @return 用户信息;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 | ||||||
|  |      */ | ||||||
|  |     @GetMapping("/get") | ||||||
|  |     public CommonResult<UserInfoRespDTO> getUser() { | ||||||
|  |         return userClient.getUser(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 更新当前登录用户的昵称 | ||||||
|  |      * | ||||||
|  |      * @param nickname 昵称 | ||||||
|  |      * @return 成功 | ||||||
|  |      */ | ||||||
|  |     @PutMapping("/update") | ||||||
|  |     public CommonResult<Boolean> updateUser(@RequestParam("nickname") String nickname) { | ||||||
|  |         UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null); | ||||||
|  |         return userClient.updateUser(updateReqDTO); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,48 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.framework.config; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.filter.TokenAuthenticationFilter; | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.handler.AccessDeniedHandlerImpl; | ||||||
|  | import org.springframework.context.annotation.Configuration; | ||||||
|  | import org.springframework.http.HttpMethod; | ||||||
|  | import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||||||
|  | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; | ||||||
|  | import org.springframework.security.web.AuthenticationEntryPoint; | ||||||
|  | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||||||
|  |  | ||||||
|  | import javax.annotation.Resource; | ||||||
|  |  | ||||||
|  | @Configuration | ||||||
|  | public class SecurityConfiguration extends WebSecurityConfigurerAdapter { | ||||||
|  |  | ||||||
|  |     @Resource | ||||||
|  |     private TokenAuthenticationFilter tokenAuthenticationFilter; | ||||||
|  |  | ||||||
|  |     @Resource | ||||||
|  |     private AccessDeniedHandlerImpl accessDeniedHandler; | ||||||
|  |     @Resource | ||||||
|  |     private AuthenticationEntryPoint authenticationEntryPoint; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void configure(HttpSecurity httpSecurity) throws Exception { | ||||||
|  |         // 设置 URL 安全权限 | ||||||
|  |         httpSecurity.csrf().disable() // 禁用 CSRF 保护 | ||||||
|  |                 .authorizeRequests() | ||||||
|  |                 // 1. 静态资源,可匿名访问 | ||||||
|  |                 .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() | ||||||
|  |                 // 2. 登录相关的接口,可匿名访问 | ||||||
|  |                 .antMatchers("/auth/login-by-code").permitAll() | ||||||
|  |                 .antMatchers("/auth/refresh-token").permitAll() | ||||||
|  |                 .antMatchers("/auth/logout").permitAll() | ||||||
|  |                 // last. 兜底规则,必须认证 | ||||||
|  |                 .and().authorizeRequests() | ||||||
|  |                 .anyRequest().authenticated(); | ||||||
|  |  | ||||||
|  |         // 设置处理器 | ||||||
|  |         httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler) | ||||||
|  |                 .authenticationEntryPoint(authenticationEntryPoint); | ||||||
|  |  | ||||||
|  |         // 添加 Token Filter | ||||||
|  |         httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.framework.core; | ||||||
|  |  | ||||||
|  | import lombok.Data; | ||||||
|  |  | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 登录用户信息 | ||||||
|  |  * | ||||||
|  |  * @author 芋道源码 | ||||||
|  |  */ | ||||||
|  | @Data | ||||||
|  | public class LoginUser { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 用户编号 | ||||||
|  |      */ | ||||||
|  |     private Long id; | ||||||
|  |     /** | ||||||
|  |      * 用户类型 | ||||||
|  |      */ | ||||||
|  |     private Integer userType; | ||||||
|  |     /** | ||||||
|  |      * 租户编号 | ||||||
|  |      */ | ||||||
|  |     private Long tenantId; | ||||||
|  |     /** | ||||||
|  |      * 授权范围 | ||||||
|  |      */ | ||||||
|  |     private List<String> scopes; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 访问令牌 | ||||||
|  |      */ | ||||||
|  |     private String accessToken; | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,66 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.framework.core.filter; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.OAuth2Client; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  | import org.springframework.util.StringUtils; | ||||||
|  | import org.springframework.web.filter.OncePerRequestFilter; | ||||||
|  |  | ||||||
|  | import javax.annotation.Resource; | ||||||
|  | import javax.servlet.FilterChain; | ||||||
|  | import javax.servlet.ServletException; | ||||||
|  | import javax.servlet.http.HttpServletRequest; | ||||||
|  | import javax.servlet.http.HttpServletResponse; | ||||||
|  | import java.io.IOException; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Token 过滤器,验证 token 的有效性 | ||||||
|  |  * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 | ||||||
|  |  * | ||||||
|  |  * @author 芋道源码 | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | public class TokenAuthenticationFilter extends OncePerRequestFilter { | ||||||
|  |  | ||||||
|  |     @Resource | ||||||
|  |     private OAuth2Client oauth2Client; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, | ||||||
|  |                                     FilterChain filterChain) throws ServletException, IOException { | ||||||
|  |         // 1. 获得访问令牌 | ||||||
|  |         String token = SecurityUtils.obtainAuthorization(request, "Authentication"); | ||||||
|  |         if (StringUtils.hasText(token)) { | ||||||
|  |             // 2. 基于 token 构建登录用户 | ||||||
|  |             LoginUser loginUser = buildLoginUserByToken(token); | ||||||
|  |             // 3. 设置当前用户 | ||||||
|  |             if (loginUser != null) { | ||||||
|  |                 SecurityUtils.setLoginUser(loginUser, request); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 继续过滤链 | ||||||
|  |         filterChain.doFilter(request, response); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private LoginUser buildLoginUserByToken(String token) { | ||||||
|  |         try { | ||||||
|  |             CommonResult<OAuth2CheckTokenRespDTO> accessTokenResult = oauth2Client.checkToken(token); | ||||||
|  |             OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); | ||||||
|  |             if (accessToken == null) { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |             // 构建登录用户 | ||||||
|  |             return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) | ||||||
|  |                     .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) | ||||||
|  |                     .setAccessToken(accessToken.getAccessToken()); | ||||||
|  |         } catch (Exception exception) { | ||||||
|  |             // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,44 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.framework.core.handler; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
|  | import org.springframework.http.HttpStatus; | ||||||
|  | import org.springframework.security.access.AccessDeniedException; | ||||||
|  | import org.springframework.security.web.access.AccessDeniedHandler; | ||||||
|  | import org.springframework.security.web.access.ExceptionTranslationFilter; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  |  | ||||||
|  | import javax.servlet.FilterChain; | ||||||
|  | import javax.servlet.ServletException; | ||||||
|  | import javax.servlet.http.HttpServletRequest; | ||||||
|  | import javax.servlet.http.HttpServletResponse; | ||||||
|  | import java.io.IOException; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 | ||||||
|  |  * | ||||||
|  |  * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 | ||||||
|  |  * | ||||||
|  |  * @author 芋道源码 | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | @SuppressWarnings("JavadocReference") | ||||||
|  | @Slf4j | ||||||
|  | public class AccessDeniedHandlerImpl implements AccessDeniedHandler { | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) | ||||||
|  |             throws IOException, ServletException { | ||||||
|  |         // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 | ||||||
|  |         log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), | ||||||
|  |                 SecurityUtils.getLoginUserId(), e); | ||||||
|  |         // 返回 403 | ||||||
|  |         CommonResult<Object> result = new CommonResult<>(); | ||||||
|  |         result.setCode(HttpStatus.FORBIDDEN.value()); | ||||||
|  |         result.setMsg("没有该操作权限"); | ||||||
|  |         ServletUtils.writeJSON(response, result); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.framework.core.handler; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
|  | import org.springframework.http.HttpStatus; | ||||||
|  | import org.springframework.security.core.AuthenticationException; | ||||||
|  | import org.springframework.security.web.AuthenticationEntryPoint; | ||||||
|  | import org.springframework.security.web.access.ExceptionTranslationFilter; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  |  | ||||||
|  | import javax.servlet.FilterChain; | ||||||
|  | import javax.servlet.http.HttpServletRequest; | ||||||
|  | import javax.servlet.http.HttpServletResponse; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 | ||||||
|  |  * | ||||||
|  |  * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 | ||||||
|  |  */ | ||||||
|  | @Component | ||||||
|  | @Slf4j | ||||||
|  | @SuppressWarnings("JavadocReference") // 忽略文档引用报错 | ||||||
|  | public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { | ||||||
|  |         log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); | ||||||
|  |         // 返回 401 | ||||||
|  |         CommonResult<Object> result = new CommonResult<>(); | ||||||
|  |         result.setCode(HttpStatus.UNAUTHORIZED.value()); | ||||||
|  |         result.setMsg("账号未登录"); | ||||||
|  |         ServletUtils.writeJSON(response, result); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,103 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.framework.core.util; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||||||
|  | import org.springframework.security.core.Authentication; | ||||||
|  | import org.springframework.security.core.context.SecurityContext; | ||||||
|  | import org.springframework.security.core.context.SecurityContextHolder; | ||||||
|  | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||||||
|  | import org.springframework.util.StringUtils; | ||||||
|  |  | ||||||
|  | import javax.servlet.http.HttpServletRequest; | ||||||
|  | import java.util.Collections; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 安全服务工具类 | ||||||
|  |  * | ||||||
|  |  * @author 芋道源码 | ||||||
|  |  */ | ||||||
|  | public class SecurityUtils { | ||||||
|  |  | ||||||
|  |     public static final String AUTHORIZATION_BEARER = "Bearer"; | ||||||
|  |  | ||||||
|  |     private SecurityUtils() {} | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 从请求中,获得认证 Token | ||||||
|  |      * | ||||||
|  |      * @param request 请求 | ||||||
|  |      * @param header 认证 Token 对应的 Header 名字 | ||||||
|  |      * @return 认证 Token | ||||||
|  |      */ | ||||||
|  |     public static String obtainAuthorization(HttpServletRequest request, String header) { | ||||||
|  |         String authorization = request.getHeader(header); | ||||||
|  |         if (!StringUtils.hasText(authorization)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); | ||||||
|  |         if (index == -1) { // 未找到 | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         return authorization.substring(index + 7).trim(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获得当前认证信息 | ||||||
|  |      * | ||||||
|  |      * @return 认证信息 | ||||||
|  |      */ | ||||||
|  |     public static Authentication getAuthentication() { | ||||||
|  |         SecurityContext context = SecurityContextHolder.getContext(); | ||||||
|  |         if (context == null) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         return context.getAuthentication(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获取当前用户 | ||||||
|  |      * | ||||||
|  |      * @return 当前用户 | ||||||
|  |      */ | ||||||
|  |     @Nullable | ||||||
|  |     public static LoginUser getLoginUser() { | ||||||
|  |         Authentication authentication = getAuthentication(); | ||||||
|  |         if (authentication == null) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获得当前用户的编号,从上下文中 | ||||||
|  |      * | ||||||
|  |      * @return 用户编号 | ||||||
|  |      */ | ||||||
|  |     @Nullable | ||||||
|  |     public static Long getLoginUserId() { | ||||||
|  |         LoginUser loginUser = getLoginUser(); | ||||||
|  |         return loginUser != null ? loginUser.getId() : null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 设置当前用户 | ||||||
|  |      * | ||||||
|  |      * @param loginUser 登录用户 | ||||||
|  |      * @param request 请求 | ||||||
|  |      */ | ||||||
|  |     public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { | ||||||
|  |         // 创建 Authentication,并设置到上下文 | ||||||
|  |         Authentication authentication = buildAuthentication(loginUser, request); | ||||||
|  |         SecurityContextHolder.getContext().setAuthentication(authentication); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { | ||||||
|  |         // 创建 UsernamePasswordAuthenticationToken 对象 | ||||||
|  |         UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( | ||||||
|  |                 loginUser, null, Collections.emptyList()); | ||||||
|  |         authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||||||
|  |         return authenticationToken; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | package cn.iocoder.yudao.ssodemo.framework.core.util; | ||||||
|  |  | ||||||
|  | import cn.hutool.extra.servlet.ServletUtil; | ||||||
|  | import cn.hutool.json.JSONUtil; | ||||||
|  | import org.springframework.http.MediaType; | ||||||
|  |  | ||||||
|  | import javax.servlet.http.HttpServletResponse; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 客户端工具类 | ||||||
|  |  * | ||||||
|  |  * @author 芋道源码 | ||||||
|  |  */ | ||||||
|  | public class ServletUtils { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 返回 JSON 字符串 | ||||||
|  |      * | ||||||
|  |      * @param response 响应 | ||||||
|  |      * @param object 对象,会序列化成 JSON 字符串 | ||||||
|  |      */ | ||||||
|  |     @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 | ||||||
|  |     public static void writeJSON(HttpServletResponse response, Object object) { | ||||||
|  |         String content = JSONUtil.toJsonStr(object); | ||||||
|  |         ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | server: | ||||||
|  |   port: 18080 | ||||||
| @@ -0,0 +1,61 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  | 	<meta charset="UTF-8"> | ||||||
|  | 	<title>SSO 授权后的回调页</title> | ||||||
|  | 	<!-- jQuery:操作 dom、发起请求等 --> | ||||||
|  | 	<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> | ||||||
|  | 	<!-- 工具类 --> | ||||||
|  | 	<script type="application/javascript"> | ||||||
|  |     (function ($) { | ||||||
|  |       /** | ||||||
|  | 			 * 获得 URL 的指定参数的值 | ||||||
|  | 			 * | ||||||
|  |        * @param name 参数名 | ||||||
|  |        * @returns 参数值 | ||||||
|  |        */ | ||||||
|  |       $.getUrlParam = function (name) { | ||||||
|  |         const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); | ||||||
|  |         const r = window.location.search.substr(1).match(reg); | ||||||
|  |         if (r != null) return unescape(r[2]); return null; | ||||||
|  |       } | ||||||
|  |     })(jQuery); | ||||||
|  | 	</script> | ||||||
|  |  | ||||||
|  | 	<script type="application/javascript"> | ||||||
|  |     $(function () { | ||||||
|  |       // 获得 code 授权码 | ||||||
|  | 			const code = $.getUrlParam('code'); | ||||||
|  |       if (!code) { | ||||||
|  |         alert('获取不到 code 参数,请排查!') | ||||||
|  |         return; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  |       // 提交 | ||||||
|  | 			const redirectUri = 'http://127.0.0.1:18080/callback.html'; // 需要修改成,你回调的地址,就是在 index.html 拼接的 redirectUri | ||||||
|  |       $.ajax({ | ||||||
|  |         url:  "http://127.0.0.1:18080/auth/login-by-code?code=" + code | ||||||
|  | 					+ '&redirectUri=' + redirectUri, | ||||||
|  |         method: 'POST', | ||||||
|  |         success: function( result ) { | ||||||
|  |           if (result.code !== 0) { | ||||||
|  |             alert('获得访问令牌失败,原因:' + result.msg) | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |           alert('获得访问令牌成功!点击确认,跳转回首页') | ||||||
|  |  | ||||||
|  |           // 设置到 localStorage 中 | ||||||
|  |           localStorage.setItem('ACCESS-TOKEN', result.data.access_token); | ||||||
|  |           localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token); | ||||||
|  |  | ||||||
|  |           // 跳转回首页 | ||||||
|  |           window.location.href = '/index.html'; | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  | 		}) | ||||||
|  | 	</script> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | 正在使用 code 授权码,进行 accessToken 访问令牌的获取 | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
| @@ -0,0 +1,159 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  | 	<meta charset="UTF-8"> | ||||||
|  | 	<title>首页</title> | ||||||
|  | 	<!-- jQuery:操作 dom、发起请求等 --> | ||||||
|  | 	<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> | ||||||
|  |  | ||||||
|  | 	<script type="application/javascript"> | ||||||
|  |  | ||||||
|  | 		/** | ||||||
|  | 		 * 跳转单点登录 | ||||||
|  |      */ | ||||||
|  | 		function ssoLogin() { | ||||||
|  | 			const clientId = 'yudao-sso-demo-by-code'; // 可以改写成,你的 clientId | ||||||
|  |       const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址 | ||||||
|  |       const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token | ||||||
|  |       window.location.href = 'http://127.0.0.1:1024/sso?client_id=' + clientId | ||||||
|  | 				+ '&redirect_uri=' + redirectUri | ||||||
|  | 				+ '&response_type=' + responseType; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  | 		 * 修改昵称 | ||||||
|  |      */ | ||||||
|  |     function updateNickname() { | ||||||
|  |       const nickname = prompt("请输入新的昵称", ""); | ||||||
|  |       if (!nickname) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       // 更新用户的昵称 | ||||||
|  |       const accessToken = localStorage.getItem('ACCESS-TOKEN'); | ||||||
|  |       $.ajax({ | ||||||
|  |         url: "http://127.0.0.1:18080/user/update?nickname=" + nickname, | ||||||
|  |         method: 'PUT', | ||||||
|  |         headers: { | ||||||
|  |           'Authentication': '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: { | ||||||
|  |           'Authentication': '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: { | ||||||
|  |           'Authentication': '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> | ||||||
| @@ -26,6 +26,7 @@ public class OAuth2OpenCheckTokenRespVO { | |||||||
|     private Long tenantId; |     private Long tenantId; | ||||||
|  |  | ||||||
|     @ApiModelProperty(value = "客户端编号", required = true, example = "car") |     @ApiModelProperty(value = "客户端编号", required = true, example = "car") | ||||||
|  |     @JsonProperty("client_id") | ||||||
|     private String clientId; |     private String clientId; | ||||||
|     @ApiModelProperty(value = "授权范围", required = true, example = "user_info") |     @ApiModelProperty(value = "授权范围", required = true, example = "user_info") | ||||||
|     private List<String> scopes; |     private List<String> scopes; | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ public class OAuth2UserInfoRespVO { | |||||||
|     @ApiModelProperty(value = "用户编号", required = true, example = "1") |     @ApiModelProperty(value = "用户编号", required = true, example = "1") | ||||||
|     private Long id; |     private Long id; | ||||||
|  |  | ||||||
|     @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿") |     @ApiModelProperty(value = "用户账号", required = true, example = "芋艿") | ||||||
|     private String username; |     private String username; | ||||||
|  |  | ||||||
|     @ApiModelProperty(value = "用户昵称", required = true, example = "芋道") |     @ApiModelProperty(value = "用户昵称", required = true, example = "芋道") | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ public class TenantPackageDO extends BaseDO { | |||||||
|      */ |      */ | ||||||
|     private String name; |     private String name; | ||||||
|     /** |     /** | ||||||
|      * 租户状态 |      * 租户套餐状态 | ||||||
|      * |      * | ||||||
|      * 枚举 {@link CommonStatusEnum} |      * 枚举 {@link CommonStatusEnum} | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -48,7 +48,8 @@ router.beforeEach((to, from, next) => { | |||||||
|       // 在免登录白名单,直接进入 |       // 在免登录白名单,直接进入 | ||||||
|       next() |       next() | ||||||
|     } else { |     } else { | ||||||
|       next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页 |       const redirect = encodeURIComponent(to.fullPath) // 编码 URI,保证参数跳转回去后,可以继续带上 | ||||||
|  |       next(`/login?redirect=${redirect}`) // 否则全部重定向到登录页 | ||||||
|       NProgress.done() |       NProgress.done() | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -190,7 +190,7 @@ export default { | |||||||
|     // 验证码开关 |     // 验证码开关 | ||||||
|     this.captchaEnable = getCaptchaEnable(); |     this.captchaEnable = getCaptchaEnable(); | ||||||
|     // 重定向地址 |     // 重定向地址 | ||||||
|     this.redirect = this.$route.query.redirect; |     this.redirect = this.$route.query.redirect ? decodeURIComponent(this.$route.query.redirect) : undefined; | ||||||
|     this.getCookie(); |     this.getCookie(); | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|   | |||||||
| @@ -117,7 +117,7 @@ export default { | |||||||
|     // 验证码开关 |     // 验证码开关 | ||||||
|     this.captchaEnable = getCaptchaEnable(); |     this.captchaEnable = getCaptchaEnable(); | ||||||
|     // 重定向地址 |     // 重定向地址 | ||||||
|     this.redirect = this.$route.query.redirect; |     this.redirect = this.$route.query.redirect ? decodeURIComponent(this.$route.query.redirect) : undefined; | ||||||
|     // 社交登录相关 |     // 社交登录相关 | ||||||
|     this.type = this.$route.query.type; |     this.type = this.$route.query.type; | ||||||
|     this.code = this.$route.query.code; |     this.code = this.$route.query.code; | ||||||
|   | |||||||
| @@ -19,12 +19,7 @@ | |||||||
|             </el-tab-pane> |             </el-tab-pane> | ||||||
|           </el-tabs> |           </el-tabs> | ||||||
|           <div> |           <div> | ||||||
|             <el-form ref="loginForm" :model="loginForm" :rules="LoginRules" class="login-form"> |             <el-form ref="loginForm" :model="loginForm" class="login-form"> | ||||||
|               <el-form-item prop="tenantName" v-if="tenantEnable"> |  | ||||||
|                 <el-input v-model="loginForm.tenantName" type="text" auto-complete="off" placeholder='租户'> |  | ||||||
|                   <svg-icon slot="prefix" icon-class="tree" class="el-input__icon input-icon"/> |  | ||||||
|                 </el-input> |  | ||||||
|               </el-form-item> |  | ||||||
|               <!-- 授权范围的选择 --> |               <!-- 授权范围的选择 --> | ||||||
|               此第三方应用请求获得以下权限: |               此第三方应用请求获得以下权限: | ||||||
|               <el-form-item prop="scopes"> |               <el-form-item prop="scopes"> | ||||||
| @@ -56,10 +51,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import {getTenantIdByName} from "@/api/system/tenant"; |  | ||||||
| import {getTenantEnable} from "@/utils/ruoyi"; |  | ||||||
| import {authorize, getAuthorize} from "@/api/login"; | import {authorize, getAuthorize} from "@/api/login"; | ||||||
| import {getTenantName, setTenantId} from "@/utils/auth"; |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: "Login", |   name: "Login", | ||||||
| @@ -67,7 +59,6 @@ export default { | |||||||
|     return { |     return { | ||||||
|       tenantEnable: true, |       tenantEnable: true, | ||||||
|       loginForm: { |       loginForm: { | ||||||
|         tenantName: "芋道源码", |  | ||||||
|         scopes: [], // 已选中的 scope 数组 |         scopes: [], // 已选中的 scope 数组 | ||||||
|       }, |       }, | ||||||
|       params: { // URL 上的 client_id、scope 等参数 |       params: { // URL 上的 client_id、scope 等参数 | ||||||
| @@ -81,35 +72,10 @@ export default { | |||||||
|         name: '', |         name: '', | ||||||
|         logo: '', |         logo: '', | ||||||
|       }, |       }, | ||||||
|       LoginRules: { |  | ||||||
|         tenantName: [ |  | ||||||
|           {required: true, trigger: "blur", message: "租户不能为空"}, |  | ||||||
|           { |  | ||||||
|             validator: (rule, value, callback) => { |  | ||||||
|               // debugger |  | ||||||
|               getTenantIdByName(value).then(res => { |  | ||||||
|                 const tenantId = res.data; |  | ||||||
|                 if (tenantId && tenantId >= 0) { |  | ||||||
|                   // 设置租户 |  | ||||||
|                   setTenantId(tenantId) |  | ||||||
|                   callback(); |  | ||||||
|                 } else { |  | ||||||
|                   callback('租户不存在'); |  | ||||||
|                 } |  | ||||||
|               }); |  | ||||||
|             }, |  | ||||||
|             trigger: 'blur' |  | ||||||
|           } |  | ||||||
|         ] |  | ||||||
|       }, |  | ||||||
|       loading: false |       loading: false | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   created() { |   created() { | ||||||
|     // 租户开关 |  | ||||||
|     this.tenantEnable = getTenantEnable(); |  | ||||||
|     this.getCookie(); |  | ||||||
|  |  | ||||||
|     // 解析参数 |     // 解析参数 | ||||||
|     // 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write |     // 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write | ||||||
|     // 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read |     // 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read | ||||||
| @@ -162,13 +128,6 @@ export default { | |||||||
|     }) |     }) | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     getCookie() { |  | ||||||
|       const tenantName = getTenantName(); |  | ||||||
|       this.loginForm = { |  | ||||||
|         ...this.loginForm, |  | ||||||
|         tenantName: tenantName ? tenantName : this.loginForm.tenantName, |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|     handleAuthorize(approved) { |     handleAuthorize(approved) { | ||||||
|       this.$refs.loginForm.validate(valid => { |       this.$refs.loginForm.validate(valid => { | ||||||
|         if (!valid) { |         if (!valid) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 芋道源码
					芋道源码