Commit d18463866ef0ede008eb8de1208a3feedea5396d

Authored by YunaiV
1 parent ed847ac2

新增 sso 页面

Showing 13 changed files with 215 additions and 95 deletions
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http
  1 +### 请求 /system/oauth2/authorize 接口 => 成功
  2 +GET {{baseUrl}}/system/oauth2/authorize?clientId=default
  3 +Authorization: Bearer {{token}}
  4 +tenant-id: {{adminTenentId}}
  5 +
1 ### 请求 /system/oauth2/authorize + token 接口 => 成功 6 ### 请求 /system/oauth2/authorize + token 接口 => 成功
2 POST {{baseUrl}}/system/oauth2/authorize 7 POST {{baseUrl}}/system/oauth2/authorize
3 Content-Type: application/x-www-form-urlencoded 8 Content-Type: application/x-www-form-urlencoded
4 Authorization: Bearer {{token}} 9 Authorization: Bearer {{token}}
5 tenant-id: {{adminTenentId}} 10 tenant-id: {{adminTenentId}}
6 11
7 -response_type=token&client_id=default&scope={"user_info": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true 12 +response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true
8 13
9 ### 请求 /system/oauth2/authorize + code 接口 => 成功 14 ### 请求 /system/oauth2/authorize + code 接口 => 成功
10 POST {{baseUrl}}/system/oauth2/authorize 15 POST {{baseUrl}}/system/oauth2/authorize
@@ -12,7 +17,7 @@ Content-Type: application/x-www-form-urlencoded @@ -12,7 +17,7 @@ Content-Type: application/x-www-form-urlencoded
12 Authorization: Bearer {{token}} 17 Authorization: Bearer {{token}}
13 tenant-id: {{adminTenentId}} 18 tenant-id: {{adminTenentId}}
14 19
15 -response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true 20 +response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=false
16 21
17 ### 请求 /system/oauth2/token + code 接口 => 成功 22 ### 请求 /system/oauth2/token + code 接口 => 成功
18 POST {{baseUrl}}/system/oauth2/token 23 POST {{baseUrl}}/system/oauth2/token
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java
@@ -11,14 +11,16 @@ import cn.iocoder.yudao.framework.common.util.http.HttpUtils; @@ -11,14 +11,16 @@ import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
11 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 11 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
12 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; 12 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
13 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; 13 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
  14 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO;
14 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; 15 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
15 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserInfoRespVO; 16 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserInfoRespVO;
16 -import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; 17 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserUpdateReqVO;
17 import cn.iocoder.yudao.module.system.convert.oauth2.OAuth2OpenConvert; 18 import cn.iocoder.yudao.module.system.convert.oauth2.OAuth2OpenConvert;
18 -import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;  
19 -import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;  
20 import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; 19 import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
21 import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; 20 import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
  21 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
  22 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
  23 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
22 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; 24 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
23 import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum; 25 import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum;
24 import cn.iocoder.yudao.module.system.service.dept.DeptService; 26 import cn.iocoder.yudao.module.system.service.dept.DeptService;
@@ -91,7 +93,7 @@ public class OAuth2OpenController { @@ -91,7 +93,7 @@ public class OAuth2OpenController {
91 * 注意,默认需要传递 client_id + client_secret 参数 93 * 注意,默认需要传递 client_id + client_secret 参数
92 */ 94 */
93 @PostMapping("/token") 95 @PostMapping("/token")
94 - @ApiOperation(value = "获得访问令牌", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用") 96 + @ApiOperation(value = "获得访问令牌", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用")
95 @ApiImplicitParams({ 97 @ApiImplicitParams({
96 @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型", example = "code", dataTypeClass = String.class), 98 @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型", example = "code", dataTypeClass = String.class),
97 @ApiImplicitParam(name = "code", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), 99 @ApiImplicitParam(name = "code", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class),
@@ -173,7 +175,7 @@ public class OAuth2OpenController { @@ -173,7 +175,7 @@ public class OAuth2OpenController {
173 @RequestParam("token") String token) { 175 @RequestParam("token") String token) {
174 // 校验客户端 176 // 校验客户端
175 String[] clientIdAndSecret = obtainBasicAuthorization(request); 177 String[] clientIdAndSecret = obtainBasicAuthorization(request);
176 - OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], 178 + oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1],
177 null, null, null); 179 null, null, null);
178 180
179 // 校验令牌 181 // 校验令牌
@@ -182,46 +184,36 @@ public class OAuth2OpenController { @@ -182,46 +184,36 @@ public class OAuth2OpenController {
182 return success(OAuth2OpenConvert.INSTANCE.convert2(accessTokenDO)); 184 return success(OAuth2OpenConvert.INSTANCE.convert2(accessTokenDO));
183 } 185 }
184 186
185 - // GET oauth/authorize AuthorizationEndpoint TODO 187 + /**
  188 + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 authorize 方法
  189 + */
186 @GetMapping("/authorize") 190 @GetMapping("/authorize")
187 - @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用")  
188 - @ApiImplicitParams({  
189 - @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class),  
190 - @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class),  
191 - @ApiImplicitParam(name = "scope", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), // 多个使用空格分隔  
192 - @ApiImplicitParam(name = "redirect_uri", required = true, value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class),  
193 - @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class)  
194 - })  
195 - public CommonResult<Object> authorize(@RequestParam("response_type") String responseType,  
196 - @RequestParam("client_id") String clientId,  
197 - @RequestParam(value = "scope", required = false) String scope,  
198 - @RequestParam("redirect_uri") String redirectUri,  
199 - @RequestParam(value = "state", required = false) String state) {  
200 - List<String> scopes = StrUtil.split(scope, ' '); 191 + @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用")
  192 + @ApiImplicitParam(name = "clientId", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class)
  193 + public CommonResult<OAuth2OpenAuthorizeInfoRespVO> authorize(@RequestParam("clientId") String clientId) {
201 // 0. 校验用户已经登录。通过 Spring Security 实现 194 // 0. 校验用户已经登录。通过 Spring Security 实现
202 195
203 - // 1.1 校验 responseType 是否满足 code 或者 token 值  
204 - OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType);  
205 - // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内  
206 - oauth2ClientService.validOAuthClientFromCache(clientId, null,  
207 - grantTypeEnum.getGrantType(), scopes, redirectUri);  
208 -  
209 - // 3. 不满足自动授权,则返回授权相关的展示信息  
210 - return null; 196 + // 1. 获得 Client 客户端的信息
  197 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null,
  198 + null, null, null);
  199 + // 2. 获得用户已经授权的信息
  200 + List<OAuth2ApproveDO> approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId);
  201 + // 拼接返回
  202 + return success(OAuth2OpenConvert.INSTANCE.convert(client, approves));
211 } 203 }
212 204
213 /** 205 /**
214 * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法 206 * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法
215 * 207 *
216 * 场景一:【自动授权 autoApprove = true】 208 * 场景一:【自动授权 autoApprove = true】
217 - * 刚进入 authorize.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权 209 + * 刚进入 sso.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权
218 * 场景二:【手动授权 autoApprove = false】 210 * 场景二:【手动授权 autoApprove = false】
219 - * 在 authorize.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false 211 + * 在 sso.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false
220 * 212 *
221 * 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理 213 * 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理
222 */ 214 */
223 @PostMapping("/authorize") 215 @PostMapping("/authorize")
224 - @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【提交】调用") 216 + @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【提交】调用")
225 @ApiImplicitParams({ 217 @ApiImplicitParams({
226 @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class), 218 @ApiImplicitParam(name = "response_type", required = true, value = "响应类型", example = "code", dataTypeClass = String.class),
227 @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class), 219 @ApiImplicitParam(name = "client_id", required = true, value = "客户端编号", example = "tudou", dataTypeClass = String.class),
@@ -346,7 +338,7 @@ public class OAuth2OpenController { @@ -346,7 +338,7 @@ public class OAuth2OpenController {
346 @PutMapping("/user/update") 338 @PutMapping("/user/update")
347 @ApiOperation("更新用户基本信息") 339 @ApiOperation("更新用户基本信息")
348 @PreAuthorize("@ss.hasScope('user.write')") 340 @PreAuthorize("@ss.hasScope('user.write')")
349 - public CommonResult<Boolean> updateUserInfo(@Valid @RequestBody UserProfileUpdateReqVO reqVO) { 341 + public CommonResult<Boolean> updateUserInfo(@Valid @RequestBody OAuth2OpenUserUpdateReqVO reqVO) {
350 // 这里将 UserProfileUpdateReqVO =》UserProfileUpdateReqVO 对象,实现接口的复用。 342 // 这里将 UserProfileUpdateReqVO =》UserProfileUpdateReqVO 对象,实现接口的复用。
351 // 主要是,AdminUserService 没有自己的 BO 对象,所以复用只能这么做 343 // 主要是,AdminUserService 没有自己的 BO 对象,所以复用只能这么做
352 userService.updateUserProfile(getLoginUserId(), OAuth2OpenConvert.INSTANCE.convert(reqVO)); 344 userService.updateUserProfile(getLoginUserId(), OAuth2OpenConvert.INSTANCE.convert(reqVO));
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientBaseVO.java
@@ -18,39 +18,39 @@ import java.util.List; @@ -18,39 +18,39 @@ import java.util.List;
18 @Data 18 @Data
19 public class OAuth2ClientBaseVO { 19 public class OAuth2ClientBaseVO {
20 20
21 - @ApiModelProperty(value = "客户端编号", required = true) 21 + @ApiModelProperty(value = "客户端编号", required = true, example = "tudou")
22 @NotNull(message = "客户端编号不能为空") 22 @NotNull(message = "客户端编号不能为空")
23 private String clientId; 23 private String clientId;
24 24
25 - @ApiModelProperty(value = "客户端密钥", required = true) 25 + @ApiModelProperty(value = "客户端密钥", required = true, example = "fan")
26 @NotNull(message = "客户端密钥不能为空") 26 @NotNull(message = "客户端密钥不能为空")
27 private String secret; 27 private String secret;
28 28
29 - @ApiModelProperty(value = "应用名", required = true) 29 + @ApiModelProperty(value = "应用名", required = true, example = "土豆")
30 @NotNull(message = "应用名不能为空") 30 @NotNull(message = "应用名不能为空")
31 private String name; 31 private String name;
32 32
33 - @ApiModelProperty(value = "应用图标", required = true) 33 + @ApiModelProperty(value = "应用图标", required = true, example = "https://www.iocoder.cn/xx.png")
34 @NotNull(message = "应用图标不能为空") 34 @NotNull(message = "应用图标不能为空")
35 @URL(message = "应用图标的地址不正确") 35 @URL(message = "应用图标的地址不正确")
36 private String logo; 36 private String logo;
37 37
38 - @ApiModelProperty(value = "应用描述") 38 + @ApiModelProperty(value = "应用描述", example = "我是一个应用")
39 private String description; 39 private String description;
40 40
41 - @ApiModelProperty(value = "状态", required = true) 41 + @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举")
42 @NotNull(message = "状态不能为空") 42 @NotNull(message = "状态不能为空")
43 private Integer status; 43 private Integer status;
44 44
45 - @ApiModelProperty(value = "访问令牌的有效期", required = true) 45 + @ApiModelProperty(value = "访问令牌的有效期", required = true, example = "8640")
46 @NotNull(message = "访问令牌的有效期不能为空") 46 @NotNull(message = "访问令牌的有效期不能为空")
47 private Integer accessTokenValiditySeconds; 47 private Integer accessTokenValiditySeconds;
48 48
49 - @ApiModelProperty(value = "刷新令牌的有效期", required = true) 49 + @ApiModelProperty(value = "刷新令牌的有效期", required = true, example = "8640000")
50 @NotNull(message = "刷新令牌的有效期不能为空") 50 @NotNull(message = "刷新令牌的有效期不能为空")
51 private Integer refreshTokenValiditySeconds; 51 private Integer refreshTokenValiditySeconds;
52 52
53 - @ApiModelProperty(value = "可重定向的 URI 地址", required = true) 53 + @ApiModelProperty(value = "可重定向的 URI 地址", required = true, example = "https://www.iocoder.cn")
54 @NotNull(message = "可重定向的 URI 地址不能为空") 54 @NotNull(message = "可重定向的 URI 地址不能为空")
55 private List<@NotEmpty(message = "重定向的 URI 不能为空") 55 private List<@NotEmpty(message = "重定向的 URI 不能为空")
56 @URL(message = "重定向的 URI 格式不正确") String> redirectUris; 56 @URL(message = "重定向的 URI 格式不正确") String> redirectUris;
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java
@@ -10,10 +10,10 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam; @@ -10,10 +10,10 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
10 @ToString(callSuper = true) 10 @ToString(callSuper = true)
11 public class OAuth2ClientPageReqVO extends PageParam { 11 public class OAuth2ClientPageReqVO extends PageParam {
12 12
13 - @ApiModelProperty(value = "应用名") 13 + @ApiModelProperty(value = "应用名", example = "土豆", notes = "模糊匹配")
14 private String name; 14 private String name;
15 15
16 - @ApiModelProperty(value = "状态") 16 + @ApiModelProperty(value = "状态", example = "1", notes = "参见 CommonStatusEnum 枚举")
17 private Integer status; 17 private Integer status;
18 18
19 } 19 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java
@@ -14,7 +14,7 @@ import java.util.Date; @@ -14,7 +14,7 @@ import java.util.Date;
14 @ToString(callSuper = true) 14 @ToString(callSuper = true)
15 public class OAuth2ClientRespVO extends OAuth2ClientBaseVO { 15 public class OAuth2ClientRespVO extends OAuth2ClientBaseVO {
16 16
17 - @ApiModelProperty(value = "编号", required = true) 17 + @ApiModelProperty(value = "编号", required = true, example = "1024")
18 private Long id; 18 private Long id;
19 19
20 @ApiModelProperty(value = "创建时间", required = true) 20 @ApiModelProperty(value = "创建时间", required = true)
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/client/OAuth2ClientUpdateReqVO.java
@@ -14,7 +14,7 @@ import javax.validation.constraints.NotNull; @@ -14,7 +14,7 @@ import javax.validation.constraints.NotNull;
14 @ToString(callSuper = true) 14 @ToString(callSuper = true)
15 public class OAuth2ClientUpdateReqVO extends OAuth2ClientBaseVO { 15 public class OAuth2ClientUpdateReqVO extends OAuth2ClientBaseVO {
16 16
17 - @ApiModelProperty(value = "编号", required = true) 17 + @ApiModelProperty(value = "编号", required = true, example = "1024")
18 @NotNull(message = "编号不能为空") 18 @NotNull(message = "编号不能为空")
19 private Long id; 19 private Long id;
20 20
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
  4 +import io.swagger.annotations.ApiModel;
  5 +import io.swagger.annotations.ApiModelProperty;
  6 +import lombok.AllArgsConstructor;
  7 +import lombok.Data;
  8 +import lombok.NoArgsConstructor;
  9 +
  10 +import java.util.List;
  11 +
  12 +@ApiModel("管理后台 - 授权页的信息 Response VO")
  13 +@Data
  14 +@NoArgsConstructor
  15 +@AllArgsConstructor
  16 +public class OAuth2OpenAuthorizeInfoRespVO {
  17 +
  18 + /**
  19 + * 客户端
  20 + */
  21 + private Client client;
  22 +
  23 + @ApiModelProperty(value = "scope 的选中信息", required = true, notes = "使用 List 保证有序性,Key 是 scope,Value 为是否选中")
  24 + private List<KeyValue<String, Boolean>> scopes;
  25 +
  26 + @Data
  27 + @NoArgsConstructor
  28 + @AllArgsConstructor
  29 + public static class Client {
  30 +
  31 + @ApiModelProperty(value = "应用名", required = true, example = "土豆")
  32 + private String name;
  33 +
  34 + @ApiModelProperty(value = "应用图标", required = true, example = "https://www.iocoder.cn/xx.png")
  35 + private String logo;
  36 +
  37 + }
  38 +
  39 +}
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java
1 package cn.iocoder.yudao.module.system.convert.oauth2; 1 package cn.iocoder.yudao.module.system.convert.oauth2;
2 2
  3 +import cn.iocoder.yudao.framework.common.core.KeyValue;
3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; 4 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  5 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
4 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; 6 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
5 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; 7 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
  8 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO;
6 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; 9 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
7 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserInfoRespVO; 10 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserInfoRespVO;
  11 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserUpdateReqVO;
8 import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; 12 import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
9 -import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;  
10 import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; 13 import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
11 import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; 14 import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
  15 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
  16 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
  17 +import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
12 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; 18 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
13 import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils; 19 import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils;
14 import org.mapstruct.Mapper; 20 import org.mapstruct.Mapper;
15 import org.mapstruct.factory.Mappers; 21 import org.mapstruct.factory.Mappers;
16 22
  23 +import java.util.ArrayList;
17 import java.util.List; 24 import java.util.List;
  25 +import java.util.Map;
18 26
19 @Mapper 27 @Mapper
20 public interface OAuth2OpenConvert { 28 public interface OAuth2OpenConvert {
@@ -44,6 +52,19 @@ public interface OAuth2OpenConvert { @@ -44,6 +52,19 @@ public interface OAuth2OpenConvert {
44 OAuth2OpenUserInfoRespVO.Dept convert(DeptDO dept); 52 OAuth2OpenUserInfoRespVO.Dept convert(DeptDO dept);
45 List<OAuth2OpenUserInfoRespVO.Post> convertList(List<PostDO> list); 53 List<OAuth2OpenUserInfoRespVO.Post> convertList(List<PostDO> list);
46 54
47 - UserProfileUpdateReqVO convert(UserProfileUpdateReqVO bean); 55 + UserProfileUpdateReqVO convert(OAuth2OpenUserUpdateReqVO bean);
  56 +
  57 + default OAuth2OpenAuthorizeInfoRespVO convert(OAuth2ClientDO client, List<OAuth2ApproveDO> approves) {
  58 + // 构建 scopes
  59 + List<KeyValue<String, Boolean>> scopes = new ArrayList<>(client.getScopes().size());
  60 + Map<String, OAuth2ApproveDO> approveMap = CollectionUtils.convertMap(approves, OAuth2ApproveDO::getScope);
  61 + client.getScopes().forEach(scope -> {
  62 + OAuth2ApproveDO approve = approveMap.get(scope);
  63 + scopes.add(new KeyValue<>(scope, approve != null ? approve.getApproved() : false));
  64 + });
  65 + // 拼接返回
  66 + return new OAuth2OpenAuthorizeInfoRespVO(
  67 + new OAuth2OpenAuthorizeInfoRespVO.Client(client.getName(), client.getLogo()), scopes);
  68 + }
48 69
49 } 70 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java
@@ -13,7 +13,7 @@ import java.util.Date; @@ -13,7 +13,7 @@ import java.util.Date;
13 /** 13 /**
14 * OAuth2 批准 DO 14 * OAuth2 批准 DO
15 * 15 *
16 - * 用户在 authorize.vue 界面时,记录接受的 scope 列表 16 + * 用户在 sso.vue 界面时,记录接受的 scope 列表
17 * 17 *
18 * @author 芋道源码 18 * @author 芋道源码
19 */ 19 */
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java
@@ -181,7 +181,7 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService { @@ -181,7 +181,7 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
181 // 校验客户端存在、且开启 181 // 校验客户端存在、且开启
182 OAuth2ClientDO client = clientCache.get(clientId); 182 OAuth2ClientDO client = clientCache.get(clientId);
183 if (client == null) { 183 if (client == null) {
184 - throw exception(OAUTH2_CLIENT_EXISTS); 184 + throw exception(OAUTH2_CLIENT_NOT_EXISTS);
185 } 185 }
186 if (ObjectUtil.notEqual(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { 186 if (ObjectUtil.notEqual(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
187 throw exception(OAUTH2_CLIENT_DISABLE); 187 throw exception(OAUTH2_CLIENT_DISABLE);
yudao-ui-admin/src/api/login.js
@@ -111,25 +111,37 @@ export function refreshToken() { @@ -111,25 +111,37 @@ export function refreshToken() {
111 } 111 }
112 112
113 // ========== OAUTH 2.0 相关 ========== 113 // ========== OAUTH 2.0 相关 ==========
114 -export function authorize() { 114 +
  115 +export function getAuthorize(clientId) {
  116 + return request({
  117 + url: '/system/oauth2/authorize?clientId=' + clientId,
  118 + method: 'get'
  119 + })
  120 +}
  121 +
  122 +export function authorize(responseType, clientId, redirectUri, state,
  123 + autoApprove, checkedScopes, uncheckedScopes) {
  124 + // 构建 scopes
  125 + const scopes = {};
  126 + for (const scope of checkedScopes) {
  127 + scopes[scope] = true;
  128 + }
  129 + for (const scope of uncheckedScopes) {
  130 + scopes[scope] = false;
  131 + }
  132 + // 发起请求
115 return service({ 133 return service({
116 url: '/system/oauth2/authorize', 134 url: '/system/oauth2/authorize',
117 headers:{ 135 headers:{
118 'Content-type': 'application/x-www-form-urlencoded', 136 'Content-type': 'application/x-www-form-urlencoded',
119 - "Access-Control-Allow-Origin": "*"  
120 }, 137 },
121 params: { 138 params: {
122 - response_type: 'code',  
123 - client_id: 'test',  
124 - redirect_uri: 'https://www.iocoder.cn',  
125 - // scopes: {  
126 - // read: true,  
127 - // write: false  
128 - // }  
129 - scope: {  
130 - read: true,  
131 - write: false  
132 - } 139 + response_type: responseType,
  140 + client_id: clientId,
  141 + redirect_uri: redirectUri,
  142 + state: state,
  143 + auto_approve: autoApprove,
  144 + scope: JSON.stringify(scopes)
133 }, 145 },
134 method: 'post' 146 method: 'post'
135 }) 147 })
yudao-ui-admin/src/router/index.js
@@ -43,8 +43,8 @@ export const constantRoutes = [ @@ -43,8 +43,8 @@ export const constantRoutes = [
43 hidden: true 43 hidden: true
44 }, 44 },
45 { 45 {
46 - path: '/authorize',  
47 - component: (resolve) => require(['@/views/authorize'], resolve), 46 + path: '/sso',
  47 + component: (resolve) => require(['@/views/sso'], resolve),
48 hidden: true 48 hidden: true
49 }, 49 },
50 { 50 {
yudao-ui-admin/src/views/authorize.vue renamed to yudao-ui-admin/src/views/sso.vue
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 14
15 <!-- 表单 --> 15 <!-- 表单 -->
16 <div class="form-cont"> 16 <div class="form-cont">
17 - <el-tabs class="form" style=" float:none;"> 17 + <el-tabs class="form" style=" float:none;" value="uname">
18 <el-tab-pane label="三方授权" name="uname"> 18 <el-tab-pane label="三方授权" name="uname">
19 </el-tab-pane> 19 </el-tab-pane>
20 </el-tabs> 20 </el-tabs>
@@ -53,7 +53,7 @@ @@ -53,7 +53,7 @@
53 <span v-if="!loading">同意授权</span> 53 <span v-if="!loading">同意授权</span>
54 <span v-else>登 录 中...</span> 54 <span v-else>登 录 中...</span>
55 </el-button> 55 </el-button>
56 - <el-button size="medium" style="width:37%">拒绝</el-button> 56 + <el-button size="medium" style="width:36%">拒绝</el-button>
57 </el-form-item> 57 </el-form-item>
58 </el-form> 58 </el-form>
59 </div> 59 </div>
@@ -69,10 +69,9 @@ @@ -69,10 +69,9 @@
69 69
70 <script> 70 <script>
71 import {getTenantIdByName} from "@/api/system/tenant"; 71 import {getTenantIdByName} from "@/api/system/tenant";
72 -import Cookies from "js-cookie";  
73 -import {SystemUserSocialTypeEnum} from "@/utils/constants";  
74 import {getTenantEnable} from "@/utils/ruoyi"; 72 import {getTenantEnable} from "@/utils/ruoyi";
75 -import {authorize} from "@/api/login"; 73 +import {authorize, getAuthorize} from "@/api/login";
  74 +import {getTenantName, setTenantId} from "@/utils/auth";
76 75
77 export default { 76 export default {
78 name: "Login", 77 name: "Login",
@@ -82,6 +81,18 @@ export default { @@ -82,6 +81,18 @@ export default {
82 loginForm: { 81 loginForm: {
83 tenantName: "芋道源码", 82 tenantName: "芋道源码",
84 }, 83 },
  84 + params: { // URL 上的 client_id、scope 等参数
  85 + responseType: undefined,
  86 + clientId: undefined,
  87 + redirectUri: undefined,
  88 + state: undefined,
  89 + scopes: [], // 优先从 query 参数获取;如果未传递,从后端获取
  90 + },
  91 + client: { // 客户端信息
  92 + name: '',
  93 + logo: '',
  94 + },
  95 + checkedScopes: [], // 已选中的 scope 数组
85 LoginRules: { 96 LoginRules: {
86 tenantName: [ 97 tenantName: [
87 {required: true, trigger: "blur", message: "租户不能为空"}, 98 {required: true, trigger: "blur", message: "租户不能为空"},
@@ -92,7 +103,7 @@ export default { @@ -92,7 +103,7 @@ export default {
92 const tenantId = res.data; 103 const tenantId = res.data;
93 if (tenantId && tenantId >= 0) { 104 if (tenantId && tenantId >= 0) {
94 // 设置租户 105 // 设置租户
95 - Cookies.set("tenantId", tenantId); 106 + setTenantId(tenantId)
96 callback(); 107 callback();
97 } else { 108 } else {
98 callback('租户不存在'); 109 callback('租户不存在');
@@ -104,44 +115,84 @@ export default { @@ -104,44 +115,84 @@ export default {
104 ] 115 ]
105 }, 116 },
106 loading: false, 117 loading: false,
107 - redirect: undefined,  
108 - // 枚举  
109 - SysUserSocialTypeEnum: SystemUserSocialTypeEnum, 118 + //
110 }; 119 };
111 }, 120 },
112 created() { 121 created() {
113 // 租户开关 122 // 租户开关
114 this.tenantEnable = getTenantEnable(); 123 this.tenantEnable = getTenantEnable();
115 - // 重定向地址  
116 - this.redirect = this.$route.query.redirect;  
117 this.getCookie(); 124 this.getCookie();
  125 +
  126 + // 解析参数
  127 + // 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
  128 + // 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
  129 + this.params.responseType = this.$route.query.response_type
  130 + this.params.clientId = this.$route.query.client_id
  131 + this.params.redirectUri = this.$route.query.redirect_uri
  132 + this.params.state = this.$route.query.state
  133 + if (this.$route.query.scope) {
  134 + this.params.scopes = this.$route.query.scope.split(' ')
  135 + }
  136 +
  137 + // 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
  138 + if (this.params.scopes.length > 0) {
  139 + this.doAuthorize(true, this.params.scopes, []).then(res => {
  140 + const href = res.data
  141 + if (!href) {
  142 + console.log('自动授权未通过!')
  143 + return;
  144 + }
  145 + location.href = href
  146 + })
  147 + }
  148 +
  149 + // 获取授权页的基本信息
  150 + getAuthorize(this.params.clientId).then(res => {
  151 + this.client = res.data.client
  152 + // 解析 scope
  153 + let scopes
  154 + // 1.1 如果 params.scope 非空,则过滤下返回的 scopes
  155 + if (this.params.scopes.length > 0) {
  156 + scopes = []
  157 + for (const scope of res.data.scopes) {
  158 + if (this.params.scopes.indexOf(scope.key) >= 0) {
  159 + scopes.push(scope)
  160 + }
  161 + }
  162 + // 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它
  163 + } else {
  164 + scopes = res.data.scopes
  165 + for (const scope of scopes) {
  166 + this.params.scopes.push(scope.key)
  167 + }
  168 + }
  169 + // 生成已选中的 checkedScopes
  170 + for (const scope of scopes) {
  171 + if (scope.value) {
  172 + this.checkedScopes.push(scope.key)
  173 + }
  174 + }
  175 + })
118 }, 176 },
119 methods: { 177 methods: {
120 getCookie() { 178 getCookie() {
121 - const tenantName = Cookies.get('tenantName'); 179 + const tenantName = getTenantName();
122 this.loginForm = { 180 this.loginForm = {
123 - tenantName: tenantName === undefined ? this.loginForm.tenantName : tenantName 181 + tenantName: tenantName ? tenantName : this.loginForm.tenantName,
124 }; 182 };
125 }, 183 },
126 handleLogin() { 184 handleLogin() {
127 - if (true) {  
128 - authorize()  
129 - return;  
130 - }  
131 this.$refs.loginForm.validate(valid => { 185 this.$refs.loginForm.validate(valid => {
132 - if (valid) {  
133 - this.loading = true;  
134 - // 发起登陆  
135 - console.log("发起登录", this.loginForm);  
136 - this.$store.dispatch(this.loginForm.loginType === "sms" ? "SmsLogin" : "Login", this.loginForm).then(() => {  
137 - this.$router.push({path: this.redirect || "/"}).catch(() => {  
138 - });  
139 - }).catch(() => {  
140 - this.loading = false;  
141 - this.getCode();  
142 - }); 186 + if (!valid) {
  187 + return
143 } 188 }
144 - }); 189 +
  190 +
  191 + })
  192 + },
  193 + doAuthorize(autoApprove, checkedScopes, uncheckedScopes) {
  194 + return authorize(this.params.responseType, this.params.clientId, this.params.redirectUri, this.params.state,
  195 + autoApprove, checkedScopes, uncheckedScopes)
145 } 196 }
146 } 197 }
147 }; 198 };