使用sessionStorage和请求头验证拦截单点登录
This commit is contained in:
parent
f691164000
commit
e9bca69a3a
@ -38,5 +38,6 @@ public class JobLogRespDTO {
|
|||||||
@Schema(description = "任务状态")
|
@Schema(description = "任务状态")
|
||||||
@ExcelProperty("任务状态")
|
@ExcelProperty("任务状态")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
@Schema(description = "任务详细结果")
|
||||||
|
private String result;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>xxl-job-admin</artifactId>
|
<artifactId>xxl-job-admin</artifactId>
|
||||||
<groupId>cn.iocoder.cloud</groupId>
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
<version>2.1.0-jdk8-snapshot</version>
|
<version>${revision}</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
@ -23,8 +23,28 @@
|
|||||||
<artifactId>ludu-job-core</artifactId>
|
<artifactId>ludu-job-core</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-module-system</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
<!-- Spring Cloud 基础 -->
|
<!-- Spring Cloud 基础 -->
|
||||||
|
<!-- Web 相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
<version>5.8.22</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.cloud</groupId>
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
<artifactId>ludu-job-admin-api</artifactId>
|
<artifactId>ludu-job-admin-api</artifactId>
|
||||||
@ -62,10 +82,6 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Web 相关 -->
|
<!-- Web 相关 -->
|
||||||
<dependency>
|
|
||||||
<groupId>cn.iocoder.cloud</groupId>
|
|
||||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
@ -9,15 +9,23 @@ import com.xxl.job.admin.api.log.dto.JobLogRespDTO;
|
|||||||
import com.xxl.job.admin.controller.JobInfoController;
|
import com.xxl.job.admin.controller.JobInfoController;
|
||||||
import com.xxl.job.admin.controller.JobLogController;
|
import com.xxl.job.admin.controller.JobLogController;
|
||||||
import com.xxl.job.admin.core.model.XxlJobLog;
|
import com.xxl.job.admin.core.model.XxlJobLog;
|
||||||
|
import com.xxl.job.admin.core.scheduler.XxlJobScheduler;
|
||||||
|
import com.xxl.job.admin.core.util.I18nUtil;
|
||||||
import com.xxl.job.admin.dao.XxlJobGroupDao;
|
import com.xxl.job.admin.dao.XxlJobGroupDao;
|
||||||
import com.xxl.job.admin.dao.XxlJobInfoDao;
|
import com.xxl.job.admin.dao.XxlJobInfoDao;
|
||||||
import com.xxl.job.admin.dao.XxlJobLogDao;
|
import com.xxl.job.admin.dao.XxlJobLogDao;
|
||||||
|
import com.xxl.job.core.biz.ExecutorBiz;
|
||||||
|
import com.xxl.job.core.biz.model.LogParam;
|
||||||
|
import com.xxl.job.core.biz.model.LogResult;
|
||||||
|
import com.xxl.job.core.biz.model.ReturnT;
|
||||||
import com.xxl.job.core.util.DateUtil;
|
import com.xxl.job.core.util.DateUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.util.HtmlUtils;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -82,6 +90,36 @@ public class JobLogApiImpl implements JobLogApi {
|
|||||||
jobLogRespDTO.setHandlerName(load.getExecutorHandler());
|
jobLogRespDTO.setHandlerName(load.getExecutorHandler());
|
||||||
jobLogRespDTO.setHandlerParam(load.getExecutorParam());
|
jobLogRespDTO.setHandlerParam(load.getExecutorParam());
|
||||||
jobLogRespDTO.setStatus(load.getHandleCode() == 200 ? 1 : 2);
|
jobLogRespDTO.setStatus(load.getHandleCode() == 200 ? 1 : 2);
|
||||||
|
// 使用原系统的方法实现日志结果显示
|
||||||
|
try {
|
||||||
|
// valid
|
||||||
|
XxlJobLog jobLog = xxlJobLogDao.load(id);
|
||||||
|
if (jobLog == null) {
|
||||||
|
jobLogRespDTO.setResult("joblog_logid_unvalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
// log cat
|
||||||
|
ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(jobLog.getExecutorAddress());
|
||||||
|
ReturnT<LogResult> logResult = executorBiz.log(new LogParam(jobLog.getTriggerTime().getTime(), id, 1));
|
||||||
|
|
||||||
|
// is end
|
||||||
|
if (logResult.getContent() != null && logResult.getContent().getFromLineNum() > logResult.getContent().getToLineNum()) {
|
||||||
|
if (jobLog.getHandleCode() > 0) {
|
||||||
|
logResult.getContent().setEnd(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix xss
|
||||||
|
if (logResult.getContent() != null && StringUtils.hasText(logResult.getContent().getLogContent())) {
|
||||||
|
String newLogContent = logResult.getContent().getLogContent();
|
||||||
|
newLogContent = HtmlUtils.htmlEscape(newLogContent, "UTF-8");
|
||||||
|
logResult.getContent().setLogContent(newLogContent);
|
||||||
|
}
|
||||||
|
jobLogRespDTO.setResult(logResult.getContent().getLogContent());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
|
jobLogRespDTO.setResult("joblog_logid_failed");
|
||||||
|
}
|
||||||
return CommonResult.success(jobLogRespDTO);
|
return CommonResult.success(jobLogRespDTO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,157 @@
|
|||||||
|
package com.xxl.job.admin.client;
|
||||||
|
|
||||||
|
import com.xxl.job.admin.client.dto.CommonResult;
|
||||||
|
import com.xxl.job.admin.client.dto.oauth2.OAuth2AccessTokenRespDTO;
|
||||||
|
import com.xxl.job.admin.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 = "ludu-job-admin";
|
||||||
|
private static final String CLIENT_SECRET = "test";
|
||||||
|
|
||||||
|
|
||||||
|
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 code 授权码,获得访问令牌
|
||||||
|
*
|
||||||
|
* @param code 授权码
|
||||||
|
* @param redirectUri 重定向 URI
|
||||||
|
* @return 访问令牌
|
||||||
|
*/
|
||||||
|
public CommonResult<OAuth2AccessTokenRespDTO> postAccessToken(String code, String redirectUri) {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.set("tenant-id", TENANT_ID.toString());
|
||||||
|
addClientHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("grant_type", "authorization_code");
|
||||||
|
body.add("code", code);
|
||||||
|
body.add("redirect_uri", redirectUri);
|
||||||
|
// body.add("state", ""); // 选填;填了会校验
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验访问令牌,并返回它的基本信息
|
||||||
|
*
|
||||||
|
* @param token 访问令牌
|
||||||
|
* @return 访问令牌的基本信息
|
||||||
|
*/
|
||||||
|
public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.set("tenant-id", TENANT_ID.toString());
|
||||||
|
addClientHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("token", token);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/check-token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用刷新令牌,获得(刷新)访问令牌
|
||||||
|
*
|
||||||
|
* @param refreshToken 刷新令牌
|
||||||
|
* @return 访问令牌
|
||||||
|
*/
|
||||||
|
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(String refreshToken) {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.set("tenant-id", TENANT_ID.toString());
|
||||||
|
addClientHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("grant_type", "refresh_token");
|
||||||
|
body.add("refresh_token", refreshToken);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除访问令牌
|
||||||
|
*
|
||||||
|
* @param token 访问令牌
|
||||||
|
* @return 成功
|
||||||
|
*/
|
||||||
|
public CommonResult<Boolean> revokeToken(String token) {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.set("tenant-id", TENANT_ID.toString());
|
||||||
|
addClientHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("token", token);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.DELETE,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addClientHeader(HttpHeaders headers) {
|
||||||
|
// client 拼接,需要 BASE64 编码
|
||||||
|
String client = CLIENT_ID + ":" + CLIENT_SECRET;
|
||||||
|
client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8));
|
||||||
|
headers.add("Authorization", "Basic " + client);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package com.xxl.job.admin.client;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import com.xxl.job.admin.client.dto.user.UserInfoRespDTO;
|
||||||
|
import com.xxl.job.admin.client.dto.user.UserUpdateReqDTO;
|
||||||
|
import com.xxl.job.admin.framework.security.core.LoginUser;
|
||||||
|
import com.xxl.job.admin.framework.security.core.util.SecurityUtils;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户 User 信息的客户端
|
||||||
|
*
|
||||||
|
* 对应调用 OAuth2UserController 接口
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class UserClient {
|
||||||
|
|
||||||
|
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2/user";
|
||||||
|
|
||||||
|
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
public CommonResult<UserInfoRespDTO> getUser() {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString());
|
||||||
|
addTokenHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/get",
|
||||||
|
HttpMethod.GET,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommonResult<Boolean> updateUser(UserUpdateReqDTO updateReqDTO) {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString());
|
||||||
|
addTokenHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
// 使用 updateReqDTO 即可
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/update",
|
||||||
|
HttpMethod.PUT,
|
||||||
|
new HttpEntity<>(updateReqDTO, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void addTokenHeader(HttpHeaders headers) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
Assert.notNull(loginUser, "登录用户不能为空");
|
||||||
|
headers.add("Authorization", "Bearer " + loginUser.getAccessToken());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.xxl.job.admin.client.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用返回
|
||||||
|
*
|
||||||
|
* @param <T> 数据泛型
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CommonResult<T> implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误码
|
||||||
|
*/
|
||||||
|
private Integer code;
|
||||||
|
/**
|
||||||
|
* 返回数据
|
||||||
|
*/
|
||||||
|
private T data;
|
||||||
|
/**
|
||||||
|
* 错误提示,用户可阅读
|
||||||
|
*/
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package com.xxl.job.admin.client.dto.oauth2;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌 Response DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class OAuth2AccessTokenRespDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌
|
||||||
|
*/
|
||||||
|
@JsonProperty("access_token")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新令牌
|
||||||
|
*/
|
||||||
|
@JsonProperty("refresh_token")
|
||||||
|
private String refreshToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 令牌类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("token_type")
|
||||||
|
private String tokenType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间;单位:秒
|
||||||
|
*/
|
||||||
|
@JsonProperty("expires_in")
|
||||||
|
private Long expiresIn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权范围;如果多个授权范围,使用空格分隔
|
||||||
|
*/
|
||||||
|
private String scope;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package com.xxl.job.admin.client.dto.oauth2;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验令牌 Response DTO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class OAuth2CheckTokenRespDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
@JsonProperty("user_id")
|
||||||
|
private Long userId;
|
||||||
|
/**
|
||||||
|
* 用户类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("user_type")
|
||||||
|
private Integer userType;
|
||||||
|
/**
|
||||||
|
* 租户编号
|
||||||
|
*/
|
||||||
|
@JsonProperty("tenant_id")
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端编号
|
||||||
|
*/
|
||||||
|
@JsonProperty("client_id")
|
||||||
|
private String clientId;
|
||||||
|
/**
|
||||||
|
* 授权范围
|
||||||
|
*/
|
||||||
|
private List<String> scopes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌
|
||||||
|
*/
|
||||||
|
@JsonProperty("access_token")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间
|
||||||
|
*
|
||||||
|
* 时间戳 / 1000,即单位:秒
|
||||||
|
*/
|
||||||
|
private Long exp;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package com.xxl.job.admin.client.dto.user;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得用户基本信息 Response dto
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserInfoRespDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户账号
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户昵称
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户邮箱
|
||||||
|
*/
|
||||||
|
private String email;
|
||||||
|
/**
|
||||||
|
* 手机号码
|
||||||
|
*/
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户性别
|
||||||
|
*/
|
||||||
|
private Integer sex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户头像
|
||||||
|
*/
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所在部门
|
||||||
|
*/
|
||||||
|
private Dept dept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所属岗位数组
|
||||||
|
*/
|
||||||
|
private List<Post> posts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class Dept {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class Post {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package com.xxl.job.admin.client.dto.user;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户基本信息 Request DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserUpdateReqDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户昵称
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户邮箱
|
||||||
|
*/
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号码
|
||||||
|
*/
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户性别
|
||||||
|
*/
|
||||||
|
private Integer sex;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package com.xxl.job.admin.controller;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.xxl.job.admin.client.OAuth2Client;
|
||||||
|
import com.xxl.job.admin.client.dto.CommonResult;
|
||||||
|
import com.xxl.job.admin.client.dto.oauth2.OAuth2AccessTokenRespDTO;
|
||||||
|
import com.xxl.job.admin.framework.security.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<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -54,10 +54,10 @@ public class IndexController {
|
|||||||
@RequestMapping("/toLogin")
|
@RequestMapping("/toLogin")
|
||||||
@PermissionLimit(limit=false)
|
@PermissionLimit(limit=false)
|
||||||
public ModelAndView toLogin(HttpServletRequest request, HttpServletResponse response,ModelAndView modelAndView) {
|
public ModelAndView toLogin(HttpServletRequest request, HttpServletResponse response,ModelAndView modelAndView) {
|
||||||
if (loginService.ifLogin(request, response) != null) {
|
/*if (loginService.ifLogin(request, response) != null) {
|
||||||
modelAndView.setView(new RedirectView("/",true,false));
|
modelAndView.setView(new RedirectView("/",true,false));
|
||||||
return modelAndView;
|
return modelAndView;
|
||||||
}
|
}*/
|
||||||
return new ModelAndView("login");
|
return new ModelAndView("login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,6 @@ public class PermissionInterceptor implements AsyncHandlerInterceptor {
|
|||||||
request.setAttribute(LoginService.LOGIN_IDENTITY_KEY, loginUser);
|
request.setAttribute(LoginService.LOGIN_IDENTITY_KEY, loginUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO 未解决单点登录问题 暂时直接放行 前面有网关的系统登录拦截
|
|
||||||
return true; // proceed with the next interceptor
|
return true; // proceed with the next interceptor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,39 +1,58 @@
|
|||||||
package com.xxl.job.admin.framework.security.config;
|
package com.xxl.job.admin.framework.security.config;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
|
|
||||||
import com.xxl.job.admin.enums.ApiConstants;
|
import com.xxl.job.admin.enums.ApiConstants;
|
||||||
|
import com.xxl.job.admin.framework.security.core.filter.TokenAuthenticationFilter;
|
||||||
|
import com.xxl.job.admin.framework.security.core.handler.AccessDeniedHandlerImpl;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
|
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Demo 模块的 Security 配置
|
* Demo 模块的 Security 配置
|
||||||
*/
|
*/
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@EnableWebSecurity
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
@Resource
|
||||||
|
private TokenAuthenticationFilter tokenAuthenticationFilter;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AccessDeniedHandlerImpl accessDeniedHandler;
|
||||||
|
@Resource
|
||||||
|
private AuthenticationEntryPoint authenticationEntryPoint;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
|
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
|
||||||
return new AuthorizeRequestsCustomizer() {
|
// 设置 URL 安全权限
|
||||||
|
httpSecurity.csrf().disable() // 禁用 CSRF 保护
|
||||||
|
.authorizeRequests()
|
||||||
|
// 1. 静态资源,可匿名访问
|
||||||
|
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
|
||||||
|
// 2. 登录相关的接口,可匿名访问
|
||||||
|
.antMatchers("/toLogin").permitAll()
|
||||||
|
.antMatchers("/**").permitAll()
|
||||||
|
.antMatchers("/auth/login-by-code").permitAll()
|
||||||
|
.antMatchers("/auth/refresh-token").permitAll()
|
||||||
|
.antMatchers("/auth/logout").permitAll()
|
||||||
|
// last. 兜底规则,必须认证
|
||||||
|
.and().authorizeRequests()
|
||||||
|
.anyRequest().authenticated();
|
||||||
|
|
||||||
@Override
|
// 设置处理器
|
||||||
public void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
|
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
|
||||||
// Swagger 接口文档
|
.authenticationEntryPoint(authenticationEntryPoint);
|
||||||
registry.antMatchers("/v3/api-docs/**").permitAll() // 元数据
|
|
||||||
.antMatchers("/swagger-ui.html").permitAll(); // Swagger UI
|
|
||||||
// Druid 监控
|
|
||||||
registry.antMatchers("/druid/**").anonymous();
|
|
||||||
// Spring Boot Actuator 的安全配置
|
|
||||||
registry.antMatchers("/actuator").anonymous()
|
|
||||||
.antMatchers("/actuator/**").anonymous();
|
|
||||||
// RPC 服务的安全配置
|
|
||||||
registry.antMatchers(ApiConstants.PREFIX + "/**").permitAll();
|
|
||||||
// 放行所有后台原本请求和rpc接口
|
|
||||||
registry.antMatchers("/**").permitAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
// 添加 Token Filter
|
||||||
|
httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
return httpSecurity.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
package com.xxl.job.admin.framework.security.core;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录用户信息
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class LoginUser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
/**
|
||||||
|
* 用户类型
|
||||||
|
*/
|
||||||
|
private Integer userType;
|
||||||
|
/**
|
||||||
|
* 租户编号
|
||||||
|
*/
|
||||||
|
private Long tenantId;
|
||||||
|
/**
|
||||||
|
* 授权范围
|
||||||
|
*/
|
||||||
|
private List<String> scopes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌
|
||||||
|
*/
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package com.xxl.job.admin.framework.security.core.filter;
|
||||||
|
|
||||||
|
|
||||||
|
import com.xxl.job.admin.client.OAuth2Client;
|
||||||
|
import com.xxl.job.admin.client.dto.CommonResult;
|
||||||
|
import com.xxl.job.admin.client.dto.oauth2.OAuth2CheckTokenRespDTO;
|
||||||
|
import com.xxl.job.admin.framework.security.core.LoginUser;
|
||||||
|
import com.xxl.job.admin.framework.security.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package com.xxl.job.admin.framework.security.core.handler;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||||
|
import com.xxl.job.admin.client.dto.CommonResult;
|
||||||
|
import com.xxl.job.admin.framework.security.core.util.SecurityUtils;
|
||||||
|
import com.xxl.job.admin.framework.security.core.util.ServletUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
|
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。
|
||||||
|
*
|
||||||
|
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@SuppressWarnings("JavadocReference")
|
||||||
|
@Slf4j
|
||||||
|
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
// 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏
|
||||||
|
log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(),
|
||||||
|
SecurityUtils.getLoginUserId(), e);
|
||||||
|
// 返回 403
|
||||||
|
CommonResult<Object> result = new CommonResult<>();
|
||||||
|
result.setCode(HttpStatus.FORBIDDEN.value());
|
||||||
|
result.setMsg("没有该操作权限");
|
||||||
|
ServletUtils.writeJSON(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package com.xxl.job.admin.framework.security.core.handler;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||||
|
import com.xxl.job.admin.client.dto.CommonResult;
|
||||||
|
import com.xxl.job.admin.framework.security.core.util.ServletUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页
|
||||||
|
*
|
||||||
|
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@SuppressWarnings("JavadocReference") // 忽略文档引用报错
|
||||||
|
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
|
||||||
|
log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e);
|
||||||
|
// 返回 401
|
||||||
|
CommonResult<Object> result = new CommonResult<>();
|
||||||
|
result.setCode(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
result.setMsg("账号未登录");
|
||||||
|
ServletUtils.writeJSON(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package com.xxl.job.admin.framework.security.core.util;
|
||||||
|
|
||||||
|
import com.xxl.job.admin.framework.security.core.LoginUser;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全服务工具类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public class SecurityUtils {
|
||||||
|
|
||||||
|
public static final String AUTHORIZATION_BEARER = "Bearer";
|
||||||
|
|
||||||
|
private SecurityUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从请求中,获得认证 Token
|
||||||
|
*
|
||||||
|
* @param request 请求
|
||||||
|
* @param header 认证 Token 对应的 Header 名字
|
||||||
|
* @return 认证 Token
|
||||||
|
*/
|
||||||
|
public static String obtainAuthorization(HttpServletRequest request, String header) {
|
||||||
|
String authorization = request.getHeader(header);
|
||||||
|
if (!StringUtils.hasText(authorization)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int index = authorization.indexOf(AUTHORIZATION_BEARER + " ");
|
||||||
|
if (index == -1) { // 未找到
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return authorization.substring(index + 7).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前认证信息
|
||||||
|
*
|
||||||
|
* @return 认证信息
|
||||||
|
*/
|
||||||
|
public static Authentication getAuthentication() {
|
||||||
|
SecurityContext context = SecurityContextHolder.getContext();
|
||||||
|
if (context == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return context.getAuthentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户
|
||||||
|
*
|
||||||
|
* @return 当前用户
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static LoginUser getLoginUser() {
|
||||||
|
Authentication authentication = getAuthentication();
|
||||||
|
if (authentication == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前用户的编号,从上下文中
|
||||||
|
*
|
||||||
|
* @return 用户编号
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Long getLoginUserId() {
|
||||||
|
LoginUser loginUser = getLoginUser();
|
||||||
|
return loginUser != null ? loginUser.getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前用户
|
||||||
|
*
|
||||||
|
* @param loginUser 登录用户
|
||||||
|
* @param request 请求
|
||||||
|
*/
|
||||||
|
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
|
||||||
|
// 创建 Authentication,并设置到上下文
|
||||||
|
Authentication authentication = buildAuthentication(loginUser, request);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
|
||||||
|
// 创建 UsernamePasswordAuthenticationToken 对象
|
||||||
|
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||||
|
loginUser, null, Collections.emptyList());
|
||||||
|
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
return authenticationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package com.xxl.job.admin.framework.security.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -63,6 +63,9 @@ $(function() {
|
|||||||
'glueSource' : glueSource,
|
'glueSource' : glueSource,
|
||||||
'glueRemark' : glueRemark
|
'glueRemark' : glueRemark
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
|
@ -8,6 +8,9 @@ $(function() {
|
|||||||
"ajax": {
|
"ajax": {
|
||||||
url: base_url + "/jobgroup/pageList",
|
url: base_url + "/jobgroup/pageList",
|
||||||
type:"post",
|
type:"post",
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
data : function ( d ) {
|
data : function ( d ) {
|
||||||
var obj = {};
|
var obj = {};
|
||||||
obj.appname = $('#appname').val();
|
obj.appname = $('#appname').val();
|
||||||
@ -166,6 +169,9 @@ $(function() {
|
|||||||
type : 'POST',
|
type : 'POST',
|
||||||
url : base_url + '/jobgroup/remove',
|
url : base_url + '/jobgroup/remove',
|
||||||
data : {"id":id},
|
data : {"id":id},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
|
@ -8,6 +8,9 @@ $(function() {
|
|||||||
"ajax": {
|
"ajax": {
|
||||||
url: base_url + "/jobinfo/pageList",
|
url: base_url + "/jobinfo/pageList",
|
||||||
type:"post",
|
type:"post",
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
data : function ( d ) {
|
data : function ( d ) {
|
||||||
var obj = {};
|
var obj = {};
|
||||||
obj.jobGroup = $('#jobGroup').val();
|
obj.jobGroup = $('#jobGroup').val();
|
||||||
@ -243,6 +246,9 @@ $(function() {
|
|||||||
data : {
|
data : {
|
||||||
"id" : id
|
"id" : id
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
@ -278,6 +284,9 @@ $(function() {
|
|||||||
"executorParam" : $("#jobTriggerModal .textarea[name='executorParam']").val(),
|
"executorParam" : $("#jobTriggerModal .textarea[name='executorParam']").val(),
|
||||||
"addressList" : $("#jobTriggerModal .textarea[name='addressList']").val()
|
"addressList" : $("#jobTriggerModal .textarea[name='addressList']").val()
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
@ -308,6 +317,9 @@ $(function() {
|
|||||||
data : {
|
data : {
|
||||||
"id" : jobGroup
|
"id" : jobGroup
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
|
|
||||||
@ -342,6 +354,9 @@ $(function() {
|
|||||||
"scheduleType" : row.scheduleType,
|
"scheduleType" : row.scheduleType,
|
||||||
"scheduleConf" : row.scheduleConf
|
"scheduleConf" : row.scheduleConf
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
|
|
||||||
|
@ -28,6 +28,9 @@ $(function() {
|
|||||||
"logId":logId,
|
"logId":logId,
|
||||||
"fromLineNum":fromLineNum
|
"fromLineNum":fromLineNum
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
|
|
||||||
|
@ -8,6 +8,9 @@ $(function() {
|
|||||||
async: false, // async, avoid js invoke pagelist before jobId data init
|
async: false, // async, avoid js invoke pagelist before jobId data init
|
||||||
url : base_url + '/joblog/getJobsByGroup',
|
url : base_url + '/joblog/getJobsByGroup',
|
||||||
data : {"jobGroup":jobGroup},
|
data : {"jobGroup":jobGroup},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
@ -77,6 +80,9 @@ $(function() {
|
|||||||
"ajax": {
|
"ajax": {
|
||||||
url: base_url + "/joblog/pageList" ,
|
url: base_url + "/joblog/pageList" ,
|
||||||
type:"post",
|
type:"post",
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
data : function ( d ) {
|
data : function ( d ) {
|
||||||
var obj = {};
|
var obj = {};
|
||||||
obj.jobGroup = $('#jobGroup').val();
|
obj.jobGroup = $('#jobGroup').val();
|
||||||
@ -279,6 +285,9 @@ $(function() {
|
|||||||
type : 'POST',
|
type : 'POST',
|
||||||
url : base_url + '/joblog/logKill',
|
url : base_url + '/joblog/logKill',
|
||||||
data : {"id":_id},
|
data : {"id":_id},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
|
@ -8,6 +8,9 @@ $(function() {
|
|||||||
"ajax": {
|
"ajax": {
|
||||||
url: base_url + "/user/pageList",
|
url: base_url + "/user/pageList",
|
||||||
type:"post",
|
type:"post",
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
data : function ( d ) {
|
data : function ( d ) {
|
||||||
var obj = {};
|
var obj = {};
|
||||||
obj.username = $('#username').val();
|
obj.username = $('#username').val();
|
||||||
@ -124,6 +127,9 @@ $(function() {
|
|||||||
data : {
|
data : {
|
||||||
"id" : id
|
"id" : id
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
|
@ -644,6 +644,9 @@
|
|||||||
"scheduleType" : 'CRON',
|
"scheduleType" : 'CRON',
|
||||||
"scheduleConf" : inputElement.val()
|
"scheduleConf" : inputElement.val()
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
|
@ -644,6 +644,9 @@
|
|||||||
"scheduleType" : 'CRON',
|
"scheduleType" : 'CRON',
|
||||||
"scheduleConf" : inputElement.val()
|
"scheduleConf" : inputElement.val()
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN')
|
||||||
|
},
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data){
|
success : function(data){
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
|
@ -1,45 +1,105 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<#import "./common/common.macro.ftl" as netCommon>
|
<#import "./common/common.macro.ftl" as netCommon>
|
||||||
<@netCommon.commonStyle />
|
<@netCommon.commonStyle />
|
||||||
<link rel="stylesheet" href="${request.contextPath}/static/adminlte/plugins/iCheck/square/blue.css">
|
<link rel="stylesheet" href="${request.contextPath}/static/adminlte/plugins/iCheck/square/blue.css">
|
||||||
<title>${I18n.admin_name}</title>
|
<title>${I18n.admin_name}</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="hold-transition login-page">
|
<body class="hold-transition login-page">
|
||||||
<div class="login-box">
|
<#-- <div class="login-box">-->
|
||||||
<div class="login-logo">
|
<#-- <div class="login-logo">-->
|
||||||
<a><b>XXL</b>JOB</a>
|
<#-- <a><b>XXL</b>JOB</a>-->
|
||||||
</div>
|
<#-- </div>-->
|
||||||
<form id="loginForm" method="post" >
|
<#-- <form id="loginForm" method="post" >-->
|
||||||
<div class="login-box-body">
|
<#-- <div class="login-box-body">-->
|
||||||
<p class="login-box-msg">${I18n.admin_name}</p>
|
<#-- <p class="login-box-msg">${I18n.admin_name}</p>-->
|
||||||
<div class="form-group has-feedback">
|
<#-- <div class="form-group has-feedback">-->
|
||||||
<input type="text" name="userName" class="form-control" placeholder="${I18n.login_username_placeholder}" maxlength="18" >
|
<#-- <input type="text" name="userName" class="form-control" placeholder="${I18n.login_username_placeholder}" maxlength="18" >-->
|
||||||
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
<#-- <span class="glyphicon glyphicon-envelope form-control-feedback"></span>-->
|
||||||
</div>
|
<#-- </div>-->
|
||||||
<div class="form-group has-feedback">
|
<#-- <div class="form-group has-feedback">-->
|
||||||
<input type="password" name="password" class="form-control" placeholder="${I18n.login_password_placeholder}" maxlength="18" >
|
<#-- <input type="password" name="password" class="form-control" placeholder="${I18n.login_password_placeholder}" maxlength="18" >-->
|
||||||
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
|
<#-- <span class="glyphicon glyphicon-lock form-control-feedback"></span>-->
|
||||||
</div>
|
<#-- </div>-->
|
||||||
<div class="row">
|
<#-- <div class="row">-->
|
||||||
<div class="col-xs-8">
|
<#-- <div class="col-xs-8">-->
|
||||||
<div class="checkbox icheck">
|
<#-- <div class="checkbox icheck">-->
|
||||||
<label>
|
<#-- <label>-->
|
||||||
<input type="checkbox" name="ifRemember" > ${I18n.login_remember_me}
|
<#-- <input type="checkbox" name="ifRemember" > ${I18n.login_remember_me}-->
|
||||||
</label>
|
<#-- </label>-->
|
||||||
</div>
|
<#-- </div>-->
|
||||||
</div><!-- /.col -->
|
<#-- </div><!-- /.col –>-->
|
||||||
<div class="col-xs-4">
|
<#-- <div class="col-xs-4">-->
|
||||||
<button type="submit" class="btn btn-primary btn-block btn-flat">${I18n.login_btn}</button>
|
<#-- <button type="submit" class="btn btn-primary btn-block btn-flat">${I18n.login_btn}</button>-->
|
||||||
</div>
|
<#-- </div>-->
|
||||||
</div>
|
<#-- </div>-->
|
||||||
</div>
|
<#-- </div>-->
|
||||||
</form>
|
<#-- </form>-->
|
||||||
</div>
|
<#-- </div>-->
|
||||||
<@netCommon.commonScript />
|
<@netCommon.commonScript />
|
||||||
<script src="${request.contextPath}/static/adminlte/plugins/iCheck/icheck.min.js"></script>
|
<#--<script src="${request.contextPath}/static/adminlte/plugins/iCheck/icheck.min.js"></script>-->
|
||||||
<script src="${request.contextPath}/static/js/login.1.js"></script>
|
<#--<script src="${request.contextPath}/static/js/login.1.js"></script>-->
|
||||||
|
<script src="${request.contextPath}/static/adminlte/bower_components/jquery/jquery.min.js"></script>
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* 跳转单点登录
|
||||||
|
*/
|
||||||
|
function ssoLogin() {
|
||||||
|
const clientId = 'ludu-job-admin'; // 可以改写成,你的 clientId
|
||||||
|
const redirectUri = encodeURIComponent('http://127.0.0.1:9090/xxl-job-admin/toLogin'); // 注意,需要使用 encodeURIComponent 编码地址
|
||||||
|
const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token
|
||||||
|
window.location.href = 'http://127.0.0.1:80/sso?client_id=' + clientId
|
||||||
|
+ '&redirect_uri=' + redirectUri
|
||||||
|
+ '&response_type=' + responseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = localStorage.getItem('ACCESS-TOKEN');
|
||||||
|
// 情况一:未登录
|
||||||
|
if (!accessToken) {
|
||||||
|
// 获取 URL 中的查询参数
|
||||||
|
let urlParams = new URLSearchParams(window.location.search);
|
||||||
|
// 获取 code 参数的值
|
||||||
|
let code = urlParams.get('code');
|
||||||
|
if (!code) {
|
||||||
|
ssoLogin();
|
||||||
|
} else {
|
||||||
|
// 提交
|
||||||
|
const redirectUri = 'http://127.0.0.1:9090/xxl-job-admin/toLogin'; // 需要修改成,你回调的地址,就是在 index.html 拼接的 redirectUri
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:9090/xxl-job-admin/auth/login-by-code?code=" + code
|
||||||
|
+ '&redirectUri=' + redirectUri,
|
||||||
|
method: 'POST',
|
||||||
|
success: function( result ) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
// layer.open({
|
||||||
|
// title: I18n.system_tips,
|
||||||
|
// btn: [ I18n.system_ok ],
|
||||||
|
// content: (data.msg || (I18n.jobgroup_del + I18n.system_fail)),
|
||||||
|
// icon: '2'
|
||||||
|
// });
|
||||||
|
alert('获得访问令牌失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 设置到 localStorage 中
|
||||||
|
localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
|
||||||
|
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
|
||||||
|
localStorage.setItem('EXPIRES-IN', Date.now() + result.data.expires_in * 1000);
|
||||||
|
// 跳转回首页
|
||||||
|
window.location.href = '/xxl-job-admin';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 情况二:Token失效(暂未实现)
|
||||||
|
else if (Date.now() > localStorage.getItem('EXPIRES-IN')) {
|
||||||
|
console.log('重新获取授权码')
|
||||||
|
}
|
||||||
|
// 情况三:已登录
|
||||||
|
else {
|
||||||
|
window.location.href = '/xxl-job-admin';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -186,7 +186,6 @@ public class TicketingSamplingJob {
|
|||||||
saleData.setItemname((String) currentMap.get("itemname"));
|
saleData.setItemname((String) currentMap.get("itemname"));
|
||||||
saleData.setSalepropetyvaluename((String) currentMap.get("salepropetyvaluename"));
|
saleData.setSalepropetyvaluename((String) currentMap.get("salepropetyvaluename"));
|
||||||
saleData.setTenant_id(1L);
|
saleData.setTenant_id(1L);
|
||||||
|
|
||||||
if (ticketSaleDataApi.createSaleData(saleData).getCode() != 0) {
|
if (ticketSaleDataApi.createSaleData(saleData).getCode() != 0) {
|
||||||
failedCount++;
|
failedCount++;
|
||||||
XxlJobHelper.log("第{}条数据插入失败,数据值为:{}", ((pageNumber - 1) * PAGE_SIZE + i + 1), currentMap);
|
XxlJobHelper.log("第{}条数据插入失败,数据值为:{}", ((pageNumber - 1) * PAGE_SIZE + i + 1), currentMap);
|
||||||
@ -216,8 +215,6 @@ public class TicketingSamplingJob {
|
|||||||
checkTicket.setSdshipping((String) currentMap.get("sdshipping"));
|
checkTicket.setSdshipping((String) currentMap.get("sdshipping"));
|
||||||
checkTicket.setSalepropetyvaluename((String) currentMap.get("salepropetyvaluename"));
|
checkTicket.setSalepropetyvaluename((String) currentMap.get("salepropetyvaluename"));
|
||||||
checkTicket.setTenant_id(1L);
|
checkTicket.setTenant_id(1L);
|
||||||
ticketCheckTicketApi.createCheckTicket(checkTicket);
|
|
||||||
|
|
||||||
if (ticketCheckTicketApi.createCheckTicket(checkTicket).getCode() != 0) {
|
if (ticketCheckTicketApi.createCheckTicket(checkTicket).getCode() != 0) {
|
||||||
failedCount++;
|
failedCount++;
|
||||||
XxlJobHelper.log("第{}条数据插入失败,数据值为:{}", ((pageNumber - 1) * PAGE_SIZE + i + 1), currentMap);
|
XxlJobHelper.log("第{}条数据插入失败,数据值为:{}", ((pageNumber - 1) * PAGE_SIZE + i + 1), currentMap);
|
||||||
|
1
pom.xml
1
pom.xml
@ -18,6 +18,7 @@
|
|||||||
<module>ludu-module-ticket-manager</module>
|
<module>ludu-module-ticket-manager</module>
|
||||||
<module>ludu-module-sampling</module>
|
<module>ludu-module-sampling</module>
|
||||||
<module>ludu-job-admin</module>
|
<module>ludu-job-admin</module>
|
||||||
|
<module>yudao-sso-demo-by-code</module>
|
||||||
<!-- <module>yudao-module-bpm</module>-->
|
<!-- <module>yudao-module-bpm</module>-->
|
||||||
<!-- <module>yudao-module-pay</module>-->
|
<!-- <module>yudao-module-pay</module>-->
|
||||||
<!-- <module>yudao-module-report</module>-->
|
<!-- <module>yudao-module-report</module>-->
|
||||||
|
@ -73,7 +73,19 @@
|
|||||||
<groupId>io.github.mouzt</groupId>
|
<groupId>io.github.mouzt</groupId>
|
||||||
<artifactId>bizlog-sdk</artifactId>
|
<artifactId>bizlog-sdk</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Oauth2单点登录 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -54,7 +54,7 @@ public class JobLogController {
|
|||||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||||
List<JobLogRespDTO> list = jobLogApi.getPage(pageReqVO.getPageNo(), pageReqVO.getPageSize(), pageReqVO.getJobId(), pageReqVO.getHandlerName(), pageReqVO.getBeginTime(), pageReqVO.getEndTime(), pageReqVO.getStatus()).getData().getList();
|
List<JobLogRespDTO> list = jobLogApi.getPage(pageReqVO.getPageNo(), pageReqVO.getPageSize(), pageReqVO.getJobId(), pageReqVO.getHandlerName(), pageReqVO.getBeginTime(), pageReqVO.getEndTime(), pageReqVO.getStatus()).getData().getList();
|
||||||
//导出 Excel
|
//导出 Excel
|
||||||
ExcelUtils.write(response, "任务调度.xls", "数据", JobLogRespDTO.class,
|
ExcelUtils.write(response, "任务调度日志.xls", "数据", JobLogRespDTO.class,
|
||||||
list);
|
list);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
|||||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
|
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
|
||||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
|
import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO;
|
||||||
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
|
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
|
||||||
import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
|
import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
|
||||||
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
|
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package cn.iocoder.yudao.module.system.service.auth;
|
package cn.iocoder.yudao.module.system.service.auth;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO;
|
||||||
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
|
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
|
||||||
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
|
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
|
||||||
|
|
||||||
|
@ -3,10 +3,12 @@ package cn.iocoder.yudao.module.system.service.auth;
|
|||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
|
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
|
||||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||||
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
|
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
|
||||||
|
import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO;
|
||||||
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
|
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
|
||||||
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
|
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
|
||||||
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
|
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
|
||||||
@ -29,10 +31,18 @@ import com.xingyuv.captcha.model.vo.CaptchaVO;
|
|||||||
import com.xingyuv.captcha.service.CaptchaService;
|
import com.xingyuv.captcha.service.CaptchaService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.http.*;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
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 javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import javax.validation.Validator;
|
import javax.validation.Validator;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
@ -70,6 +80,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
|
|||||||
*/
|
*/
|
||||||
@Value("${yudao.captcha.enable:true}")
|
@Value("${yudao.captcha.enable:true}")
|
||||||
private Boolean captchaEnable;
|
private Boolean captchaEnable;
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AdminUserDO authenticate(String username, String password) {
|
public AdminUserDO authenticate(String username, String password) {
|
||||||
|
69
yudao-sso-demo-by-code/pom.xml
Normal file
69
yudao-sso-demo-by-code/pom.xml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<!-- 由于方便大家拷贝,使用不使用 yudao 作为 Maven parent -->
|
||||||
|
<parent>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
<groupId>cn.iocoder</groupId>
|
||||||
|
<artifactId>yudao-sso-demo-by-code</artifactId>
|
||||||
|
<version>1.0.0-snapshot</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>${project.artifactId}</name>
|
||||||
|
<description>基于授权码模式,如何实现 SSO 单点登录?</description>
|
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<!-- Maven 相关 -->
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<!-- 统一依赖管理 -->
|
||||||
|
<spring.boot.version>2.7.18</spring.boot.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<!-- 统一依赖管理 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Web 相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
<version>5.8.22</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
@ -0,0 +1,13 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class SSODemoApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(SSODemoApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.client;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.Base64Utils;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth 2.0 客户端
|
||||||
|
*
|
||||||
|
* 对应调用 OAuth2OpenController 接口
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class OAuth2Client {
|
||||||
|
|
||||||
|
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户编号
|
||||||
|
*
|
||||||
|
* 默认使用 1;如果使用别的租户,可以调整
|
||||||
|
*/
|
||||||
|
public static final Long TENANT_ID = 1L;
|
||||||
|
|
||||||
|
private static final String CLIENT_ID = "yudao-sso-demo-by-code";
|
||||||
|
private static final String CLIENT_SECRET = "test";
|
||||||
|
|
||||||
|
|
||||||
|
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 code 授权码,获得访问令牌
|
||||||
|
*
|
||||||
|
* @param code 授权码
|
||||||
|
* @param redirectUri 重定向 URI
|
||||||
|
* @return 访问令牌
|
||||||
|
*/
|
||||||
|
public CommonResult<OAuth2AccessTokenRespDTO> postAccessToken(String code, String redirectUri) {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.set("tenant-id", TENANT_ID.toString());
|
||||||
|
addClientHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("grant_type", "authorization_code");
|
||||||
|
body.add("code", code);
|
||||||
|
body.add("redirect_uri", redirectUri);
|
||||||
|
// body.add("state", ""); // 选填;填了会校验
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验访问令牌,并返回它的基本信息
|
||||||
|
*
|
||||||
|
* @param token 访问令牌
|
||||||
|
* @return 访问令牌的基本信息
|
||||||
|
*/
|
||||||
|
public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.set("tenant-id", TENANT_ID.toString());
|
||||||
|
addClientHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("token", token);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/check-token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用刷新令牌,获得(刷新)访问令牌
|
||||||
|
*
|
||||||
|
* @param refreshToken 刷新令牌
|
||||||
|
* @return 访问令牌
|
||||||
|
*/
|
||||||
|
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(String refreshToken) {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.set("tenant-id", TENANT_ID.toString());
|
||||||
|
addClientHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("grant_type", "refresh_token");
|
||||||
|
body.add("refresh_token", refreshToken);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.POST,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除访问令牌
|
||||||
|
*
|
||||||
|
* @param token 访问令牌
|
||||||
|
* @return 成功
|
||||||
|
*/
|
||||||
|
public CommonResult<Boolean> revokeToken(String token) {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.set("tenant-id", TENANT_ID.toString());
|
||||||
|
addClientHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("token", token);
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/token",
|
||||||
|
HttpMethod.DELETE,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addClientHeader(HttpHeaders headers) {
|
||||||
|
// client 拼接,需要 BASE64 编码
|
||||||
|
String client = CLIENT_ID + ":" + CLIENT_SECRET;
|
||||||
|
client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8));
|
||||||
|
headers.add("Authorization", "Basic " + client);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.client;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO;
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户 User 信息的客户端
|
||||||
|
*
|
||||||
|
* 对应调用 OAuth2UserController 接口
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class UserClient {
|
||||||
|
|
||||||
|
private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user";
|
||||||
|
|
||||||
|
// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
public CommonResult<UserInfoRespDTO> getUser() {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString());
|
||||||
|
addTokenHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<UserInfoRespDTO>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/get",
|
||||||
|
HttpMethod.GET,
|
||||||
|
new HttpEntity<>(body, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<UserInfoRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommonResult<Boolean> updateUser(UserUpdateReqDTO updateReqDTO) {
|
||||||
|
// 1.1 构建请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("tenant-id", OAuth2Client.TENANT_ID.toString());
|
||||||
|
addTokenHeader(headers);
|
||||||
|
// 1.2 构建请求参数
|
||||||
|
// 使用 updateReqDTO 即可
|
||||||
|
|
||||||
|
// 2. 执行请求
|
||||||
|
ResponseEntity<CommonResult<Boolean>> exchange = restTemplate.exchange(
|
||||||
|
BASE_URL + "/update",
|
||||||
|
HttpMethod.PUT,
|
||||||
|
new HttpEntity<>(updateReqDTO, headers),
|
||||||
|
new ParameterizedTypeReference<CommonResult<Boolean>>() {}); // 解决 CommonResult 的泛型丢失
|
||||||
|
Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
|
||||||
|
return exchange.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void addTokenHeader(HttpHeaders headers) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
Assert.notNull(loginUser, "登录用户不能为空");
|
||||||
|
headers.add("Authorization", "Bearer " + loginUser.getAccessToken());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.client.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用返回
|
||||||
|
*
|
||||||
|
* @param <T> 数据泛型
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CommonResult<T> implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误码
|
||||||
|
*/
|
||||||
|
private Integer code;
|
||||||
|
/**
|
||||||
|
* 返回数据
|
||||||
|
*/
|
||||||
|
private T data;
|
||||||
|
/**
|
||||||
|
* 错误提示,用户可阅读
|
||||||
|
*/
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.client.dto.oauth2;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌 Response DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class OAuth2AccessTokenRespDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌
|
||||||
|
*/
|
||||||
|
@JsonProperty("access_token")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新令牌
|
||||||
|
*/
|
||||||
|
@JsonProperty("refresh_token")
|
||||||
|
private String refreshToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 令牌类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("token_type")
|
||||||
|
private String tokenType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间;单位:秒
|
||||||
|
*/
|
||||||
|
@JsonProperty("expires_in")
|
||||||
|
private Long expiresIn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权范围;如果多个授权范围,使用空格分隔
|
||||||
|
*/
|
||||||
|
private String scope;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.client.dto.oauth2;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验令牌 Response DTO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class OAuth2CheckTokenRespDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
@JsonProperty("user_id")
|
||||||
|
private Long userId;
|
||||||
|
/**
|
||||||
|
* 用户类型
|
||||||
|
*/
|
||||||
|
@JsonProperty("user_type")
|
||||||
|
private Integer userType;
|
||||||
|
/**
|
||||||
|
* 租户编号
|
||||||
|
*/
|
||||||
|
@JsonProperty("tenant_id")
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端编号
|
||||||
|
*/
|
||||||
|
@JsonProperty("client_id")
|
||||||
|
private String clientId;
|
||||||
|
/**
|
||||||
|
* 授权范围
|
||||||
|
*/
|
||||||
|
private List<String> scopes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌
|
||||||
|
*/
|
||||||
|
@JsonProperty("access_token")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间
|
||||||
|
*
|
||||||
|
* 时间戳 / 1000,即单位:秒
|
||||||
|
*/
|
||||||
|
private Long exp;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.client.dto.user;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得用户基本信息 Response dto
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserInfoRespDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户账号
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户昵称
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户邮箱
|
||||||
|
*/
|
||||||
|
private String email;
|
||||||
|
/**
|
||||||
|
* 手机号码
|
||||||
|
*/
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户性别
|
||||||
|
*/
|
||||||
|
private Integer sex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户头像
|
||||||
|
*/
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所在部门
|
||||||
|
*/
|
||||||
|
private Dept dept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所属岗位数组
|
||||||
|
*/
|
||||||
|
private List<Post> posts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class Dept {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class Post {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 岗位名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.client.dto.user;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户基本信息 Request DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserUpdateReqDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户昵称
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户邮箱
|
||||||
|
*/
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号码
|
||||||
|
*/
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户性别
|
||||||
|
*/
|
||||||
|
private Integer sex;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.controller;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/auth")
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OAuth2Client oauth2Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 code 访问令牌,获得访问令牌
|
||||||
|
*
|
||||||
|
* @param code 授权码
|
||||||
|
* @param redirectUri 重定向 URI
|
||||||
|
* @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段
|
||||||
|
*/
|
||||||
|
@PostMapping("/login-by-code")
|
||||||
|
public CommonResult<OAuth2AccessTokenRespDTO> loginByCode(@RequestParam("code") String code,
|
||||||
|
@RequestParam("redirectUri") String redirectUri) {
|
||||||
|
return oauth2Client.postAccessToken(code, redirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用刷新令牌,获得(刷新)访问令牌
|
||||||
|
*
|
||||||
|
* @param refreshToken 刷新令牌
|
||||||
|
* @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段
|
||||||
|
*/
|
||||||
|
@PostMapping("/refresh-token")
|
||||||
|
public CommonResult<OAuth2AccessTokenRespDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
|
||||||
|
return oauth2Client.refreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*
|
||||||
|
* @param request 请求
|
||||||
|
* @return 成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public CommonResult<Boolean> logout(HttpServletRequest request) {
|
||||||
|
String token = SecurityUtils.obtainAuthorization(request, "Authorization");
|
||||||
|
if (StrUtil.isNotBlank(token)) {
|
||||||
|
return oauth2Client.revokeToken(token);
|
||||||
|
}
|
||||||
|
// 返回成功
|
||||||
|
return new CommonResult<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.controller;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.UserClient;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/user")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserClient userClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前登录用户的基本信息
|
||||||
|
*
|
||||||
|
* @return 用户信息;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段
|
||||||
|
*/
|
||||||
|
@GetMapping("/get")
|
||||||
|
public CommonResult<UserInfoRespDTO> getUser() {
|
||||||
|
return userClient.getUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新当前登录用户的昵称
|
||||||
|
*
|
||||||
|
* @param nickname 昵称
|
||||||
|
* @return 成功
|
||||||
|
*/
|
||||||
|
@PutMapping("/update")
|
||||||
|
public CommonResult<Boolean> updateUser(@RequestParam("nickname") String nickname) {
|
||||||
|
UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null);
|
||||||
|
return userClient.updateUser(updateReqDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.framework.config;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.filter.TokenAuthenticationFilter;
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.handler.AccessDeniedHandlerImpl;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.security.config.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.framework.core;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录用户信息
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class LoginUser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
/**
|
||||||
|
* 用户类型
|
||||||
|
*/
|
||||||
|
private Integer userType;
|
||||||
|
/**
|
||||||
|
* 租户编号
|
||||||
|
*/
|
||||||
|
private Long tenantId;
|
||||||
|
/**
|
||||||
|
* 授权范围
|
||||||
|
*/
|
||||||
|
private List<String> scopes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问令牌
|
||||||
|
*/
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.framework.core.filter;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token 过滤器,验证 token 的有效性
|
||||||
|
* 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OAuth2Client oauth2Client;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
// 1. 获得访问令牌
|
||||||
|
String token = SecurityUtils.obtainAuthorization(request, "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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.framework.core.handler;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils;
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
|
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。
|
||||||
|
*
|
||||||
|
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@SuppressWarnings("JavadocReference")
|
||||||
|
@Slf4j
|
||||||
|
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
// 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏
|
||||||
|
log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(),
|
||||||
|
SecurityUtils.getLoginUserId(), e);
|
||||||
|
// 返回 403
|
||||||
|
CommonResult<Object> result = new CommonResult<>();
|
||||||
|
result.setCode(HttpStatus.FORBIDDEN.value());
|
||||||
|
result.setMsg("没有该操作权限");
|
||||||
|
ServletUtils.writeJSON(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.framework.core.handler;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页
|
||||||
|
*
|
||||||
|
* 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@SuppressWarnings("JavadocReference") // 忽略文档引用报错
|
||||||
|
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
|
||||||
|
log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e);
|
||||||
|
// 返回 401
|
||||||
|
CommonResult<Object> result = new CommonResult<>();
|
||||||
|
result.setCode(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
result.setMsg("账号未登录");
|
||||||
|
ServletUtils.writeJSON(response, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.framework.core.util;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.ssodemo.framework.core.LoginUser;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全服务工具类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public class SecurityUtils {
|
||||||
|
|
||||||
|
public static final String AUTHORIZATION_BEARER = "Bearer";
|
||||||
|
|
||||||
|
private SecurityUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从请求中,获得认证 Token
|
||||||
|
*
|
||||||
|
* @param request 请求
|
||||||
|
* @param header 认证 Token 对应的 Header 名字
|
||||||
|
* @return 认证 Token
|
||||||
|
*/
|
||||||
|
public static String obtainAuthorization(HttpServletRequest request, String header) {
|
||||||
|
String authorization = request.getHeader(header);
|
||||||
|
if (!StringUtils.hasText(authorization)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int index = authorization.indexOf(AUTHORIZATION_BEARER + " ");
|
||||||
|
if (index == -1) { // 未找到
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return authorization.substring(index + 7).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前认证信息
|
||||||
|
*
|
||||||
|
* @return 认证信息
|
||||||
|
*/
|
||||||
|
public static Authentication getAuthentication() {
|
||||||
|
SecurityContext context = SecurityContextHolder.getContext();
|
||||||
|
if (context == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return context.getAuthentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户
|
||||||
|
*
|
||||||
|
* @return 当前用户
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static LoginUser getLoginUser() {
|
||||||
|
Authentication authentication = getAuthentication();
|
||||||
|
if (authentication == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前用户的编号,从上下文中
|
||||||
|
*
|
||||||
|
* @return 用户编号
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Long getLoginUserId() {
|
||||||
|
LoginUser loginUser = getLoginUser();
|
||||||
|
return loginUser != null ? loginUser.getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前用户
|
||||||
|
*
|
||||||
|
* @param loginUser 登录用户
|
||||||
|
* @param request 请求
|
||||||
|
*/
|
||||||
|
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
|
||||||
|
// 创建 Authentication,并设置到上下文
|
||||||
|
Authentication authentication = buildAuthentication(loginUser, request);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
|
||||||
|
// 创建 UsernamePasswordAuthenticationToken 对象
|
||||||
|
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||||
|
loginUser, null, Collections.emptyList());
|
||||||
|
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
return authenticationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package cn.iocoder.yudao.ssodemo.framework.core.util;
|
||||||
|
|
||||||
|
import cn.hutool.extra.servlet.ServletUtil;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端工具类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public class ServletUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 JSON 字符串
|
||||||
|
*
|
||||||
|
* @param response 响应
|
||||||
|
* @param object 对象,会序列化成 JSON 字符串
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
|
||||||
|
public static void writeJSON(HttpServletResponse response, Object object) {
|
||||||
|
String content = JSONUtil.toJsonStr(object);
|
||||||
|
ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void write(HttpServletResponse response, String text, String contentType) {
|
||||||
|
ServletUtil.write(response, text, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
server:
|
||||||
|
port: 18080
|
@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>SSO 授权后的回调页</title>
|
||||||
|
<!-- jQuery:操作 dom、发起请求等 -->
|
||||||
|
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
|
||||||
|
<!-- 工具类 -->
|
||||||
|
<script type="application/javascript">
|
||||||
|
(function ($) {
|
||||||
|
/**
|
||||||
|
* 获得 URL 的指定参数的值
|
||||||
|
*
|
||||||
|
* @param name 参数名
|
||||||
|
* @returns 参数值
|
||||||
|
*/
|
||||||
|
$.getUrlParam = function (name) {
|
||||||
|
const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||||
|
const r = window.location.search.substr(1).match(reg);
|
||||||
|
if (r != null) return unescape(r[2]); return null;
|
||||||
|
}
|
||||||
|
})(jQuery);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
$(function () {
|
||||||
|
// 获得 code 授权码
|
||||||
|
const code = $.getUrlParam('code');
|
||||||
|
if (!code) {
|
||||||
|
alert('获取不到 code 参数,请排查!')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
const redirectUri = 'http://127.0.0.1:18080/callback.html'; // 需要修改成,你回调的地址,就是在 index.html 拼接的 redirectUri
|
||||||
|
$.ajax({
|
||||||
|
url: "http://127.0.0.1:18080/auth/login-by-code?code=" + code
|
||||||
|
+ '&redirectUri=' + redirectUri,
|
||||||
|
method: 'POST',
|
||||||
|
success: function( result ) {
|
||||||
|
if (result.code !== 0) {
|
||||||
|
alert('获得访问令牌失败,原因:' + result.msg)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert('获得访问令牌成功!点击确认,跳转回首页')
|
||||||
|
|
||||||
|
// 设置到 localStorage 中
|
||||||
|
localStorage.setItem('ACCESS-TOKEN', result.data.access_token);
|
||||||
|
localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token);
|
||||||
|
|
||||||
|
// 跳转回首页
|
||||||
|
window.location.href = '/index.html';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
正在使用 code 授权码,进行 accessToken 访问令牌的获取
|
||||||
|
</body>
|
||||||
|
</html>
|
159
yudao-sso-demo-by-code/src/main/resources/static/index.html
Normal file
159
yudao-sso-demo-by-code/src/main/resources/static/index.html
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>首页</title>
|
||||||
|
<!-- jQuery:操作 dom、发起请求等 -->
|
||||||
|
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转单点登录
|
||||||
|
*/
|
||||||
|
function ssoLogin() {
|
||||||
|
const clientId = 'yudao-sso-demo-by-code'; // 可以改写成,你的 clientId
|
||||||
|
const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址
|
||||||
|
const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token
|
||||||
|
window.location.href = 'http://127.0.0.1:80/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>
|
Loading…
Reference in New Issue
Block a user