Commit 9cde6c45ab985532fe6b3c4bc569ac9677554729

Authored by 芋道源码
Committed by Gitee
2 parents 1be09d09 585d2a52

!272 增加“基于授权码模式,实现 SSO 单点登录“示例

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