forked from sfms3.0/sfms3.0
50 changed files with 200 additions and 2566 deletions
@ -1,22 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<!-- 由于方便大家拷贝,使用不使用 win 作为 Maven parent --> |
|||
|
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-example</artifactId> |
|||
<version>1.0.0-snapshot</version> |
|||
<packaging>pom</packaging> |
|||
<modules> |
|||
<module>win-sso-demo-by-code</module> |
|||
<module>win-sso-demo-by-password</module> |
|||
</modules> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>提供各种示例,例如说:SSO 单点登录</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
</project> |
@ -1,65 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<!-- 由于方便大家拷贝,使用不使用 win 作为 Maven parent --> |
|||
|
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-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.7.15</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.21</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -1,13 +0,0 @@ |
|||
package com.win.ssodemo; |
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
|
|||
@SpringBootApplication |
|||
public class SSODemoApplication { |
|||
|
|||
public static void main(String[] args) { |
|||
SpringApplication.run(SSODemoApplication.class, args); |
|||
} |
|||
|
|||
} |
@ -1,157 +0,0 @@ |
|||
package com.win.ssodemo.client; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; |
|||
import com.win.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 = "win-sso-demo-by-code"; |
|||
private static final String CLIENT_SECRET = "test"; |
|||
|
|||
|
|||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
|||
private final RestTemplate restTemplate = new RestTemplate(); |
|||
|
|||
/** |
|||
* 使用 code 授权码,获得访问令牌 |
|||
* |
|||
* @param code 授权码 |
|||
* @param redirectUri 重定向 URI |
|||
* @return 访问令牌 |
|||
*/ |
|||
public CommonResult<OAuth2AccessTokenRespDTO> postAccessToken(String code, String redirectUri) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("grant_type", "authorization_code"); |
|||
body.add("code", code); |
|||
body.add("redirect_uri", redirectUri); |
|||
// body.add("state", ""); // 选填;填了会校验
|
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/token", |
|||
HttpMethod.POST, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* 校验访问令牌,并返回它的基本信息 |
|||
* |
|||
* @param token 访问令牌 |
|||
* @return 访问令牌的基本信息 |
|||
*/ |
|||
public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("token", token); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/check-token", |
|||
HttpMethod.POST, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* 使用刷新令牌,获得(刷新)访问令牌 |
|||
* |
|||
* @param refreshToken 刷新令牌 |
|||
* @return 访问令牌 |
|||
*/ |
|||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(String refreshToken) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("grant_type", "refresh_token"); |
|||
body.add("refresh_token", refreshToken); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/token", |
|||
HttpMethod.POST, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* 删除访问令牌 |
|||
* |
|||
* @param token 访问令牌 |
|||
* @return 成功 |
|||
*/ |
|||
public CommonResult<Boolean> revokeToken(String token) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("token", token); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/token", |
|||
HttpMethod.DELETE, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
private static void addClientHeader(HttpHeaders headers) { |
|||
// client 拼接,需要 BASE64 编码
|
|||
String client = CLIENT_ID + ":" + CLIENT_SECRET; |
|||
client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8)); |
|||
headers.add("Authorization", "Basic " + client); |
|||
} |
|||
|
|||
} |
@ -1,73 +0,0 @@ |
|||
package com.win.ssodemo.client; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.user.UserInfoRespDTO; |
|||
import com.win.ssodemo.client.dto.user.UserUpdateReqDTO; |
|||
import com.win.ssodemo.framework.core.LoginUser; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import org.springframework.core.ParameterizedTypeReference; |
|||
import org.springframework.http.*; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.Assert; |
|||
import org.springframework.util.LinkedMultiValueMap; |
|||
import org.springframework.util.MultiValueMap; |
|||
import org.springframework.web.client.RestTemplate; |
|||
|
|||
/** |
|||
* 用户 User 信息的客户端 |
|||
* |
|||
* 对应调用 OAuth2UserController 接口 |
|||
*/ |
|||
@Component |
|||
public class UserClient { |
|||
|
|||
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user"; |
|||
|
|||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
|||
private final RestTemplate restTemplate = new RestTemplate(); |
|||
|
|||
public CommonResult<UserInfoRespDTO> getUser() { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); |
|||
addTokenHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/get", |
|||
HttpMethod.GET, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
public CommonResult<Boolean> updateUser(UserUpdateReqDTO updateReqDTO) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_JSON); |
|||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); |
|||
addTokenHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
// 使用 updateReqDTO 即可
|
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/update", |
|||
HttpMethod.PUT, |
|||
new HttpEntity<>(updateReqDTO, headers), |
|||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
|
|||
private static void addTokenHeader(HttpHeaders headers) { |
|||
LoginUser loginUser = SecurityUtils.getLoginUser(); |
|||
Assert.notNull(loginUser, "登录用户不能为空"); |
|||
headers.add("Authorization", "Bearer " + loginUser.getAccessToken()); |
|||
} |
|||
} |
@ -1,28 +0,0 @@ |
|||
package com.win.ssodemo.client.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
/** |
|||
* 通用返回 |
|||
* |
|||
* @param <T> 数据泛型 |
|||
*/ |
|||
@Data |
|||
public class CommonResult<T> implements Serializable { |
|||
|
|||
/** |
|||
* 错误码 |
|||
*/ |
|||
private Integer code; |
|||
/** |
|||
* 返回数据 |
|||
*/ |
|||
private T data; |
|||
/** |
|||
* 错误提示,用户可阅读 |
|||
*/ |
|||
private String msg; |
|||
|
|||
} |
@ -1,45 +0,0 @@ |
|||
package com.win.ssodemo.client.dto.oauth2; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* 访问令牌 Response DTO |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class OAuth2AccessTokenRespDTO { |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
@JsonProperty("access_token") |
|||
private String accessToken; |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
*/ |
|||
@JsonProperty("refresh_token") |
|||
private String refreshToken; |
|||
|
|||
/** |
|||
* 令牌类型 |
|||
*/ |
|||
@JsonProperty("token_type") |
|||
private String tokenType; |
|||
|
|||
/** |
|||
* 过期时间;单位:秒 |
|||
*/ |
|||
@JsonProperty("expires_in") |
|||
private Long expiresIn; |
|||
|
|||
/** |
|||
* 授权范围;如果多个授权范围,使用空格分隔 |
|||
*/ |
|||
private String scope; |
|||
|
|||
} |
@ -1,59 +0,0 @@ |
|||
package com.win.ssodemo.client.dto.oauth2; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 校验令牌 Response DTO |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class OAuth2CheckTokenRespDTO { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
@JsonProperty("user_id") |
|||
private Long userId; |
|||
/** |
|||
* 用户类型 |
|||
*/ |
|||
@JsonProperty("user_type") |
|||
private Integer userType; |
|||
/** |
|||
* 租户编号 |
|||
*/ |
|||
@JsonProperty("tenant_id") |
|||
private Long tenantId; |
|||
|
|||
/** |
|||
* 客户端编号 |
|||
*/ |
|||
@JsonProperty("client_id") |
|||
private String clientId; |
|||
/** |
|||
* 授权范围 |
|||
*/ |
|||
private List<String> scopes; |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
@JsonProperty("access_token") |
|||
private String accessToken; |
|||
|
|||
/** |
|||
* 过期时间 |
|||
* |
|||
* 时间戳 / 1000,即单位:秒 |
|||
*/ |
|||
private Long exp; |
|||
|
|||
} |
@ -1,97 +0,0 @@ |
|||
package com.win.ssodemo.client.dto.user; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 获得用户基本信息 Response dto |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class UserInfoRespDTO { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 用户账号 |
|||
*/ |
|||
private String username; |
|||
|
|||
/** |
|||
* 用户昵称 |
|||
*/ |
|||
private String nickname; |
|||
|
|||
/** |
|||
* 用户邮箱 |
|||
*/ |
|||
private String email; |
|||
/** |
|||
* 手机号码 |
|||
*/ |
|||
private String mobile; |
|||
|
|||
/** |
|||
* 用户性别 |
|||
*/ |
|||
private Integer sex; |
|||
|
|||
/** |
|||
* 用户头像 |
|||
*/ |
|||
private String avatar; |
|||
|
|||
/** |
|||
* 所在部门 |
|||
*/ |
|||
private Dept dept; |
|||
|
|||
/** |
|||
* 所属岗位数组 |
|||
*/ |
|||
private List<Post> posts; |
|||
|
|||
/** |
|||
* 部门 |
|||
*/ |
|||
@Data |
|||
public static class Dept { |
|||
|
|||
/** |
|||
* 部门编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 部门名称 |
|||
*/ |
|||
private String name; |
|||
|
|||
} |
|||
|
|||
/** |
|||
* 岗位 |
|||
*/ |
|||
@Data |
|||
public static class Post { |
|||
|
|||
/** |
|||
* 岗位编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 岗位名称 |
|||
*/ |
|||
private String name; |
|||
|
|||
} |
|||
|
|||
} |
@ -1,35 +0,0 @@ |
|||
package com.win.ssodemo.client.dto.user; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* 更新用户基本信息 Request DTO |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class UserUpdateReqDTO { |
|||
|
|||
/** |
|||
* 用户昵称 |
|||
*/ |
|||
private String nickname; |
|||
|
|||
/** |
|||
* 用户邮箱 |
|||
*/ |
|||
private String email; |
|||
|
|||
/** |
|||
* 手机号码 |
|||
*/ |
|||
private String mobile; |
|||
|
|||
/** |
|||
* 用户性别 |
|||
*/ |
|||
private Integer sex; |
|||
|
|||
} |
@ -1,63 +0,0 @@ |
|||
package com.win.ssodemo.controller; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.win.ssodemo.client.OAuth2Client; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; |
|||
import com.win.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, "Authorization"); |
|||
if (StrUtil.isNotBlank(token)) { |
|||
return oauth2Client.revokeToken(token); |
|||
} |
|||
// 返回成功
|
|||
return new CommonResult<>(); |
|||
} |
|||
|
|||
} |
@ -1,40 +0,0 @@ |
|||
package com.win.ssodemo.controller; |
|||
|
|||
import com.win.ssodemo.client.UserClient; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.user.UserInfoRespDTO; |
|||
import com.win.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); |
|||
} |
|||
|
|||
} |
@ -1,52 +0,0 @@ |
|||
package com.win.ssodemo.framework.config; |
|||
|
|||
import com.win.ssodemo.framework.core.filter.TokenAuthenticationFilter; |
|||
import com.win.ssodemo.framework.core.handler.AccessDeniedHandlerImpl; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.http.HttpMethod; |
|||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
|||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
|||
import org.springframework.security.web.AuthenticationEntryPoint; |
|||
import org.springframework.security.web.SecurityFilterChain; |
|||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
|||
|
|||
import javax.annotation.Resource; |
|||
|
|||
@Configuration(proxyBeanMethods = false) |
|||
@EnableWebSecurity |
|||
public class SecurityConfiguration{ |
|||
|
|||
@Resource |
|||
private TokenAuthenticationFilter tokenAuthenticationFilter; |
|||
|
|||
@Resource |
|||
private AccessDeniedHandlerImpl accessDeniedHandler; |
|||
@Resource |
|||
private AuthenticationEntryPoint authenticationEntryPoint; |
|||
|
|||
@Bean |
|||
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { |
|||
// 设置 URL 安全权限
|
|||
httpSecurity.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); |
|||
return httpSecurity.build(); |
|||
} |
|||
|
|||
} |
@ -1,37 +0,0 @@ |
|||
package com.win.ssodemo.framework.core; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 登录用户信息 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
@Data |
|||
public class LoginUser { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
private Long id; |
|||
/** |
|||
* 用户类型 |
|||
*/ |
|||
private Integer userType; |
|||
/** |
|||
* 租户编号 |
|||
*/ |
|||
private Long tenantId; |
|||
/** |
|||
* 授权范围 |
|||
*/ |
|||
private List<String> scopes; |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
private String accessToken; |
|||
|
|||
} |
@ -1,66 +0,0 @@ |
|||
package com.win.ssodemo.framework.core.filter; |
|||
|
|||
import com.win.ssodemo.client.OAuth2Client; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; |
|||
import com.win.ssodemo.framework.core.LoginUser; |
|||
import com.win.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, "Authorization"); |
|||
if (StringUtils.hasText(token)) { |
|||
// 2. 基于 token 构建登录用户
|
|||
LoginUser loginUser = buildLoginUserByToken(token); |
|||
// 3. 设置当前用户
|
|||
if (loginUser != null) { |
|||
SecurityUtils.setLoginUser(loginUser, request); |
|||
} |
|||
} |
|||
|
|||
// 继续过滤链
|
|||
filterChain.doFilter(request, response); |
|||
} |
|||
|
|||
private LoginUser buildLoginUserByToken(String token) { |
|||
try { |
|||
CommonResult<OAuth2CheckTokenRespDTO> accessTokenResult = oauth2Client.checkToken(token); |
|||
OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); |
|||
if (accessToken == null) { |
|||
return null; |
|||
} |
|||
// 构建登录用户
|
|||
return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) |
|||
.setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) |
|||
.setAccessToken(accessToken.getAccessToken()); |
|||
} catch (Exception exception) { |
|||
// 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
|
|||
return null; |
|||
} |
|||
} |
|||
|
|||
} |
@ -1,44 +0,0 @@ |
|||
package com.win.ssodemo.framework.core.handler; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import com.win.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); |
|||
} |
|||
|
|||
} |
@ -1,36 +0,0 @@ |
|||
package com.win.ssodemo.framework.core.handler; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.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); |
|||
} |
|||
|
|||
} |
@ -1,103 +0,0 @@ |
|||
package com.win.ssodemo.framework.core.util; |
|||
|
|||
import com.win.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; |
|||
} |
|||
|
|||
} |
@ -1,32 +0,0 @@ |
|||
package com.win.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); |
|||
} |
|||
|
|||
public static void write(HttpServletResponse response, String text, String contentType) { |
|||
ServletUtil.write(response, text, contentType); |
|||
} |
|||
|
|||
} |
@ -1,2 +0,0 @@ |
|||
server: |
|||
port: 18080 |
@ -1,61 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>SSO 授权后的回调页</title> |
|||
<!-- jQuery:操作 dom、发起请求等 --> |
|||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> |
|||
<!-- 工具类 --> |
|||
<script type="application/javascript"> |
|||
(function ($) { |
|||
/** |
|||
* 获得 URL 的指定参数的值 |
|||
* |
|||
* @param name 参数名 |
|||
* @returns 参数值 |
|||
*/ |
|||
$.getUrlParam = function (name) { |
|||
const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); |
|||
const r = window.location.search.substr(1).match(reg); |
|||
if (r != null) return unescape(r[2]); return null; |
|||
} |
|||
})(jQuery); |
|||
</script> |
|||
|
|||
<script type="application/javascript"> |
|||
$(function () { |
|||
// 获得 code 授权码 |
|||
const code = $.getUrlParam('code'); |
|||
if (!code) { |
|||
alert('获取不到 code 参数,请排查!') |
|||
return; |
|||
} |
|||
|
|||
// 提交 |
|||
const redirectUri = 'http://127.0.0.1:18080/callback.html'; // 需要修改成,你回调的地址,就是在 index.html 拼接的 redirectUri |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/auth/login-by-code?code=" + code |
|||
+ '&redirectUri=' + redirectUri, |
|||
method: 'POST', |
|||
success: function( result ) { |
|||
if (result.code !== 0) { |
|||
alert('获得访问令牌失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('获得访问令牌成功!点击确认,跳转回首页') |
|||
|
|||
// 设置到 localStorage 中 |
|||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token); |
|||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token); |
|||
|
|||
// 跳转回首页 |
|||
window.location.href = '/index.html'; |
|||
} |
|||
}) |
|||
}) |
|||
</script> |
|||
</head> |
|||
<body> |
|||
正在使用 code 授权码,进行 accessToken 访问令牌的获取 |
|||
</body> |
|||
</html> |
@ -1,159 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>首页</title> |
|||
<!-- jQuery:操作 dom、发起请求等 --> |
|||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> |
|||
|
|||
<script type="application/javascript"> |
|||
|
|||
/** |
|||
* 跳转单点登录 |
|||
*/ |
|||
function ssoLogin() { |
|||
const clientId = 'win-sso-demo-by-code'; // 可以改写成,你的 clientId |
|||
const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址 |
|||
const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token |
|||
window.location.href = 'http://127.0.0.1:1024/sso?client_id=' + clientId |
|||
+ '&redirect_uri=' + redirectUri |
|||
+ '&response_type=' + responseType; |
|||
} |
|||
|
|||
/** |
|||
* 修改昵称 |
|||
*/ |
|||
function updateNickname() { |
|||
const nickname = prompt("请输入新的昵称", ""); |
|||
if (!nickname) { |
|||
return; |
|||
} |
|||
// 更新用户的昵称 |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/user/update?nickname=" + nickname, |
|||
method: 'PUT', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('更新昵称失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('更新昵称成功!'); |
|||
$('#nicknameSpan').html(nickname); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
*/ |
|||
function refreshToken() { |
|||
const refreshToken = localStorage.getItem('REFRESH-TOKEN'); |
|||
if (!refreshToken) { |
|||
alert("获取不到刷新令牌"); |
|||
return; |
|||
} |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken, |
|||
method: 'POST', |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('刷新访问令牌失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('更新访问令牌成功!'); |
|||
$('#accessTokenSpan').html(result.data.access_token); |
|||
|
|||
// 设置到 localStorage 中 |
|||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token); |
|||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 登出,删除访问令牌 |
|||
*/ |
|||
function logout() { |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
if (!accessToken) { |
|||
location.reload(); |
|||
return; |
|||
} |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/auth/logout", |
|||
method: 'POST', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('退出登录失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('退出登录成功!'); |
|||
// 删除 localStorage 中 |
|||
localStorage.removeItem('ACCESS-TOKEN'); |
|||
localStorage.removeItem('REFRESH-TOKEN'); |
|||
|
|||
location.reload(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
$(function () { |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
// 情况一:未登录 |
|||
if (!accessToken) { |
|||
$('#noLoginDiv').css("display", "block"); |
|||
return; |
|||
} |
|||
|
|||
// 情况二:已登录 |
|||
$('#yesLoginDiv').css("display", "block"); |
|||
$('#accessTokenSpan').html(accessToken); |
|||
// 获得登录用户的信息 |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/user/get", |
|||
method: 'GET', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('获得个人信息失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
$('#nicknameSpan').html(result.data.nickname); |
|||
} |
|||
}); |
|||
}) |
|||
</script> |
|||
</head> |
|||
<body> |
|||
<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 --> |
|||
<div id="noLoginDiv" style="display: none"> |
|||
您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录 |
|||
</div> |
|||
|
|||
<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 --> |
|||
<div id="yesLoginDiv" style="display: none"> |
|||
您已登录!<button onclick="logout()">退出登录</button> <br /> |
|||
昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br /> |
|||
访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br /> |
|||
</div> |
|||
</body> |
|||
<style> |
|||
body { /** 页面居中 */ |
|||
border-radius: 20px; |
|||
height: 350px; |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%,-50%); |
|||
} |
|||
</style> |
|||
</html> |
@ -1,65 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<!-- 由于方便大家拷贝,使用不使用 win 作为 Maven parent --> |
|||
|
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-sso-demo-by-password</artifactId> |
|||
<version>1.0.0-snapshot</version> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description>基于密码模式,如何实现 SSO 单点登录?</description> |
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
|||
|
|||
<properties> |
|||
<!-- Maven 相关 --> |
|||
<maven.compiler.source>8</maven.compiler.source> |
|||
<maven.compiler.target>8</maven.compiler.target> |
|||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
|||
<!-- 统一依赖管理 --> |
|||
<spring.boot.version>2.7.15</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.21</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
@ -1,13 +0,0 @@ |
|||
package com.win.ssodemo; |
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
|
|||
@SpringBootApplication |
|||
public class SSODemoApplication { |
|||
|
|||
public static void main(String[] args) { |
|||
SpringApplication.run(SSODemoApplication.class, args); |
|||
} |
|||
|
|||
} |
@ -1,127 +0,0 @@ |
|||
package com.win.ssodemo.client; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; |
|||
import com.win.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 = "win-sso-demo-by-password"; |
|||
private static final String CLIENT_SECRET = "test"; |
|||
|
|||
|
|||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
|||
private final RestTemplate restTemplate = new RestTemplate(); |
|||
|
|||
/** |
|||
* 校验访问令牌,并返回它的基本信息 |
|||
* |
|||
* @param token 访问令牌 |
|||
* @return 访问令牌的基本信息 |
|||
*/ |
|||
public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("token", token); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/check-token", |
|||
HttpMethod.POST, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* 使用刷新令牌,获得(刷新)访问令牌 |
|||
* |
|||
* @param refreshToken 刷新令牌 |
|||
* @return 访问令牌 |
|||
*/ |
|||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(String refreshToken) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("grant_type", "refresh_token"); |
|||
body.add("refresh_token", refreshToken); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/token", |
|||
HttpMethod.POST, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* 删除访问令牌 |
|||
* |
|||
* @param token 访问令牌 |
|||
* @return 成功 |
|||
*/ |
|||
public CommonResult<Boolean> revokeToken(String token) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", TENANT_ID.toString()); |
|||
addClientHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
body.add("token", token); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/token", |
|||
HttpMethod.DELETE, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
private static void addClientHeader(HttpHeaders headers) { |
|||
// client 拼接,需要 BASE64 编码
|
|||
String client = CLIENT_ID + ":" + CLIENT_SECRET; |
|||
client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8)); |
|||
headers.add("Authorization", "Basic " + client); |
|||
} |
|||
|
|||
} |
@ -1,73 +0,0 @@ |
|||
package com.win.ssodemo.client; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.user.UserInfoRespDTO; |
|||
import com.win.ssodemo.client.dto.user.UserUpdateReqDTO; |
|||
import com.win.ssodemo.framework.core.LoginUser; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import org.springframework.core.ParameterizedTypeReference; |
|||
import org.springframework.http.*; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.util.Assert; |
|||
import org.springframework.util.LinkedMultiValueMap; |
|||
import org.springframework.util.MultiValueMap; |
|||
import org.springframework.web.client.RestTemplate; |
|||
|
|||
/** |
|||
* 用户 User 信息的客户端 |
|||
* |
|||
* 对应调用 OAuth2UserController 接口 |
|||
*/ |
|||
@Component |
|||
public class UserClient { |
|||
|
|||
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user"; |
|||
|
|||
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
|||
private final RestTemplate restTemplate = new RestTemplate(); |
|||
|
|||
public CommonResult<UserInfoRespDTO> getUser() { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
|||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); |
|||
addTokenHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); |
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/get", |
|||
HttpMethod.GET, |
|||
new HttpEntity<>(body, headers), |
|||
new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
public CommonResult<Boolean> updateUser(UserUpdateReqDTO updateReqDTO) { |
|||
// 1.1 构建请求头
|
|||
HttpHeaders headers = new HttpHeaders(); |
|||
headers.setContentType(MediaType.APPLICATION_JSON); |
|||
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); |
|||
addTokenHeader(headers); |
|||
// 1.2 构建请求参数
|
|||
// 使用 updateReqDTO 即可
|
|||
|
|||
// 2. 执行请求
|
|||
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange( |
|||
BASE_URL + "/update", |
|||
HttpMethod.PUT, |
|||
new HttpEntity<>(updateReqDTO, headers), |
|||
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
|||
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); |
|||
return exchange.getBody(); |
|||
} |
|||
|
|||
|
|||
private static void addTokenHeader(HttpHeaders headers) { |
|||
LoginUser loginUser = SecurityUtils.getLoginUser(); |
|||
Assert.notNull(loginUser, "登录用户不能为空"); |
|||
headers.add("Authorization", "Bearer " + loginUser.getAccessToken()); |
|||
} |
|||
} |
@ -1,28 +0,0 @@ |
|||
package com.win.ssodemo.client.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
/** |
|||
* 通用返回 |
|||
* |
|||
* @param <T> 数据泛型 |
|||
*/ |
|||
@Data |
|||
public class CommonResult<T> implements Serializable { |
|||
|
|||
/** |
|||
* 错误码 |
|||
*/ |
|||
private Integer code; |
|||
/** |
|||
* 返回数据 |
|||
*/ |
|||
private T data; |
|||
/** |
|||
* 错误提示,用户可阅读 |
|||
*/ |
|||
private String msg; |
|||
|
|||
} |
@ -1,45 +0,0 @@ |
|||
package com.win.ssodemo.client.dto.oauth2; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* 访问令牌 Response DTO |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class OAuth2AccessTokenRespDTO { |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
@JsonProperty("access_token") |
|||
private String accessToken; |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
*/ |
|||
@JsonProperty("refresh_token") |
|||
private String refreshToken; |
|||
|
|||
/** |
|||
* 令牌类型 |
|||
*/ |
|||
@JsonProperty("token_type") |
|||
private String tokenType; |
|||
|
|||
/** |
|||
* 过期时间;单位:秒 |
|||
*/ |
|||
@JsonProperty("expires_in") |
|||
private Long expiresIn; |
|||
|
|||
/** |
|||
* 授权范围;如果多个授权范围,使用空格分隔 |
|||
*/ |
|||
private String scope; |
|||
|
|||
} |
@ -1,59 +0,0 @@ |
|||
package com.win.ssodemo.client.dto.oauth2; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 校验令牌 Response DTO |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class OAuth2CheckTokenRespDTO { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
@JsonProperty("user_id") |
|||
private Long userId; |
|||
/** |
|||
* 用户类型 |
|||
*/ |
|||
@JsonProperty("user_type") |
|||
private Integer userType; |
|||
/** |
|||
* 租户编号 |
|||
*/ |
|||
@JsonProperty("tenant_id") |
|||
private Long tenantId; |
|||
|
|||
/** |
|||
* 客户端编号 |
|||
*/ |
|||
@JsonProperty("client_id") |
|||
private String clientId; |
|||
/** |
|||
* 授权范围 |
|||
*/ |
|||
private List<String> scopes; |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
@JsonProperty("access_token") |
|||
private String accessToken; |
|||
|
|||
/** |
|||
* 过期时间 |
|||
* |
|||
* 时间戳 / 1000,即单位:秒 |
|||
*/ |
|||
private Long exp; |
|||
|
|||
} |
@ -1,97 +0,0 @@ |
|||
package com.win.ssodemo.client.dto.user; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 获得用户基本信息 Response dto |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class UserInfoRespDTO { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 用户账号 |
|||
*/ |
|||
private String username; |
|||
|
|||
/** |
|||
* 用户昵称 |
|||
*/ |
|||
private String nickname; |
|||
|
|||
/** |
|||
* 用户邮箱 |
|||
*/ |
|||
private String email; |
|||
/** |
|||
* 手机号码 |
|||
*/ |
|||
private String mobile; |
|||
|
|||
/** |
|||
* 用户性别 |
|||
*/ |
|||
private Integer sex; |
|||
|
|||
/** |
|||
* 用户头像 |
|||
*/ |
|||
private String avatar; |
|||
|
|||
/** |
|||
* 所在部门 |
|||
*/ |
|||
private Dept dept; |
|||
|
|||
/** |
|||
* 所属岗位数组 |
|||
*/ |
|||
private List<Post> posts; |
|||
|
|||
/** |
|||
* 部门 |
|||
*/ |
|||
@Data |
|||
public static class Dept { |
|||
|
|||
/** |
|||
* 部门编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 部门名称 |
|||
*/ |
|||
private String name; |
|||
|
|||
} |
|||
|
|||
/** |
|||
* 岗位 |
|||
*/ |
|||
@Data |
|||
public static class Post { |
|||
|
|||
/** |
|||
* 岗位编号 |
|||
*/ |
|||
private Long id; |
|||
|
|||
/** |
|||
* 岗位名称 |
|||
*/ |
|||
private String name; |
|||
|
|||
} |
|||
|
|||
} |
@ -1,35 +0,0 @@ |
|||
package com.win.ssodemo.client.dto.user; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* 更新用户基本信息 Request DTO |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
public class UserUpdateReqDTO { |
|||
|
|||
/** |
|||
* 用户昵称 |
|||
*/ |
|||
private String nickname; |
|||
|
|||
/** |
|||
* 用户邮箱 |
|||
*/ |
|||
private String email; |
|||
|
|||
/** |
|||
* 手机号码 |
|||
*/ |
|||
private String mobile; |
|||
|
|||
/** |
|||
* 用户性别 |
|||
*/ |
|||
private Integer sex; |
|||
|
|||
} |
@ -1,50 +0,0 @@ |
|||
package com.win.ssodemo.controller; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.win.ssodemo.client.OAuth2Client; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; |
|||
import com.win.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; |
|||
|
|||
/** |
|||
* 使用刷新令牌,获得(刷新)访问令牌 |
|||
* |
|||
* @param refreshToken 刷新令牌 |
|||
* @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 |
|||
*/ |
|||
@PostMapping("/refresh-token") |
|||
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) { |
|||
return oauth2Client.refreshToken(refreshToken); |
|||
} |
|||
|
|||
/** |
|||
* 退出登录 |
|||
* |
|||
* @param request 请求 |
|||
* @return 成功 |
|||
*/ |
|||
@PostMapping("/logout") |
|||
public CommonResult<Boolean> logout(HttpServletRequest request) { |
|||
String token = SecurityUtils.obtainAuthorization(request, "Authorization"); |
|||
if (StrUtil.isNotBlank(token)) { |
|||
return oauth2Client.revokeToken(token); |
|||
} |
|||
// 返回成功
|
|||
return new CommonResult<>(); |
|||
} |
|||
|
|||
} |
@ -1,40 +0,0 @@ |
|||
package com.win.ssodemo.controller; |
|||
|
|||
import com.win.ssodemo.client.UserClient; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.user.UserInfoRespDTO; |
|||
import com.win.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); |
|||
} |
|||
|
|||
} |
@ -1,52 +0,0 @@ |
|||
package com.win.ssodemo.framework.config; |
|||
|
|||
import com.win.ssodemo.framework.core.filter.TokenAuthenticationFilter; |
|||
import com.win.ssodemo.framework.core.handler.AccessDeniedHandlerImpl; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.http.HttpMethod; |
|||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
|||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
|||
import org.springframework.security.web.AuthenticationEntryPoint; |
|||
import org.springframework.security.web.SecurityFilterChain; |
|||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
|||
|
|||
import javax.annotation.Resource; |
|||
|
|||
@Configuration(proxyBeanMethods = false) |
|||
@EnableWebSecurity |
|||
public class SecurityConfiguration { |
|||
|
|||
@Resource |
|||
private TokenAuthenticationFilter tokenAuthenticationFilter; |
|||
|
|||
@Resource |
|||
private AccessDeniedHandlerImpl accessDeniedHandler; |
|||
@Resource |
|||
private AuthenticationEntryPoint authenticationEntryPoint; |
|||
|
|||
@Bean |
|||
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { |
|||
// 设置 URL 安全权限
|
|||
httpSecurity.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); |
|||
return httpSecurity.build(); |
|||
} |
|||
|
|||
} |
@ -1,37 +0,0 @@ |
|||
package com.win.ssodemo.framework.core; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 登录用户信息 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
@Data |
|||
public class LoginUser { |
|||
|
|||
/** |
|||
* 用户编号 |
|||
*/ |
|||
private Long id; |
|||
/** |
|||
* 用户类型 |
|||
*/ |
|||
private Integer userType; |
|||
/** |
|||
* 租户编号 |
|||
*/ |
|||
private Long tenantId; |
|||
/** |
|||
* 授权范围 |
|||
*/ |
|||
private List<String> scopes; |
|||
|
|||
/** |
|||
* 访问令牌 |
|||
*/ |
|||
private String accessToken; |
|||
|
|||
} |
@ -1,66 +0,0 @@ |
|||
package com.win.ssodemo.framework.core.filter; |
|||
|
|||
import com.win.ssodemo.client.OAuth2Client; |
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; |
|||
import com.win.ssodemo.framework.core.LoginUser; |
|||
import com.win.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, "Authorization"); |
|||
if (StringUtils.hasText(token)) { |
|||
// 2. 基于 token 构建登录用户
|
|||
LoginUser loginUser = buildLoginUserByToken(token); |
|||
// 3. 设置当前用户
|
|||
if (loginUser != null) { |
|||
SecurityUtils.setLoginUser(loginUser, request); |
|||
} |
|||
} |
|||
|
|||
// 继续过滤链
|
|||
filterChain.doFilter(request, response); |
|||
} |
|||
|
|||
private LoginUser buildLoginUserByToken(String token) { |
|||
try { |
|||
CommonResult<OAuth2CheckTokenRespDTO> accessTokenResult = oauth2Client.checkToken(token); |
|||
OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); |
|||
if (accessToken == null) { |
|||
return null; |
|||
} |
|||
// 构建登录用户
|
|||
return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) |
|||
.setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) |
|||
.setAccessToken(accessToken.getAccessToken()); |
|||
} catch (Exception exception) { |
|||
// 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
|
|||
return null; |
|||
} |
|||
} |
|||
|
|||
} |
@ -1,44 +0,0 @@ |
|||
package com.win.ssodemo.framework.core.handler; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.ssodemo.framework.core.util.SecurityUtils; |
|||
import com.win.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); |
|||
} |
|||
|
|||
} |
@ -1,36 +0,0 @@ |
|||
package com.win.ssodemo.framework.core.handler; |
|||
|
|||
import com.win.ssodemo.client.dto.CommonResult; |
|||
import com.win.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); |
|||
} |
|||
|
|||
} |
@ -1,103 +0,0 @@ |
|||
package com.win.ssodemo.framework.core.util; |
|||
|
|||
import com.win.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; |
|||
} |
|||
|
|||
} |
@ -1,32 +0,0 @@ |
|||
package com.win.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); |
|||
} |
|||
|
|||
public static void write(HttpServletResponse response, String text, String contentType) { |
|||
ServletUtil.write(response, text, contentType); |
|||
} |
|||
|
|||
} |
@ -1,2 +0,0 @@ |
|||
server: |
|||
port: 18080 |
@ -1,154 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>首页</title> |
|||
<!-- jQuery:操作 dom、发起请求等 --> |
|||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> |
|||
|
|||
<script type="application/javascript"> |
|||
|
|||
/** |
|||
* 跳转单点登录 |
|||
*/ |
|||
function passwordLogin() { |
|||
window.location.href = '/login.html' |
|||
} |
|||
|
|||
/** |
|||
* 修改昵称 |
|||
*/ |
|||
function updateNickname() { |
|||
const nickname = prompt("请输入新的昵称", ""); |
|||
if (!nickname) { |
|||
return; |
|||
} |
|||
// 更新用户的昵称 |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/user/update?nickname=" + nickname, |
|||
method: 'PUT', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('更新昵称失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('更新昵称成功!'); |
|||
$('#nicknameSpan').html(nickname); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
*/ |
|||
function refreshToken() { |
|||
const refreshToken = localStorage.getItem('REFRESH-TOKEN'); |
|||
if (!refreshToken) { |
|||
alert("获取不到刷新令牌"); |
|||
return; |
|||
} |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/auth/refresh-token?refreshToken=" + refreshToken, |
|||
method: 'POST', |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('刷新访问令牌失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('更新访问令牌成功!'); |
|||
$('#accessTokenSpan').html(result.data.access_token); |
|||
|
|||
// 设置到 localStorage 中 |
|||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token); |
|||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 登出,删除访问令牌 |
|||
*/ |
|||
function logout() { |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
if (!accessToken) { |
|||
location.reload(); |
|||
return; |
|||
} |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/auth/logout", |
|||
method: 'POST', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('退出登录失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
alert('退出登录成功!'); |
|||
// 删除 localStorage 中 |
|||
localStorage.removeItem('ACCESS-TOKEN'); |
|||
localStorage.removeItem('REFRESH-TOKEN'); |
|||
|
|||
location.reload(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
$(function () { |
|||
const accessToken = localStorage.getItem('ACCESS-TOKEN'); |
|||
// 情况一:未登录 |
|||
if (!accessToken) { |
|||
$('#noLoginDiv').css("display", "block"); |
|||
return; |
|||
} |
|||
|
|||
// 情况二:已登录 |
|||
$('#yesLoginDiv').css("display", "block"); |
|||
$('#accessTokenSpan').html(accessToken); |
|||
// 获得登录用户的信息 |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:18080/user/get", |
|||
method: 'GET', |
|||
headers: { |
|||
'Authorization': 'Bearer ' + accessToken |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('获得个人信息失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
$('#nicknameSpan').html(result.data.nickname); |
|||
} |
|||
}); |
|||
}) |
|||
</script> |
|||
</head> |
|||
<body> |
|||
<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 --> |
|||
<div id="noLoginDiv" style="display: none"> |
|||
您未登录,点击 <a href="#" onclick="passwordLogin()">跳转 </a> 账号密码登录 |
|||
</div> |
|||
|
|||
<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 --> |
|||
<div id="yesLoginDiv" style="display: none"> |
|||
您已登录!<button onclick="logout()">退出登录</button> <br /> |
|||
昵称:<span id="nicknameSpan"> 加载中... </span> <button onclick="updateNickname()">修改昵称</button> <br /> |
|||
访问令牌:<span id="accessTokenSpan"> 加载中... </span> <button onclick="refreshToken()">刷新令牌</button> <br /> |
|||
</div> |
|||
</body> |
|||
<style> |
|||
body { /** 页面居中 */ |
|||
border-radius: 20px; |
|||
height: 350px; |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%,-50%); |
|||
} |
|||
</style> |
|||
</html> |
@ -1,74 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>登录</title> |
|||
<!-- jQuery:操作 dom、发起请求等 --> |
|||
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> |
|||
|
|||
<script type="application/javascript"> |
|||
|
|||
/** |
|||
* 账号密码登录 |
|||
*/ |
|||
function login() { |
|||
const clientId = 'win-sso-demo-by-password'; // 可以改写成,你的 clientId |
|||
const clientSecret = 'test'; // 可以改写成,你的 clientSecret |
|||
const grantType = 'password'; // 密码模式 |
|||
|
|||
// 账号 + 密码 |
|||
const username = $('#username').val(); |
|||
const password = $('#password').val(); |
|||
if (username.length === 0 || password.length === 0) { |
|||
alert('账号或密码未输入'); |
|||
return; |
|||
} |
|||
|
|||
// 发起请求 |
|||
$.ajax({ |
|||
url: "http://127.0.0.1:48080/admin-api/system/oauth2/token?" |
|||
// 客户端 |
|||
+ "client_id=" + clientId |
|||
+ "&client_secret=" + clientSecret |
|||
// 密码模式的参数 |
|||
+ "&grant_type=" + grantType |
|||
+ "&username=" + username |
|||
+ "&password=" + password |
|||
+ '&scope=user.read user.write', |
|||
method: 'POST', |
|||
headers: { |
|||
'tenant-id': '1', // 多租户编号,写死 |
|||
}, |
|||
success: function (result) { |
|||
if (result.code !== 0) { |
|||
alert('登录失败,原因:' + result.msg) |
|||
return; |
|||
} |
|||
// 设置到 localStorage 中 |
|||
localStorage.setItem('ACCESS-TOKEN', result.data.access_token); |
|||
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token); |
|||
|
|||
// 提示登录成功 |
|||
alert('登录成功!点击确认,跳转回首页'); |
|||
window.location.href = '/index.html'; |
|||
} |
|||
}); |
|||
} |
|||
</script> |
|||
</head> |
|||
<body> |
|||
账号:<input id="username" value="admin" /> <br /> |
|||
密码:<input id="password" value="admin123" > <br /> |
|||
<button style="float: right; margin-top: 5px;" onclick="login()">登录</button> |
|||
</body> |
|||
<style> |
|||
body { /** 页面居中 */ |
|||
border-radius: 20px; |
|||
height: 350px; |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%,-50%); |
|||
} |
|||
</style> |
|||
</html> |
@ -0,0 +1,24 @@ |
|||
<?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"> |
|||
<parent> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win</artifactId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<modules> |
|||
<module>win-module-wms-api</module> |
|||
<module>win-module-wms-biz</module> |
|||
</modules> |
|||
<artifactId>win-module-wms</artifactId> |
|||
<packaging>pom</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description> |
|||
system 模块下,我们放通用业务,支撑上层的核心业务。 |
|||
例如说:用户、部门、权限、数据字典等等 |
|||
</description> |
|||
|
|||
</project> |
@ -0,0 +1,34 @@ |
|||
<?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"> |
|||
<parent> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-wms</artifactId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<artifactId>win-module-wms-api</artifactId> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description> |
|||
wms 模块 API,暴露给其它模块调用 |
|||
</description> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-common</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 参数校验 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-validation</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
|
|||
</dependencies> |
|||
|
|||
</project> |
@ -0,0 +1,4 @@ |
|||
/** |
|||
* System API 包,定义暴露给其它模块的 API |
|||
*/ |
|||
package com.win.wms.api; |
@ -0,0 +1,123 @@ |
|||
<?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"> |
|||
<parent> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-wms</artifactId> |
|||
<version>${revision}</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<artifactId>win-module-wms-biz</artifactId> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>${project.artifactId}</name> |
|||
<description> |
|||
wms 模块下,我们放通用业务,支撑上层的核心业务。 |
|||
</description> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-system-api</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-module-infra-api</artifactId> |
|||
<version>${revision}</version> |
|||
</dependency> |
|||
|
|||
<!-- 业务组件 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-operatelog</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-sms</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-dict</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-data-permission</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-social</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-tenant</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-ip</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-biz-weixin</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Web 相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-security</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-validation</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- DB 相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-mybatis</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-redis</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Job 定时任务相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-job</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- 消息队列相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-mq</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Test 测试相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-test</artifactId> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
|
|||
<!-- 工具类相关 --> |
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-excel</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.win</groupId> |
|||
<artifactId>win-spring-boot-starter-captcha</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-mail</artifactId> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
</project> |
Loading…
Reference in new issue