Commit 6ca88277d80a6f13ec69698bc3735bfe98538645

Authored by YunaiV
1 parent 7d1deab4

完成 oauth2 implicit 简化模式的实现

Showing 17 changed files with 208 additions and 35 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
@@ -3,8 +3,12 @@ package cn.iocoder.yudao.framework.common.util.http; @@ -3,8 +3,12 @@ package cn.iocoder.yudao.framework.common.util.http;
3 import cn.hutool.core.map.TableMap; 3 import cn.hutool.core.map.TableMap;
4 import cn.hutool.core.net.url.UrlBuilder; 4 import cn.hutool.core.net.url.UrlBuilder;
5 import cn.hutool.core.util.ReflectUtil; 5 import cn.hutool.core.util.ReflectUtil;
  6 +import org.springframework.web.util.UriComponents;
  7 +import org.springframework.web.util.UriComponentsBuilder;
6 8
  9 +import java.net.URI;
7 import java.nio.charset.Charset; 10 import java.nio.charset.Charset;
  11 +import java.util.Map;
8 12
9 /** 13 /**
10 * HTTP 工具类 14 * HTTP 工具类
@@ -25,4 +29,70 @@ public class HttpUtils { @@ -25,4 +29,70 @@ public class HttpUtils {
25 return builder.build(); 29 return builder.build();
26 } 30 }
27 31
  32 + private String append(String base, Map<String, ?> query, boolean fragment) {
  33 + return append(base, query, null, fragment);
  34 + }
  35 +
  36 + /**
  37 + * 拼接 URL
  38 + *
  39 + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法
  40 + *
  41 + * @param base 基础 URL
  42 + * @param query 查询参数
  43 + * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射
  44 + * @param fragment URL 的 fragment,即拼接到 # 中
  45 + * @return 拼接后的 URL
  46 + */
  47 + public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
  48 + UriComponentsBuilder template = UriComponentsBuilder.newInstance();
  49 + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
  50 + URI redirectUri;
  51 + try {
  52 + // assume it's encoded to start with (if it came in over the wire)
  53 + redirectUri = builder.build(true).toUri();
  54 + } catch (Exception e) {
  55 + // ... but allow client registrations to contain hard-coded non-encoded values
  56 + redirectUri = builder.build().toUri();
  57 + builder = UriComponentsBuilder.fromUri(redirectUri);
  58 + }
  59 + template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
  60 + .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
  61 +
  62 + if (fragment) {
  63 + StringBuilder values = new StringBuilder();
  64 + if (redirectUri.getFragment() != null) {
  65 + String append = redirectUri.getFragment();
  66 + values.append(append);
  67 + }
  68 + for (String key : query.keySet()) {
  69 + if (values.length() > 0) {
  70 + values.append("&");
  71 + }
  72 + String name = key;
  73 + if (keys != null && keys.containsKey(key)) {
  74 + name = keys.get(key);
  75 + }
  76 + values.append(name).append("={").append(key).append("}");
  77 + }
  78 + if (values.length() > 0) {
  79 + template.fragment(values.toString());
  80 + }
  81 + UriComponents encoded = template.build().expand(query).encode();
  82 + builder.fragment(encoded.getFragment());
  83 + } else {
  84 + for (String key : query.keySet()) {
  85 + String name = key;
  86 + if (keys != null && keys.containsKey(key)) {
  87 + name = keys.get(key);
  88 + }
  89 + template.queryParam(name, "{" + key + "}");
  90 + }
  91 + template.fragment(redirectUri.getFragment());
  92 + UriComponents encoded = template.build().expand(query).encode();
  93 + builder.query(encoded.getQuery());
  94 + }
  95 + return builder.build().toUriString();
  96 + }
  97 +
28 } 98 }
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
@@ -20,6 +20,8 @@ import java.util.Collections; @@ -20,6 +20,8 @@ import java.util.Collections;
20 */ 20 */
21 public class SecurityFrameworkUtils { 21 public class SecurityFrameworkUtils {
22 22
  23 + public static final String TOKEN_TYPE = "Bearer";
  24 +
23 private SecurityFrameworkUtils() {} 25 private SecurityFrameworkUtils() {}
24 26
25 /** 27 /**
@@ -34,7 +36,7 @@ public class SecurityFrameworkUtils { @@ -34,7 +36,7 @@ public class SecurityFrameworkUtils {
34 if (!StringUtils.hasText(authorization)) { 36 if (!StringUtils.hasText(authorization)) {
35 return null; 37 return null;
36 } 38 }
37 - int index = authorization.indexOf("Bearer "); 39 + int index = authorization.indexOf(TOKEN_TYPE + " ");
38 if (index == -1) { // 未找到 40 if (index == -1) { // 未找到
39 return null; 41 return null;
40 } 42 }
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCreateReqDTO.java
@@ -6,6 +6,7 @@ import lombok.Data; @@ -6,6 +6,7 @@ import lombok.Data;
6 6
7 import javax.validation.constraints.NotNull; 7 import javax.validation.constraints.NotNull;
8 import java.io.Serializable; 8 import java.io.Serializable;
  9 +import java.util.List;
9 10
10 /** 11 /**
11 * OAuth2.0 访问令牌创建 Request DTO 12 * OAuth2.0 访问令牌创建 Request DTO
@@ -31,5 +32,9 @@ public class OAuth2AccessTokenCreateReqDTO implements Serializable { @@ -31,5 +32,9 @@ public class OAuth2AccessTokenCreateReqDTO implements Serializable {
31 */ 32 */
32 @NotNull(message = "客户端编号不能为空") 33 @NotNull(message = "客户端编号不能为空")
33 private String clientId; 34 private String clientId;
  35 + /**
  36 + * 授权范围
  37 + */
  38 + private List<String> scopes;
34 39
35 } 40 }
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
@@ -129,6 +129,6 @@ public interface ErrorCodeConstants { @@ -129,6 +129,6 @@ public interface ErrorCodeConstants {
129 ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1002020002, "OAuth2 客户端已禁用"); 129 ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1002020002, "OAuth2 客户端已禁用");
130 ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1002020003, "不支持该授权类型"); 130 ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1002020003, "不支持该授权类型");
131 ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1002020004, "授权范围过大"); 131 ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1002020004, "授权范围过大");
132 - ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020004, "重定向地址不匹配"); 132 + ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020005, "重定向地址不匹配");
133 133
134 } 134 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/auth/OAuth2TokenApiImpl.java
@@ -24,7 +24,7 @@ public class OAuth2TokenApiImpl implements OAuth2TokenApi { @@ -24,7 +24,7 @@ public class OAuth2TokenApiImpl implements OAuth2TokenApi {
24 @Override 24 @Override
25 public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) { 25 public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) {
26 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken( 26 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(
27 - reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId()); 27 + reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId(), reqDTO.getScopes());
28 return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); 28 return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO);
29 } 29 }
30 30
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.http 0 → 100644
  1 +### 请求 /system/oauth2/authorize 接口 => 成功
  2 +POST {{baseUrl}}/system/oauth2/authorize
  3 +Content-Type: application/x-www-form-urlencoded
  4 +Authorization: Bearer {{token}}
  5 +tenant-id: {{adminTenentId}}
  6 +
  7 +response_type=token&client_id=default&scope={"user_info": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.java
1 package cn.iocoder.yudao.module.system.controller.admin.oauth2; 1 package cn.iocoder.yudao.module.system.controller.admin.oauth2;
2 2
  3 +import cn.hutool.core.lang.Assert;
3 import cn.hutool.core.util.ObjectUtil; 4 import cn.hutool.core.util.ObjectUtil;
4 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
5 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; 6 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
6 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 8 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; 9 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
  10 +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  11 +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
9 import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum; 12 import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum;
10 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ApproveService; 13 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ApproveService;
11 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService; 14 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService;
12 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2GrantService; 15 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2GrantService;
  16 +import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils;
13 import io.swagger.annotations.Api; 17 import io.swagger.annotations.Api;
14 import io.swagger.annotations.ApiImplicitParam; 18 import io.swagger.annotations.ApiImplicitParam;
15 import io.swagger.annotations.ApiImplicitParams; 19 import io.swagger.annotations.ApiImplicitParams;
@@ -26,6 +30,7 @@ import java.util.Map; @@ -26,6 +30,7 @@ import java.util.Map;
26 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; 30 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
27 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; 31 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
28 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 32 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  33 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
29 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; 34 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
30 35
31 @Api(tags = "管理后台 - OAuth2.0 授权") 36 @Api(tags = "管理后台 - OAuth2.0 授权")
@@ -72,16 +77,6 @@ public class OAuth2Controller { @@ -72,16 +77,6 @@ public class OAuth2Controller {
72 // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 77 // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内
73 oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes, redirectUri); 78 oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes, redirectUri);
74 79
75 - // 2. 判断是否满足自动授权(满足)  
76 - boolean approved = oauth2ApproveService.checkForPreApproval(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes);  
77 - if (approved) {  
78 - // 2.1 如果是 code 授权码模式,则发放 code 授权码,并重定向  
79 - if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) {  
80 - return success(getAuthorizationCodeRedirect());  
81 - }  
82 - return success(getImplicitGrantRedirect());  
83 - }  
84 -  
85 // 3. 不满足自动授权,则返回授权相关的展示信息 80 // 3. 不满足自动授权,则返回授权相关的展示信息
86 return null; 81 return null;
87 } 82 }
@@ -104,7 +99,7 @@ public class OAuth2Controller { @@ -104,7 +99,7 @@ public class OAuth2Controller {
104 @RequestParam("client_id") String clientId, 99 @RequestParam("client_id") String clientId,
105 @RequestParam(value = "scope", required = false) String scope, 100 @RequestParam(value = "scope", required = false) String scope,
106 @RequestParam("redirect_uri") String redirectUri, 101 @RequestParam("redirect_uri") String redirectUri,
107 - @RequestParam(value = "autoApprove") Boolean autoApprove, 102 + @RequestParam(value = "auto_approve") Boolean autoApprove,
108 @RequestParam(value = "state", required = false) String state) { 103 @RequestParam(value = "state", required = false) String state) {
109 @SuppressWarnings("unchecked") 104 @SuppressWarnings("unchecked")
110 Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class); 105 Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class);
@@ -115,27 +110,28 @@ public class OAuth2Controller { @@ -115,27 +110,28 @@ public class OAuth2Controller {
115 // 1.1 校验 responseType 是否满足 code 或者 token 值 110 // 1.1 校验 responseType 是否满足 code 或者 token 值
116 OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); 111 OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType);
117 // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 112 // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内
118 - oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); 113 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);
119 114
120 // 2.1 假设 approved 为 null,说明是场景一 115 // 2.1 假设 approved 为 null,说明是场景一
121 if (Boolean.TRUE.equals(autoApprove)) { 116 if (Boolean.TRUE.equals(autoApprove)) {
122 // 如果无法自动授权通过,则返回空 url,前端不进行跳转 117 // 如果无法自动授权通过,则返回空 url,前端不进行跳转
123 - if (!oauth2ApproveService.checkForPreApproval(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes.keySet())) { 118 + if (!oauth2ApproveService.checkForPreApproval(getLoginUserId(), getUserType(), clientId, scopes.keySet())) {
124 return success(null); 119 return success(null);
125 } 120 }
126 } else { // 2.2 假设 approved 非 null,说明是场景二 121 } else { // 2.2 假设 approved 非 null,说明是场景二
127 // 如果计算后不通过,则跳转一个错误链接 122 // 如果计算后不通过,则跳转一个错误链接
128 - if (!oauth2ApproveService.updateAfterApproval(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes)) { 123 + if (!oauth2ApproveService.updateAfterApproval(getLoginUserId(), getUserType(), clientId, scopes)) {
129 return success("TODO"); 124 return success("TODO");
130 } 125 }
131 } 126 }
132 127
133 // 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向 128 // 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向
  129 + List<String> approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue);
134 if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) { 130 if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) {
135 return success(getAuthorizationCodeRedirect()); 131 return success(getAuthorizationCodeRedirect());
136 } 132 }
137 // 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向 133 // 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向
138 - return success(getImplicitGrantRedirect()); 134 + return success(getImplicitGrantRedirect(getLoginUserId(), client, redirectUri, state, approveScopes));
139 } 135 }
140 136
141 private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) { 137 private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) {
@@ -148,12 +144,22 @@ public class OAuth2Controller { @@ -148,12 +144,22 @@ public class OAuth2Controller {
148 throw exception0(BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token"); 144 throw exception0(BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token");
149 } 145 }
150 146
151 - private String getImplicitGrantRedirect() {  
152 - return ""; 147 + private String getImplicitGrantRedirect(Long userId, OAuth2ClientDO client,
  148 + String redirectUri, String state, List<String> scopes) {
  149 + OAuth2AccessTokenDO accessTokenDO = oAuth2GrantService.grantImplicit(userId, getUserType(), client.getClientId(), scopes);
  150 + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查
  151 + // 拼接 URL
  152 + // noinspection unchecked
  153 + return OAuth2Utils.buildImplicitRedirectUri(redirectUri, accessTokenDO.getAccessToken(), state, accessTokenDO.getExpiresTime(),
  154 + scopes, JsonUtils.parseObject(client.getAdditionalInformation(), Map.class));
153 } 155 }
154 156
155 private String getAuthorizationCodeRedirect() { 157 private String getAuthorizationCodeRedirect() {
156 return ""; 158 return "";
157 } 159 }
158 160
  161 + private Integer getUserType() {
  162 + return UserTypeEnum.ADMIN.getValue();
  163 + }
  164 +
159 } 165 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2AccessTokenDO.java
@@ -3,13 +3,16 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth; @@ -3,13 +3,16 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth;
3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; 3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
4 import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; 4 import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
5 import com.baomidou.mybatisplus.annotation.KeySequence; 5 import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableField;
6 import com.baomidou.mybatisplus.annotation.TableId; 7 import com.baomidou.mybatisplus.annotation.TableId;
7 import com.baomidou.mybatisplus.annotation.TableName; 8 import com.baomidou.mybatisplus.annotation.TableName;
  9 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
8 import lombok.Data; 10 import lombok.Data;
9 import lombok.EqualsAndHashCode; 11 import lombok.EqualsAndHashCode;
10 import lombok.experimental.Accessors; 12 import lombok.experimental.Accessors;
11 13
12 import java.util.Date; 14 import java.util.Date;
  15 +import java.util.List;
13 16
14 /** 17 /**
15 * OAuth2 访问令牌 DO 18 * OAuth2 访问令牌 DO
@@ -56,6 +59,11 @@ public class OAuth2AccessTokenDO extends TenantBaseDO { @@ -56,6 +59,11 @@ public class OAuth2AccessTokenDO extends TenantBaseDO {
56 */ 59 */
57 private String clientId; 60 private String clientId;
58 /** 61 /**
  62 + * 授权范围
  63 + */
  64 + @TableField(typeHandler = JacksonTypeHandler.class)
  65 + private List<String> scopes;
  66 + /**
59 * 过期时间 67 * 过期时间
60 */ 68 */
61 private Date expiresTime; 69 private Date expiresTime;
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/auth/OAuth2RefreshTokenDO.java
@@ -3,12 +3,15 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth; @@ -3,12 +3,15 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth;
3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; 3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 import com.baomidou.mybatisplus.annotation.KeySequence; 5 import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableField;
6 import com.baomidou.mybatisplus.annotation.TableName; 7 import com.baomidou.mybatisplus.annotation.TableName;
  8 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
7 import lombok.Data; 9 import lombok.Data;
8 import lombok.EqualsAndHashCode; 10 import lombok.EqualsAndHashCode;
9 import lombok.experimental.Accessors; 11 import lombok.experimental.Accessors;
10 12
11 import java.util.Date; 13 import java.util.Date;
  14 +import java.util.List;
12 15
13 /** 16 /**
14 * OAuth2 刷新令牌 17 * OAuth2 刷新令牌
@@ -48,6 +51,11 @@ public class OAuth2RefreshTokenDO extends BaseDO { @@ -48,6 +51,11 @@ public class OAuth2RefreshTokenDO extends BaseDO {
48 */ 51 */
49 private String clientId; 52 private String clientId;
50 /** 53 /**
  54 + * 授权范围
  55 + */
  56 + @TableField(typeHandler = JacksonTypeHandler.class)
  57 + private List<String> scopes;
  58 + /**
51 * 过期时间 59 * 过期时间
52 */ 60 */
53 private Date expiresTime; 61 private Date expiresTime;
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
@@ -207,7 +207,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { @@ -207,7 +207,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
207 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); 207 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
208 // 创建访问令牌 208 // 创建访问令牌
209 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), 209 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
210 - OAuth2ClientConstants.CLIENT_ID_DEFAULT); 210 + OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
211 // 构建返回结果 211 // 构建返回结果
212 return AuthConvert.INSTANCE.convert(accessTokenDO); 212 return AuthConvert.INSTANCE.convert(accessTokenDO);
213 } 213 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java
1 package cn.iocoder.yudao.module.system.service.oauth2; 1 package cn.iocoder.yudao.module.system.service.oauth2;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjectUtil;
4 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 import cn.iocoder.yudao.framework.common.pojo.PageResult; 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -181,7 +182,7 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService { @@ -181,7 +182,7 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
181 if (client == null) { 182 if (client == null) {
182 throw exception(OAUTH2_CLIENT_EXISTS); 183 throw exception(OAUTH2_CLIENT_EXISTS);
183 } 184 }
184 - if (Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { 185 + if (ObjectUtil.notEqual(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
185 throw exception(OAUTH2_CLIENT_DISABLE); 186 throw exception(OAUTH2_CLIENT_DISABLE);
186 } 187 }
187 188
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.service.oauth2; @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.service.oauth2;
2 2
3 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; 3 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
4 4
5 -import java.util.Collection; 5 +import java.util.List;
6 6
7 /** 7 /**
8 * OAuth2 授予 Service 接口 8 * OAuth2 授予 Service 接口
@@ -20,11 +20,11 @@ public interface OAuth2GrantService { @@ -20,11 +20,11 @@ public interface OAuth2GrantService {
20 20
21 // ImplicitTokenGranter 21 // ImplicitTokenGranter
22 OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, 22 OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
23 - String clientId, Collection<String> scopes); 23 + String clientId, List<String> scopes);
24 24
25 // AuthorizationCodeTokenGranter 25 // AuthorizationCodeTokenGranter
26 String grantAuthorizationCode(Long userId, Integer userType, 26 String grantAuthorizationCode(Long userId, Integer userType,
27 - String clientId, Collection<String> scopes, 27 + String clientId, List<String> scopes,
28 String redirectUri, String state); 28 String redirectUri, String state);
29 29
30 } 30 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java
@@ -3,7 +3,9 @@ package cn.iocoder.yudao.module.system.service.oauth2; @@ -3,7 +3,9 @@ package cn.iocoder.yudao.module.system.service.oauth2;
3 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; 3 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
4 import org.springframework.stereotype.Service; 4 import org.springframework.stereotype.Service;
5 5
  6 +import javax.annotation.Resource;
6 import java.util.Collection; 7 import java.util.Collection;
  8 +import java.util.List;
7 9
8 /** 10 /**
9 * OAuth2 授予 Service 实现类 11 * OAuth2 授予 Service 实现类
@@ -13,15 +15,18 @@ import java.util.Collection; @@ -13,15 +15,18 @@ import java.util.Collection;
13 @Service 15 @Service
14 public class OAuth2GrantServiceImpl implements OAuth2GrantService { 16 public class OAuth2GrantServiceImpl implements OAuth2GrantService {
15 17
  18 + @Resource
  19 + private OAuth2TokenService oauth2TokenService;
  20 +
16 @Override 21 @Override
17 public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, 22 public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
18 - String clientId, Collection<String> scopes) {  
19 - return null; 23 + String clientId, List<String> scopes) {
  24 + return oauth2TokenService.createAccessToken(userId, userType, clientId, scopes);
20 } 25 }
21 26
22 @Override 27 @Override
23 public String grantAuthorizationCode(Long userId, Integer userType, 28 public String grantAuthorizationCode(Long userId, Integer userType,
24 - String clientId, Collection<String> scopes, 29 + String clientId, List<String> scopes,
25 String redirectUri, String state) { 30 String redirectUri, String state) {
26 return null; 31 return null;
27 } 32 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java
@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; 4 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO;
5 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; 5 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
6 6
  7 +import java.util.List;
  8 +
7 /** 9 /**
8 * OAuth2.0 Token Service 接口 10 * OAuth2.0 Token Service 接口
9 * 11 *
@@ -22,9 +24,10 @@ public interface OAuth2TokenService { @@ -22,9 +24,10 @@ public interface OAuth2TokenService {
22 * @param userId 用户编号 24 * @param userId 用户编号
23 * @param userType 用户类型 25 * @param userType 用户类型
24 * @param clientId 客户端编号 26 * @param clientId 客户端编号
  27 + * @param scopes 授权范围
25 * @return 访问令牌的信息 28 * @return 访问令牌的信息
26 */ 29 */
27 - OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId); 30 + OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes);
28 31
29 /** 32 /**
30 * 刷新访问令牌 33 * 刷新访问令牌
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java
@@ -45,10 +45,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @@ -45,10 +45,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
45 45
46 @Override 46 @Override
47 @Transactional 47 @Transactional
48 - public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId) { 48 + public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) {
49 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); 49 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
50 // 创建刷新令牌 50 // 创建刷新令牌
51 - OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO); 51 + OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes);
52 // 创建访问令牌 52 // 创建访问令牌
53 return createOAuth2AccessToken(refreshTokenDO, clientDO); 53 return createOAuth2AccessToken(refreshTokenDO, clientDO);
54 } 54 }
@@ -134,7 +134,8 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @@ -134,7 +134,8 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
134 134
135 private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { 135 private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
136 OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) 136 OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken())
137 - .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getClientId()) 137 + .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType())
  138 + .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
138 .setRefreshToken(refreshTokenDO.getRefreshToken()) 139 .setRefreshToken(refreshTokenDO.getRefreshToken())
139 .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds())); 140 .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds()));
140 accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号 141 accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号
@@ -144,9 +145,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @@ -144,9 +145,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
144 return accessTokenDO; 145 return accessTokenDO;
145 } 146 }
146 147
147 - private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) { 148 + private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List<String> scopes) {
148 OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) 149 OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
149 - .setUserId(userId).setUserType(userType).setClientId(clientDO.getClientId()) 150 + .setUserId(userId).setUserType(userType)
  151 + .setClientId(clientDO.getClientId()).setScopes(scopes)
150 .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds())); 152 .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds()));
151 oauth2RefreshTokenMapper.insert(refreshToken); 153 oauth2RefreshTokenMapper.insert(refreshToken);
152 return refreshToken; 154 return refreshToken;
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.util.oauth2;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
  5 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  6 +
  7 +import java.util.*;
  8 +
  9 +/**
  10 + * OAuth2 相关的工具类
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +public class OAuth2Utils {
  15 +
  16 + /**
  17 + * 构建简化模式下,重定向的 URI
  18 + *
  19 + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 appendAccessToken 方法
  20 + *
  21 + * @param redirectUri 重定向 URI
  22 + * @param accessToken 访问令牌
  23 + * @param state 状态
  24 + * @param expireTime 过期时间
  25 + * @param scopes 授权范围
  26 + * @param additionalInformation 附加信息
  27 + * @return 简化授权模式下的重定向 URI
  28 + */
  29 + public static String buildImplicitRedirectUri(String redirectUri, String accessToken, String state, Date expireTime,
  30 + Collection<String> scopes, Map<String, Object> additionalInformation) {
  31 + Map<String, Object> vars = new LinkedHashMap<String, Object>();
  32 + Map<String, String> keys = new HashMap<String, String>();
  33 + vars.put("access_token", accessToken);
  34 + vars.put("token_type", SecurityFrameworkUtils.TOKEN_TYPE.toLowerCase());
  35 + if (state != null) {
  36 + vars.put("state", state);
  37 + }
  38 + if (expireTime != null) {
  39 + long expires_in = (expireTime.getTime() - System.currentTimeMillis()) / 1000;
  40 + vars.put("expires_in", expires_in);
  41 + }
  42 + if (CollUtil.isNotEmpty(scopes)) {
  43 + vars.put("scope", CollUtil.join(scopes, " "));
  44 + }
  45 + for (String key : additionalInformation.keySet()) {
  46 + Object value = additionalInformation.get(key);
  47 + if (value != null) {
  48 + keys.put("extra_" + key, key);
  49 + vars.put("extra_" + key, value);
  50 + }
  51 + }
  52 + // Do not include the refresh token (even if there is one)
  53 + return HttpUtils.append(redirectUri, vars, keys, true);
  54 + }
  55 +
  56 +}
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java
@@ -201,7 +201,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { @@ -201,7 +201,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
201 // mock 缓存登录用户到 Redis 201 // mock 缓存登录用户到 Redis
202 OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) 202 OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L)
203 .setUserType(UserTypeEnum.ADMIN.getValue())); 203 .setUserType(UserTypeEnum.ADMIN.getValue()));
204 - when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"))) 204 + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull()))
205 .thenReturn(accessTokenDO); 205 .thenReturn(accessTokenDO);
206 206
207 // 调用, 并断言异常 207 // 调用, 并断言异常