diff --git a/ludu-job-admin/ludu-job-admin-api/src/main/java/com/xxl/job/admin/api/log/dto/JobLogRespDTO.java b/ludu-job-admin/ludu-job-admin-api/src/main/java/com/xxl/job/admin/api/log/dto/JobLogRespDTO.java index fa0b6f931..0149d9e8d 100644 --- a/ludu-job-admin/ludu-job-admin-api/src/main/java/com/xxl/job/admin/api/log/dto/JobLogRespDTO.java +++ b/ludu-job-admin/ludu-job-admin-api/src/main/java/com/xxl/job/admin/api/log/dto/JobLogRespDTO.java @@ -38,5 +38,6 @@ public class JobLogRespDTO { @Schema(description = "任务状态") @ExcelProperty("任务状态") private Integer status; - + @Schema(description = "任务详细结果") + private String result; } diff --git a/ludu-job-admin/ludu-job-admin-biz/pom.xml b/ludu-job-admin/ludu-job-admin-biz/pom.xml index 5d73635c8..faedb7e1c 100644 --- a/ludu-job-admin/ludu-job-admin-biz/pom.xml +++ b/ludu-job-admin/ludu-job-admin-biz/pom.xml @@ -5,7 +5,7 @@ xxl-job-admin cn.iocoder.cloud - 2.1.0-jdk8-snapshot + ${revision} 4.0.0 @@ -23,8 +23,28 @@ ludu-job-core ${revision} + + cn.iocoder.cloud + yudao-module-system + ${revision} + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + + cn.hutool + hutool-all + 5.8.22 + cn.iocoder.cloud ludu-job-admin-api @@ -62,10 +82,6 @@ - - cn.iocoder.cloud - yudao-spring-boot-starter-security - org.springframework.boot diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/api/log/JobLogApiImpl.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/api/log/JobLogApiImpl.java index 74d43a19a..9287b8b68 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/api/log/JobLogApiImpl.java +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/api/log/JobLogApiImpl.java @@ -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.JobLogController; 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.XxlJobInfoDao; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.HtmlUtils; import javax.annotation.Resource; import java.util.Date; @@ -82,6 +90,36 @@ public class JobLogApiImpl implements JobLogApi { jobLogRespDTO.setHandlerName(load.getExecutorHandler()); jobLogRespDTO.setHandlerParam(load.getExecutorParam()); 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 = 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); } } diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/OAuth2Client.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/OAuth2Client.java new file mode 100644 index 000000000..80fc9563b --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/OAuth2Client.java @@ -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 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 body = new LinkedMultiValueMap<>(); + body.add("grant_type", "authorization_code"); + body.add("code", code); + body.add("redirect_uri", redirectUri); +// body.add("state", ""); // 选填;填了会校验 + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 校验访问令牌,并返回它的基本信息 + * + * @param token 访问令牌 + * @return 访问令牌的基本信息 + */ + public CommonResult 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 body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/check-token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌 + */ + public CommonResult 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 body = new LinkedMultiValueMap<>(); + body.add("grant_type", "refresh_token"); + body.add("refresh_token", refreshToken); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 删除访问令牌 + * + * @param token 访问令牌 + * @return 成功 + */ + public CommonResult 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 body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.DELETE, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 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); + } + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/UserClient.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/UserClient.java new file mode 100644 index 000000000..884b164b0 --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/UserClient.java @@ -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 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 body = new LinkedMultiValueMap<>(); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/get", + HttpMethod.GET, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + public CommonResult 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> exchange = restTemplate.exchange( + BASE_URL + "/update", + HttpMethod.PUT, + new HttpEntity<>(updateReqDTO, headers), + new ParameterizedTypeReference>() {}); // 解决 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()); + } +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/CommonResult.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/CommonResult.java new file mode 100644 index 000000000..ef23cdab4 --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/CommonResult.java @@ -0,0 +1,28 @@ +package com.xxl.job.admin.client.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + */ + private String msg; + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/oauth2/OAuth2AccessTokenRespDTO.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/oauth2/OAuth2AccessTokenRespDTO.java new file mode 100644 index 000000000..67b9ea7d3 --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/oauth2/OAuth2AccessTokenRespDTO.java @@ -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; + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/oauth2/OAuth2CheckTokenRespDTO.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/oauth2/OAuth2CheckTokenRespDTO.java new file mode 100644 index 000000000..97c3c58d9 --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/oauth2/OAuth2CheckTokenRespDTO.java @@ -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 scopes; + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 过期时间 + * + * 时间戳 / 1000,即单位:秒 + */ + private Long exp; + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/user/UserInfoRespDTO.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/user/UserInfoRespDTO.java new file mode 100644 index 000000000..0b3a44dca --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/user/UserInfoRespDTO.java @@ -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 posts; + + /** + * 部门 + */ + @Data + public static class Dept { + + /** + * 部门编号 + */ + private Long id; + + /** + * 部门名称 + */ + private String name; + + } + + /** + * 岗位 + */ + @Data + public static class Post { + + /** + * 岗位编号 + */ + private Long id; + + /** + * 岗位名称 + */ + private String name; + + } + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/user/UserUpdateReqDTO.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/user/UserUpdateReqDTO.java new file mode 100644 index 000000000..9ceed44e8 --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/client/dto/user/UserUpdateReqDTO.java @@ -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; + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/AuthController.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/AuthController.java new file mode 100644 index 000000000..03e4a3245 --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/AuthController.java @@ -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 loginByCode(@RequestParam("code") String code, + @RequestParam("redirectUri") String redirectUri) { + return oauth2Client.postAccessToken(code, redirectUri); + } + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @PostMapping("/refresh-token") + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return oauth2Client.refreshToken(refreshToken); + } + + /** + * 退出登录 + * + * @param request 请求 + * @return 成功 + */ + @PostMapping("/logout") + public CommonResult logout(HttpServletRequest request) { + String token = SecurityUtils.obtainAuthorization(request, "Authorization"); + if (StrUtil.isNotBlank(token)) { + return oauth2Client.revokeToken(token); + } + // 返回成功 + return new CommonResult<>(); + } + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/IndexController.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/IndexController.java index eb63f0bd4..4e4ef4c62 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/IndexController.java +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/IndexController.java @@ -54,10 +54,10 @@ public class IndexController { @RequestMapping("/toLogin") @PermissionLimit(limit=false) 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)); return modelAndView; - } + }*/ return new ModelAndView("login"); } diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java index e80202ac1..840f0ebcf 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java @@ -53,7 +53,6 @@ public class PermissionInterceptor implements AsyncHandlerInterceptor { request.setAttribute(LoginService.LOGIN_IDENTITY_KEY, loginUser); } - // TODO 未解决单点登录问题 暂时直接放行 前面有网关的系统登录拦截 return true; // proceed with the next interceptor } diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/config/SecurityConfiguration.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/config/SecurityConfiguration.java index a66983351..743300f66 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/config/SecurityConfiguration.java +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/config/SecurityConfiguration.java @@ -1,39 +1,58 @@ 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.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.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.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 配置 */ @Configuration(proxyBeanMethods = false) +@EnableWebSecurity public class SecurityConfiguration { + @Resource + private TokenAuthenticationFilter tokenAuthenticationFilter; + + @Resource + private AccessDeniedHandlerImpl accessDeniedHandler; + @Resource + private AuthenticationEntryPoint authenticationEntryPoint; @Bean - public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { - return new AuthorizeRequestsCustomizer() { + protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + // 设置 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.ExpressionInterceptUrlRegistry registry) { - // Swagger 接口文档 - 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(); - } + // 设置处理器 + httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler) + .authenticationEntryPoint(authenticationEntryPoint); - }; + // 添加 Token Filter + httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + return httpSecurity.build(); } } diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/LoginUser.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/LoginUser.java new file mode 100644 index 000000000..17ed79931 --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/LoginUser.java @@ -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 scopes; + + /** + * 访问令牌 + */ + private String accessToken; + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/filter/TokenAuthenticationFilter.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 000000000..3fcb5d88b --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/filter/TokenAuthenticationFilter.java @@ -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 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; + } + } + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/handler/AccessDeniedHandlerImpl.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 000000000..1d5576791 --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/handler/AccessDeniedHandlerImpl.java @@ -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 result = new CommonResult<>(); + result.setCode(HttpStatus.FORBIDDEN.value()); + result.setMsg("没有该操作权限"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/handler/AuthenticationEntryPointImpl.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 000000000..1b8fbf8cb --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/handler/AuthenticationEntryPointImpl.java @@ -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 result = new CommonResult<>(); + result.setCode(HttpStatus.UNAUTHORIZED.value()); + result.setMsg("账号未登录"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/util/SecurityUtils.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/util/SecurityUtils.java new file mode 100644 index 000000000..0bc7401e6 --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/util/SecurityUtils.java @@ -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; + } + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/util/ServletUtils.java b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/util/ServletUtils.java new file mode 100644 index 000000000..b0884e577 --- /dev/null +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/java/com/xxl/job/admin/framework/security/core/util/ServletUtils.java @@ -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); + } + +} diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobcode.index.1.js b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobcode.index.1.js index 668d63470..12a0c95d7 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobcode.index.1.js +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobcode.index.1.js @@ -63,6 +63,9 @@ $(function() { 'glueSource' : glueSource, 'glueRemark' : glueRemark }, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ if (data.code == 200) { diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobgroup.index.1.js b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobgroup.index.1.js index 0e5b23565..f4d12c9c0 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobgroup.index.1.js +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobgroup.index.1.js @@ -8,6 +8,9 @@ $(function() { "ajax": { url: base_url + "/jobgroup/pageList", type:"post", + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, data : function ( d ) { var obj = {}; obj.appname = $('#appname').val(); @@ -166,6 +169,9 @@ $(function() { type : 'POST', url : base_url + '/jobgroup/remove', data : {"id":id}, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ if (data.code == 200) { diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobinfo.index.1.js b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobinfo.index.1.js index b479e9724..4bb80fb21 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobinfo.index.1.js +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/jobinfo.index.1.js @@ -8,6 +8,9 @@ $(function() { "ajax": { url: base_url + "/jobinfo/pageList", type:"post", + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, data : function ( d ) { var obj = {}; obj.jobGroup = $('#jobGroup').val(); @@ -243,6 +246,9 @@ $(function() { data : { "id" : id }, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ if (data.code == 200) { @@ -278,6 +284,9 @@ $(function() { "executorParam" : $("#jobTriggerModal .textarea[name='executorParam']").val(), "addressList" : $("#jobTriggerModal .textarea[name='addressList']").val() }, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ if (data.code == 200) { @@ -308,6 +317,9 @@ $(function() { data : { "id" : jobGroup }, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ @@ -342,6 +354,9 @@ $(function() { "scheduleType" : row.scheduleType, "scheduleConf" : row.scheduleConf }, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/joblog.detail.1.js b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/joblog.detail.1.js index 0638eee27..c0d7dc6f0 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/joblog.detail.1.js +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/joblog.detail.1.js @@ -28,6 +28,9 @@ $(function() { "logId":logId, "fromLineNum":fromLineNum }, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/joblog.index.1.js b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/joblog.index.1.js index e0fc3f20b..7fe5f66c8 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/joblog.index.1.js +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/joblog.index.1.js @@ -8,6 +8,9 @@ $(function() { async: false, // async, avoid js invoke pagelist before jobId data init url : base_url + '/joblog/getJobsByGroup', data : {"jobGroup":jobGroup}, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ if (data.code == 200) { @@ -77,6 +80,9 @@ $(function() { "ajax": { url: base_url + "/joblog/pageList" , type:"post", + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, data : function ( d ) { var obj = {}; obj.jobGroup = $('#jobGroup').val(); @@ -279,6 +285,9 @@ $(function() { type : 'POST', url : base_url + '/joblog/logKill', data : {"id":_id}, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ if (data.code == 200) { diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/user.index.1.js b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/user.index.1.js index 48d3f3020..86f3dce07 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/user.index.1.js +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/js/user.index.1.js @@ -8,6 +8,9 @@ $(function() { "ajax": { url: base_url + "/user/pageList", type:"post", + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, data : function ( d ) { var obj = {}; obj.username = $('#username').val(); @@ -124,6 +127,9 @@ $(function() { data : { "id" : id }, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ if (data.code == 200) { diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/plugins/cronGen/cronGen.js b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/plugins/cronGen/cronGen.js index 223937224..9cc0c3fb8 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/plugins/cronGen/cronGen.js +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/plugins/cronGen/cronGen.js @@ -644,6 +644,9 @@ "scheduleType" : 'CRON', "scheduleConf" : inputElement.val() }, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ if (data.code === 200) { diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/plugins/cronGen/cronGen_en.js b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/plugins/cronGen/cronGen_en.js index cbf84ee3f..ebdedc73e 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/plugins/cronGen/cronGen_en.js +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/static/plugins/cronGen/cronGen_en.js @@ -644,6 +644,9 @@ "scheduleType" : 'CRON', "scheduleConf" : inputElement.val() }, + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('ACCESS-TOKEN') + }, dataType : "json", success : function(data){ if (data.code === 200) { diff --git a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/templates/login.ftl b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/templates/login.ftl index c3f696385..671f7fb16 100644 --- a/ludu-job-admin/ludu-job-admin-biz/src/main/resources/templates/login.ftl +++ b/ludu-job-admin/ludu-job-admin-biz/src/main/resources/templates/login.ftl @@ -1,45 +1,105 @@ - <#import "./common/common.macro.ftl" as netCommon> - <@netCommon.commonStyle /> + <#import "./common/common.macro.ftl" as netCommon> + <@netCommon.commonStyle /> - ${I18n.admin_name} + ${I18n.admin_name} - +<#-- --> <@netCommon.commonScript /> - - +<#----> +<#----> + + diff --git a/ludu-module-sampling/ludu-module-sampling-biz/src/main/java/cn/iocoder/yudao/module/sampling/service/ticketing/TicketingSamplingJob.java b/ludu-module-sampling/ludu-module-sampling-biz/src/main/java/cn/iocoder/yudao/module/sampling/service/ticketing/TicketingSamplingJob.java index b571c75f0..2ad20ea96 100644 --- a/ludu-module-sampling/ludu-module-sampling-biz/src/main/java/cn/iocoder/yudao/module/sampling/service/ticketing/TicketingSamplingJob.java +++ b/ludu-module-sampling/ludu-module-sampling-biz/src/main/java/cn/iocoder/yudao/module/sampling/service/ticketing/TicketingSamplingJob.java @@ -186,7 +186,6 @@ public class TicketingSamplingJob { saleData.setItemname((String) currentMap.get("itemname")); saleData.setSalepropetyvaluename((String) currentMap.get("salepropetyvaluename")); saleData.setTenant_id(1L); - if (ticketSaleDataApi.createSaleData(saleData).getCode() != 0) { failedCount++; XxlJobHelper.log("第{}条数据插入失败,数据值为:{}", ((pageNumber - 1) * PAGE_SIZE + i + 1), currentMap); @@ -216,8 +215,6 @@ public class TicketingSamplingJob { checkTicket.setSdshipping((String) currentMap.get("sdshipping")); checkTicket.setSalepropetyvaluename((String) currentMap.get("salepropetyvaluename")); checkTicket.setTenant_id(1L); - ticketCheckTicketApi.createCheckTicket(checkTicket); - if (ticketCheckTicketApi.createCheckTicket(checkTicket).getCode() != 0) { failedCount++; XxlJobHelper.log("第{}条数据插入失败,数据值为:{}", ((pageNumber - 1) * PAGE_SIZE + i + 1), currentMap); diff --git a/pom.xml b/pom.xml index 691ab0ada..bd6567f3b 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ ludu-module-ticket-manager ludu-module-sampling ludu-job-admin + yudao-sso-demo-by-code diff --git a/yudao-framework/yudao-spring-boot-starter-security/pom.xml b/yudao-framework/yudao-spring-boot-starter-security/pom.xml index 18d4c73b9..3720d81f6 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-security/pom.xml @@ -73,7 +73,19 @@ io.github.mouzt bizlog-sdk - + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.security + spring-security-oauth2-jose + diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/joblog/JobLogController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/joblog/JobLogController.java index ac8c60f90..e1047d03c 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/joblog/JobLogController.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/joblog/JobLogController.java @@ -54,7 +54,7 @@ public class JobLogController { pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = jobLogApi.getPage(pageReqVO.getPageNo(), pageReqVO.getPageSize(), pageReqVO.getJobId(), pageReqVO.getHandlerName(), pageReqVO.getBeginTime(), pageReqVO.getEndTime(), pageReqVO.getStatus()).getData().getList(); //导出 Excel - ExcelUtils.write(response, "任务调度.xls", "数据", JobLogRespDTO.class, + ExcelUtils.write(response, "任务调度日志.xls", "数据", JobLogRespDTO.class, list); } } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index 521e4e7cf..105183c3d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -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.security.config.SecurityProperties; 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.convert.auth.AuthConvert; import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java index 52796ec2f..6de1c90ef 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java @@ -1,5 +1,6 @@ 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.dal.dataobject.user.AdminUserDO; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java index 53a6b2075..eeea14dec 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java @@ -3,10 +3,12 @@ package cn.iocoder.yudao.module.system.service.auth; import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 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.servlet.ServletUtils; 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.oauth2.dto.OAuth2AccessTokenRespDTO; 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.SocialUserRespDTO; @@ -29,10 +31,18 @@ import com.xingyuv.captcha.model.vo.CaptchaVO; import com.xingyuv.captcha.service.CaptchaService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; 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.validation.Validator; +import java.nio.charset.StandardCharsets; import java.util.Objects; 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}") private Boolean captchaEnable; + private final RestTemplate restTemplate = new RestTemplate(); @Override public AdminUserDO authenticate(String username, String password) { diff --git a/yudao-sso-demo-by-code/pom.xml b/yudao-sso-demo-by-code/pom.xml new file mode 100644 index 000000000..4ced4f2e0 --- /dev/null +++ b/yudao-sso-demo-by-code/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + + cn.iocoder.cloud + yudao + ${revision} + + cn.iocoder + yudao-sso-demo-by-code + 1.0.0-snapshot + jar + + ${project.artifactId} + 基于授权码模式,如何实现 SSO 单点登录? + https://github.com/YunaiV/ruoyi-vue-pro + + + + 8 + 8 + UTF-8 + + 2.7.18 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + cn.hutool + hutool-all + 5.8.22 + + + + org.projectlombok + lombok + true + + + + diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java new file mode 100644 index 000000000..f6b160745 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/SSODemoApplication.java @@ -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); + } + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java new file mode 100644 index 000000000..d2e160f6f --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java @@ -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 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 body = new LinkedMultiValueMap<>(); + body.add("grant_type", "authorization_code"); + body.add("code", code); + body.add("redirect_uri", redirectUri); +// body.add("state", ""); // 选填;填了会校验 + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 校验访问令牌,并返回它的基本信息 + * + * @param token 访问令牌 + * @return 访问令牌的基本信息 + */ + public CommonResult 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 body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/check-token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌 + */ + public CommonResult 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 body = new LinkedMultiValueMap<>(); + body.add("grant_type", "refresh_token"); + body.add("refresh_token", refreshToken); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 删除访问令牌 + * + * @param token 访问令牌 + * @return 成功 + */ + public CommonResult 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 body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.DELETE, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 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); + } + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java new file mode 100644 index 000000000..666bd3ee4 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java @@ -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 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 body = new LinkedMultiValueMap<>(); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/get", + HttpMethod.GET, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + public CommonResult 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> exchange = restTemplate.exchange( + BASE_URL + "/update", + HttpMethod.PUT, + new HttpEntity<>(updateReqDTO, headers), + new ParameterizedTypeReference>() {}); // 解决 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()); + } +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java new file mode 100644 index 000000000..548fe51e4 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.ssodemo.client.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + */ + private String msg; + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java new file mode 100644 index 000000000..6a5369a20 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java @@ -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; + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java new file mode 100644 index 000000000..862bcf04b --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java @@ -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 scopes; + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 过期时间 + * + * 时间戳 / 1000,即单位:秒 + */ + private Long exp; + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java new file mode 100644 index 000000000..e81bea9eb --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserInfoRespDTO.java @@ -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 posts; + + /** + * 部门 + */ + @Data + public static class Dept { + + /** + * 部门编号 + */ + private Long id; + + /** + * 部门名称 + */ + private String name; + + } + + /** + * 岗位 + */ + @Data + public static class Post { + + /** + * 岗位编号 + */ + private Long id; + + /** + * 岗位名称 + */ + private String name; + + } + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java new file mode 100644 index 000000000..e711d7311 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java @@ -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; + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java new file mode 100644 index 000000000..fe64a1547 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java @@ -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 loginByCode(@RequestParam("code") String code, + @RequestParam("redirectUri") String redirectUri) { + return oauth2Client.postAccessToken(code, redirectUri); + } + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @PostMapping("/refresh-token") + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return oauth2Client.refreshToken(refreshToken); + } + + /** + * 退出登录 + * + * @param request 请求 + * @return 成功 + */ + @PostMapping("/logout") + public CommonResult logout(HttpServletRequest request) { + String token = SecurityUtils.obtainAuthorization(request, "Authorization"); + if (StrUtil.isNotBlank(token)) { + return oauth2Client.revokeToken(token); + } + // 返回成功 + return new CommonResult<>(); + } + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java new file mode 100644 index 000000000..819c752bc --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java @@ -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 getUser() { + return userClient.getUser(); + } + + /** + * 更新当前登录用户的昵称 + * + * @param nickname 昵称 + * @return 成功 + */ + @PutMapping("/update") + public CommonResult updateUser(@RequestParam("nickname") String nickname) { + UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null); + return userClient.updateUser(updateReqDTO); + } + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java new file mode 100644 index 000000000..0a71a650f --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java @@ -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(); + } + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java new file mode 100644 index 000000000..44f3edf54 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java @@ -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 scopes; + + /** + * 访问令牌 + */ + private String accessToken; + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 000000000..70bb40eaa --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java @@ -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 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; + } + } + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 000000000..dccc35eb1 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java @@ -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 result = new CommonResult<>(); + result.setCode(HttpStatus.FORBIDDEN.value()); + result.setMsg("没有该操作权限"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 000000000..241ff56f5 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java @@ -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 result = new CommonResult<>(); + result.setCode(HttpStatus.UNAUTHORIZED.value()); + result.setMsg("账号未登录"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java new file mode 100644 index 000000000..0352a4785 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java @@ -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; + } + +} diff --git a/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java new file mode 100644 index 000000000..dec561ff1 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java @@ -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); + } + +} diff --git a/yudao-sso-demo-by-code/src/main/resources/application.yaml b/yudao-sso-demo-by-code/src/main/resources/application.yaml new file mode 100644 index 000000000..a62cf97dc --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/resources/application.yaml @@ -0,0 +1,2 @@ +server: + port: 18080 diff --git a/yudao-sso-demo-by-code/src/main/resources/static/callback.html b/yudao-sso-demo-by-code/src/main/resources/static/callback.html new file mode 100644 index 000000000..123a1af9b --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/resources/static/callback.html @@ -0,0 +1,61 @@ + + + + + SSO 授权后的回调页 + + + + + + + + +正在使用 code 授权码,进行 accessToken 访问令牌的获取 + + diff --git a/yudao-sso-demo-by-code/src/main/resources/static/index.html b/yudao-sso-demo-by-code/src/main/resources/static/index.html new file mode 100644 index 000000000..247b18f54 --- /dev/null +++ b/yudao-sso-demo-by-code/src/main/resources/static/index.html @@ -0,0 +1,159 @@ + + + + + 首页 + + + + + + + + + + + + + +