Commit 05951783dee6ad55dd8edb482d54d76f609296b0

Authored by YunaiV
1 parent 6be30c08

增加 yudao-sso-demo-by-password 示例,基于密码模式,实现 SSO 单点登录

Showing 23 changed files with 1226 additions and 1 deletions
yudao-example/pom.xml
... ... @@ -12,6 +12,7 @@
12 12 <packaging>pom</packaging>
13 13 <modules>
14 14 <module>yudao-sso-demo-by-code</module>
  15 + <module>yudao-sso-demo-by-password</module>
15 16 </modules>
16 17  
17 18 <name>${project.artifactId}</name>
... ...
yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html
... ... @@ -75,7 +75,7 @@
75 75 }
76 76  
77 77 /**
78   - * 刷新令牌
  78 + * 登出,删除访问令牌
79 79 */
80 80 function logout() {
81 81 const accessToken = localStorage.getItem('ACCESS-TOKEN');
... ...
yudao-example/yudao-sso-demo-by-password/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-password</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-password/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-password/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 + * 校验访问令牌,并返回它的基本信息
  43 + *
  44 + * @param token 访问令牌
  45 + * @return 访问令牌的基本信息
  46 + */
  47 + public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) {
  48 + // 1.1 构建请求头
  49 + HttpHeaders headers = new HttpHeaders();
  50 + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
  51 + headers.set("tenant-id", TENANT_ID.toString());
  52 + addClientHeader(headers);
  53 + // 1.2 构建请求参数
  54 + MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
  55 + body.add("token", token);
  56 +
  57 + // 2. 执行请求
  58 + ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange(
  59 + BASE_URL + "/check-token",
  60 + HttpMethod.POST,
  61 + new HttpEntity<>(body, headers),
  62 + new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
  63 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
  64 + return exchange.getBody();
  65 + }
  66 +
  67 + /**
  68 + * 使用刷新令牌,获得(刷新)访问令牌
  69 + *
  70 + * @param refreshToken 刷新令牌
  71 + * @return 访问令牌
  72 + */
  73 + public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(String refreshToken) {
  74 + // 1.1 构建请求头
  75 + HttpHeaders headers = new HttpHeaders();
  76 + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
  77 + headers.set("tenant-id", TENANT_ID.toString());
  78 + addClientHeader(headers);
  79 + // 1.2 构建请求参数
  80 + MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
  81 + body.add("grant_type", "refresh_token");
  82 + body.add("refresh_token", refreshToken);
  83 +
  84 + // 2. 执行请求
  85 + ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
  86 + BASE_URL + "/token",
  87 + HttpMethod.POST,
  88 + new HttpEntity<>(body, headers),
  89 + new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
  90 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
  91 + return exchange.getBody();
  92 + }
  93 +
  94 + /**
  95 + * 删除访问令牌
  96 + *
  97 + * @param token 访问令牌
  98 + * @return 成功
  99 + */
  100 + public CommonResult<Boolean> revokeToken(String token) {
  101 + // 1.1 构建请求头
  102 + HttpHeaders headers = new HttpHeaders();
  103 + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
  104 + headers.set("tenant-id", TENANT_ID.toString());
  105 + addClientHeader(headers);
  106 + // 1.2 构建请求参数
  107 + MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
  108 + body.add("token", token);
  109 +
  110 + // 2. 执行请求
  111 + ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
  112 + BASE_URL + "/token",
  113 + HttpMethod.DELETE,
  114 + new HttpEntity<>(body, headers),
  115 + new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
  116 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
  117 + return exchange.getBody();
  118 + }
  119 +
  120 + private static void addClientHeader(HttpHeaders headers) {
  121 + // client 拼接,需要 BASE64 编码
  122 + String client = CLIENT_ID + ":" + CLIENT_SECRET;
  123 + client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8));
  124 + headers.add("Authorization", "Basic " + client);
  125 + }
  126 +
  127 +}
... ...
yudao-example/yudao-sso-demo-by-password/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-password/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-password/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-password/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-password/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-password/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-password/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 + * 使用刷新令牌,获得(刷新)访问令牌
  25 + *
  26 + * @param refreshToken 刷新令牌
  27 + * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段
  28 + */
  29 + @PostMapping("/refresh-token")
  30 + public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
  31 + return oauth2Client.refreshToken(refreshToken);
  32 + }
  33 +
  34 + /**
  35 + * 退出登录
  36 + *
  37 + * @param request 请求
  38 + * @return 成功
  39 + */
  40 + @PostMapping("/logout")
  41 + public CommonResult<Boolean> logout(HttpServletRequest request) {
  42 + String token = SecurityUtils.obtainAuthorization(request, "Authorization");
  43 + if (StrUtil.isNotBlank(token)) {
  44 + return oauth2Client.revokeToken(token);
  45 + }
  46 + // 返回成功
  47 + return new CommonResult<>();
  48 + }
  49 +
  50 +}
... ...
yudao-example/yudao-sso-demo-by-password/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-password/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-password/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-password/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, "Authorization");
  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-password/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-password/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-password/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-password/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-password/src/main/resources/application.yaml 0 → 100644
  1 +server:
  2 + port: 18080
... ...
yudao-example/yudao-sso-demo-by-password/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 passwordLogin() {
  15 + window.location.href = '/login.html'
  16 + }
  17 +
  18 + /**
  19 + * 修改昵称
  20 + */
  21 + function updateNickname() {
  22 + const nickname = prompt("请输入新的昵称", "");
  23 + if (!nickname) {
  24 + return;
  25 + }
  26 + // 更新用户的昵称
  27 + const accessToken = localStorage.getItem('ACCESS-TOKEN');
  28 + $.ajax({
  29 + url: "http://127.0.0.1:18080/user/update?nickname=" + nickname,
  30 + method: 'PUT',
  31 + headers: {
  32 + 'Authorization': 'Bearer ' + accessToken
  33 + },
  34 + success: function (result) {
  35 + if (result.code !== 0) {
  36 + alert('更新昵称失败,原因:' + result.msg)
  37 + return;
  38 + }
  39 + alert('更新昵称成功!');
  40 + $('#nicknameSpan').html(nickname);
  41 + }
  42 + });
  43 + }
  44 +
  45 + /**
  46 + * 刷新令牌
  47 + */
  48 + function refreshToken() {
  49 + const refreshToken = localStorage.getItem('REFRESH-TOKEN');
  50 + if (!refreshToken) {
  51 + alert("获取不到刷新令牌");
  52 + return;
  53 + }
  54 + $.ajax({
  55 + url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken,
  56 + method: 'POST',
  57 + success: function (result) {
  58 + if (result.code !== 0) {
  59 + alert('刷新访问令牌失败,原因:' + result.msg)
  60 + return;
  61 + }
  62 + alert('更新访问令牌成功!');
  63 + $('#accessTokenSpan').html(result.data.access_token);
  64 +
  65 + // 设置到 localStorage 中
  66 + localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
  67 + localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
  68 + }
  69 + });
  70 + }
  71 +
  72 + /**
  73 + * 登出,删除访问令牌
  74 + */
  75 + function logout() {
  76 + const accessToken = localStorage.getItem('ACCESS-TOKEN');
  77 + if (!accessToken) {
  78 + location.reload();
  79 + return;
  80 + }
  81 + $.ajax({
  82 + url: "http://127.0.0.1:18080/auth/logout",
  83 + method: 'POST',
  84 + headers: {
  85 + 'Authorization': 'Bearer ' + accessToken
  86 + },
  87 + success: function (result) {
  88 + if (result.code !== 0) {
  89 + alert('退出登录失败,原因:' + result.msg)
  90 + return;
  91 + }
  92 + alert('退出登录成功!');
  93 + // 删除 localStorage 中
  94 + localStorage.removeItem('ACCESS-TOKEN');
  95 + localStorage.removeItem('REFRESH-TOKEN');
  96 +
  97 + location.reload();
  98 + }
  99 + });
  100 + }
  101 +
  102 + $(function () {
  103 + const accessToken = localStorage.getItem('ACCESS-TOKEN');
  104 + // 情况一:未登录
  105 + if (!accessToken) {
  106 + $('#noLoginDiv').css("display", "block");
  107 + return;
  108 + }
  109 +
  110 + // 情况二:已登录
  111 + $('#yesLoginDiv').css("display", "block");
  112 + $('#accessTokenSpan').html(accessToken);
  113 + // 获得登录用户的信息
  114 + $.ajax({
  115 + url: "http://127.0.0.1:18080/user/get",
  116 + method: 'GET',
  117 + headers: {
  118 + 'Authorization': 'Bearer ' + accessToken
  119 + },
  120 + success: function (result) {
  121 + if (result.code !== 0) {
  122 + alert('获得个人信息失败,原因:' + result.msg)
  123 + return;
  124 + }
  125 + $('#nicknameSpan').html(result.data.nickname);
  126 + }
  127 + });
  128 + })
  129 + </script>
  130 +</head>
  131 +<body>
  132 +<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
  133 +<div id="noLoginDiv" style="display: none">
  134 + 您未登录,点击 <a href="#" onclick="passwordLogin()">跳转 </a> 账号密码登录
  135 +</div>
  136 +
  137 +<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
  138 +<div id="yesLoginDiv" style="display: none">
  139 + 您已登录!<button onclick="logout()">退出登录</button> <br />
  140 + 昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br />
  141 + 访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br />
  142 +</div>
  143 +</body>
  144 +<style>
  145 + body { /** 页面居中 */
  146 + border-radius: 20px;
  147 + height: 350px;
  148 + position: absolute;
  149 + left: 50%;
  150 + top: 50%;
  151 + transform: translate(-50%,-50%);
  152 + }
  153 +</style>
  154 +</html>
... ...
yudao-example/yudao-sso-demo-by-password/src/main/resources/static/login.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 login() {
  15 + const clientId = 'yudao-sso-demo-by-password'; // 可以改写成,你的 clientId
  16 + const clientSecret = 'test'; // 可以改写成,你的 clientSecret
  17 + const grantType = 'password'; // 密码模式
  18 +
  19 + // 账号 + 密码
  20 + const username = $('#username').val();
  21 + const password = $('#password').val();
  22 + if (username.length === 0 || password.length === 0) {
  23 + alert('账号或密码未输入');
  24 + return;
  25 + }
  26 +
  27 + // 发起请求
  28 + $.ajax({
  29 + url: "http://127.0.0.1:48080/admin-api/system/oauth2/token?"
  30 + // 客户端
  31 + + "client_id=" + clientId
  32 + + "&client_secret=" + clientSecret
  33 + // 密码模式的参数
  34 + + "&grant_type=" + grantType
  35 + + "&username=" + username
  36 + + "&password=" + password
  37 + + '&scope=user.read user.write',
  38 + method: 'POST',
  39 + headers: {
  40 + 'tenant-id': '1', // 多租户编号,写死
  41 + },
  42 + success: function (result) {
  43 + if (result.code !== 0) {
  44 + alert('登录失败,原因:' + result.msg)
  45 + return;
  46 + }
  47 + // 设置到 localStorage 中
  48 + localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
  49 + localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
  50 +
  51 + // 提示登录成功
  52 + alert('登录成功!点击确认,跳转回首页');
  53 + window.location.href = '/index.html';
  54 + }
  55 + });
  56 + }
  57 + </script>
  58 +</head>
  59 +<body>
  60 + 账号:<input id="username" value="admin" /> <br />
  61 + 密码:<input id="password" value="admin123" > <br />
  62 + <button style="float: right; margin-top: 5px;" onclick="login()">登录</button>
  63 +</body>
  64 +<style>
  65 + body { /** 页面居中 */
  66 + border-radius: 20px;
  67 + height: 350px;
  68 + position: absolute;
  69 + left: 50%;
  70 + top: 50%;
  71 + transform: translate(-50%,-50%);
  72 + }
  73 +</style>
  74 +</html>
... ...