Commit 99ba7ccee8a59b52614ee01649c535272b8ff734

Authored by YunaiV
1 parent 66034d26

完成 oauth2 code 授权码模式的实现

Showing 14 changed files with 267 additions and 31 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
1 1 package cn.iocoder.yudao.framework.common.util.http;
2 2  
  3 +import cn.hutool.core.codec.Base64;
3 4 import cn.hutool.core.map.TableMap;
4 5 import cn.hutool.core.net.url.UrlBuilder;
5 6 import cn.hutool.core.util.ReflectUtil;
  7 +import cn.hutool.core.util.StrUtil;
  8 +import org.springframework.util.StringUtils;
6 9 import org.springframework.web.util.UriComponents;
7 10 import org.springframework.web.util.UriComponentsBuilder;
8 11  
  12 +import javax.servlet.http.HttpServletRequest;
9 13 import java.net.URI;
10 14 import java.nio.charset.Charset;
11 15 import java.util.Map;
... ... @@ -95,4 +99,28 @@ public class HttpUtils {
95 99 return builder.build().toUriString();
96 100 }
97 101  
  102 + public static String[] obtainBasicAuthorization(HttpServletRequest request) {
  103 + String clientId;
  104 + String clientSecret;
  105 + // 先从 Header 中获取
  106 + String authorization = request.getHeader("Authorization");
  107 + authorization = StrUtil.subAfter(authorization, "Basic ", true);
  108 + if (StringUtils.hasText(authorization)) {
  109 + authorization = Base64.decodeStr(authorization);
  110 + clientId = StrUtil.subBefore(authorization, ":", false);
  111 + clientSecret = StrUtil.subAfter(authorization, ":", false);
  112 + // 再从 Param 中获取
  113 + } else {
  114 + clientId = request.getParameter("client_id");
  115 + clientSecret = request.getParameter("client_secret");
  116 + }
  117 +
  118 + // 如果两者非空,则返回
  119 + if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
  120 + return new String[]{clientId, clientSecret};
  121 + }
  122 + return null;
  123 + }
  124 +
  125 +
98 126 }
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
... ... @@ -20,7 +20,7 @@ import java.util.Collections;
20 20 */
21 21 public class SecurityFrameworkUtils {
22 22  
23   - public static final String TOKEN_TYPE = "Bearer";
  23 + public static final String AUTHORIZATION_BEARER = "Bearer";
24 24  
25 25 private SecurityFrameworkUtils() {}
26 26  
... ... @@ -36,7 +36,7 @@ public class SecurityFrameworkUtils {
36 36 if (!StringUtils.hasText(authorization)) {
37 37 return null;
38 38 }
39   - int index = authorization.indexOf(TOKEN_TYPE + " ");
  39 + int index = authorization.indexOf(AUTHORIZATION_BEARER + " ");
40 40 if (index == -1) { // 未找到
41 41 return null;
42 42 }
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
... ... @@ -123,12 +123,19 @@ public interface ErrorCodeConstants {
123 123 ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1002019000, "系统敏感词在所有标签中都不存在");
124 124 ErrorCode SENSITIVE_WORD_EXISTS = new ErrorCode(1002019001, "系统敏感词已在标签中存在");
125 125  
126   - // ========== 系统敏感词 1002020000 =========
  126 + // ========== OAuth2 客户端 1002020000 =========
127 127 ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在");
128 128 ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1002020001, "OAuth2 客户端编号已存在");
129 129 ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1002020002, "OAuth2 客户端已禁用");
130 130 ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1002020003, "不支持该授权类型");
131 131 ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1002020004, "授权范围过大");
132   - ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020005, "重定向地址不匹配");
  132 + ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020005, "无效 redirect_uri: {}");
  133 + ErrorCode OAUTH2_CLIENT_CLIENT_SECRET_ERROR = new ErrorCode(1002020006, "无效 client_secret: {}");
  134 +
  135 + // ========== OAuth2 授权 1002021000 =========
  136 + ErrorCode OAUTH2_GRANT_CLIENT_ID_MISMATCH = new ErrorCode(1002020000, "client_id 不匹配");
  137 + ErrorCode OAUTH2_GRANT_REDIRECT_URI_MISMATCH = new ErrorCode(1002020001, "redirect_uri 不匹配");
  138 + ErrorCode OAUTH2_GRANT_STATE_MISMATCH = new ErrorCode(1002020002, "state 不匹配");
  139 + ErrorCode OAUTH2_GRANT_CODE_NOT_EXISTS = new ErrorCode(1002020003, "code 不存在");
133 140  
134 141 }
... ...
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2GrantTypeEnum.java
1 1 package cn.iocoder.yudao.module.system.enums.auth;
2 2  
  3 +import cn.hutool.core.util.ArrayUtil;
3 4 import lombok.AllArgsConstructor;
4 5 import lombok.Getter;
5 6  
... ... @@ -21,4 +22,8 @@ public enum OAuth2GrantTypeEnum {
21 22  
22 23 private final String grantType;
23 24  
  25 + public static OAuth2GrantTypeEnum getByGranType(String grantType) {
  26 + return ArrayUtil.firstMatch(o -> o.getGrantType().equals(grantType), values());
  27 + }
  28 +
24 29 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.http
... ... @@ -13,3 +13,11 @@ Authorization: Bearer {{token}}
13 13 tenant-id: {{adminTenentId}}
14 14  
15 15 response_type=code&client_id=default&scope={"user_info": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true
  16 +
  17 +### 请求 /system/oauth2/token + code 接口 => 成功
  18 +POST {{baseUrl}}/system/oauth2/token
  19 +Content-Type: application/x-www-form-urlencoded
  20 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
  21 +tenant-id: {{adminTenentId}}
  22 +
  23 +grant_type=authorization_code&redirect_uri=https://www.iocoder.cn
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.java renamed to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java
1 1 package cn.iocoder.yudao.module.system.controller.admin.oauth2;
2 2  
3 3 import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ArrayUtil;
4 5 import cn.hutool.core.util.ObjectUtil;
5 6 import cn.hutool.core.util.StrUtil;
6 7 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
7 8 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  9 +import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
8 10 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
9 11 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
  12 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
  13 +import cn.iocoder.yudao.module.system.convert.oauth2.OAuth2OpenConvert;
10 14 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
11 15 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
12 16 import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum;
... ... @@ -23,6 +27,7 @@ import org.springframework.validation.annotation.Validated;
23 27 import org.springframework.web.bind.annotation.*;
24 28  
25 29 import javax.annotation.Resource;
  30 +import javax.servlet.http.HttpServletRequest;
26 31 import java.util.Collections;
27 32 import java.util.List;
28 33 import java.util.Map;
... ... @@ -33,21 +38,17 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
33 38 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
34 39 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
35 40  
36   -@Api(tags = "管理后台 - OAuth2.0 授权")
  41 +@Api(tags = "管理后台 - OAuth2.0 授权") // 提供给外部应用调用为主
37 42 @RestController
38 43 @RequestMapping("/system/oauth2")
39 44 @Validated
40 45 @Slf4j
41   -public class OAuth2Controller {
42   -
43   -// POST oauth/token TokenEndpoint:Password、Implicit、Code、Refresh Token
  46 +public class OAuth2OpenController {
44 47  
45 48 // POST oauth/check_token CheckTokenEndpoint
46 49  
47 50 // DELETE oauth/token ConsumerTokenServices#revokeToken
48 51  
49   -// GET oauth/authorize AuthorizationEndpoint
50   -
51 52 @Resource
52 53 private OAuth2GrantService oauth2GrantService;
53 54 @Resource
... ... @@ -55,6 +56,56 @@ public class OAuth2Controller {
55 56 @Resource
56 57 private OAuth2ApproveService oauth2ApproveService;
57 58  
  59 + @PostMapping("/token")
  60 + @ApiOperation(value = "获得访问令牌", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用")
  61 + @ApiImplicitParams({
  62 + @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型", example = "code", dataTypeClass = String.class),
  63 + @ApiImplicitParam(name = "code", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class),
  64 + @ApiImplicitParam(name = "redirect_uri", value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class),
  65 + @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class)
  66 + })
  67 + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
  68 + public CommonResult<OAuth2OpenAccessTokenRespVO> postAccessToken(HttpServletRequest request,
  69 + @RequestParam("grant_type") String grantType,
  70 + @RequestParam(value = "code", required = false) String code, // 授权码模式
  71 + @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式
  72 + @RequestParam(value = "state", required = false) String state) { // 授权码模式
  73 + // 授权类型
  74 + OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType);
  75 + if (grantTypeEnum == null) {
  76 + throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType));
  77 + }
  78 + if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) {
  79 + throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式");
  80 + }
  81 +
  82 + // 校验客户端
  83 + String[] clientIdAndSecret = HttpUtils.obtainBasicAuthorization(request);
  84 + if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) {
  85 + throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递");
  86 + }
  87 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], grantType, null, null);
  88 +
  89 + // 根据授权模式,获取访问令牌
  90 + OAuth2AccessTokenDO accessTokenDO = null;
  91 + switch (grantTypeEnum) {
  92 + case AUTHORIZATION_CODE:
  93 + accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state);
  94 + break;
  95 + case PASSWORD:
  96 + break;
  97 + case CLIENT_CREDENTIALS:
  98 + break;
  99 + case REFRESH_TOKEN:
  100 + break;
  101 + default:
  102 + throw new IllegalArgumentException("未知授权类型:" + grantType);
  103 + }
  104 + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查
  105 + return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO));
  106 + }
  107 +
  108 + // GET oauth/authorize AuthorizationEndpoint TODO
58 109 @GetMapping("/authorize")
59 110 @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用")
60 111 @ApiImplicitParams({
... ... @@ -75,12 +126,23 @@ public class OAuth2Controller {
75 126 // 1.1 校验 responseType 是否满足 code 或者 token 值
76 127 OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType);
77 128 // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内
78   - oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes, redirectUri);
  129 + oauth2ClientService.validOAuthClientFromCache(clientId, null,
  130 + grantTypeEnum.getGrantType(), scopes, redirectUri);
79 131  
80 132 // 3. 不满足自动授权,则返回授权相关的展示信息
81 133 return null;
82 134 }
83 135  
  136 + /**
  137 + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法
  138 + *
  139 + * 场景一:【自动授权 autoApprove = true】
  140 + * 刚进入 authorize.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权
  141 + * 场景二:【手动授权 autoApprove = false】
  142 + * 在 authorize.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false
  143 + *
  144 + * 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理
  145 + */
84 146 @PostMapping("/authorize")
85 147 @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【提交】调用")
86 148 @ApiImplicitParams({
... ... @@ -92,9 +154,6 @@ public class OAuth2Controller {
92 154 @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class)
93 155 })
94 156 @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
95   - // 场景一:【自动授权 autoApprove = true】刚进入 authorize.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权
96   - // 场景二:【手动授权 autoApprove = false】在 authorize.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false
97   - // 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理
98 157 public CommonResult<String> approveOrDeny(@RequestParam("response_type") String responseType,
99 158 @RequestParam("client_id") String clientId,
100 159 @RequestParam(value = "scope", required = false) String scope,
... ... @@ -110,7 +169,8 @@ public class OAuth2Controller {
110 169 // 1.1 校验 responseType 是否满足 code 或者 token 值
111 170 OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType);
112 171 // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内
113   - OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);
  172 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null,
  173 + grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);
114 174  
115 175 // 2.1 假设 approved 为 null,说明是场景一
116 176 if (Boolean.TRUE.equals(autoApprove)) {
... ... @@ -159,7 +219,7 @@ public class OAuth2Controller {
159 219 private String getAuthorizationCodeRedirect(Long userId, OAuth2ClientDO client,
160 220 List<String> scopes, String redirectUri, String state) {
161 221 // 1. 创建 code 授权码
162   - String authorizationCode = oauth2GrantService.grantAuthorizationCode(userId,getUserType(), client.getClientId(), scopes,
  222 + String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(userId,getUserType(), client.getClientId(), scopes,
163 223 redirectUri, state);
164 224 // 2. 拼接重定向的 URL
165 225 return OAuth2Utils.buildAuthorizationCodeRedirectUri(redirectUri, authorizationCode, state);
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  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 +@ApiModel("管理后台 - 访问令牌 Response VO")
  11 +@Data
  12 +@NoArgsConstructor
  13 +@AllArgsConstructor
  14 +public class OAuth2OpenAccessTokenRespVO {
  15 +
  16 + @ApiModelProperty(value = "访问令牌", required = true, example = "tudou")
  17 + @JsonProperty("access_token")
  18 + private String accessToken;
  19 +
  20 + @ApiModelProperty(value = "刷新令牌", required = true, example = "nice")
  21 + @JsonProperty("refresh_token")
  22 + private String refreshToken;
  23 +
  24 + @ApiModelProperty(value = "令牌类型", required = true, example = "bearer")
  25 + @JsonProperty("token_type")
  26 + private String tokenType;
  27 +
  28 + @ApiModelProperty(value = "过期时间", required = true, example = "42430", notes = "单位:秒")
  29 + @JsonProperty("expires_in")
  30 + private Long expiresIn;
  31 +
  32 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java 0 → 100644
  1 +package cn.iocoder.yudao.module.system.convert.oauth2;
  2 +
  3 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  4 +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
  5 +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  6 +import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils;
  7 +import org.mapstruct.Mapper;
  8 +import org.mapstruct.factory.Mappers;
  9 +
  10 +@Mapper
  11 +public interface OAuth2OpenConvert {
  12 +
  13 + OAuth2OpenConvert INSTANCE = Mappers.getMapper(OAuth2OpenConvert.class);
  14 +
  15 + default OAuth2OpenAccessTokenRespVO convert(OAuth2AccessTokenDO bean) {
  16 + OAuth2OpenAccessTokenRespVO respVO = convert0(bean);
  17 + respVO.setTokenType(SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase());
  18 + respVO.setExpiresIn(OAuth2Utils.getExpiresIn(bean.getExpiresTime()));
  19 + return respVO;
  20 + }
  21 +
  22 + OAuth2OpenAccessTokenRespVO convert0(OAuth2AccessTokenDO bean);
  23 +
  24 +}
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java
... ... @@ -35,6 +35,8 @@ public class SecurityConfiguration {
35 35 registry.antMatchers(buildAdminApi("/system/tenant/get-id-by-name")).permitAll();
36 36 // 短信回调 API
37 37 registry.antMatchers(buildAdminApi("/system/sms/callback/**")).permitAll();
  38 + // OAuth2 API
  39 + registry.antMatchers(buildAdminApi("/system/oauth2/token")).permitAll();
38 40 }
39 41  
40 42 };
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientService.java
... ... @@ -67,7 +67,7 @@ public interface OAuth2ClientService {
67 67 * @return 客户端
68 68 */
69 69 default OAuth2ClientDO validOAuthClientFromCache(String clientId) {
70   - return validOAuthClientFromCache(clientId, null, null, null);
  70 + return validOAuthClientFromCache(clientId, null, null, null, null);
71 71 }
72 72  
73 73 /**
... ... @@ -76,12 +76,13 @@ public interface OAuth2ClientService {
76 76 * 非空时,进行校验
77 77 *
78 78 * @param clientId 客户端编号
  79 + * @param clientSecret 客户端密钥
79 80 * @param authorizedGrantType 授权方式
80 81 * @param scopes 授权范围
81 82 * @param redirectUri 重定向地址
82 83 * @return 客户端
83 84 */
84   - OAuth2ClientDO validOAuthClientFromCache(String clientId, String authorizedGrantType,
85   - Collection<String> scopes, String redirectUri);
  85 + OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret,
  86 + String authorizedGrantType, Collection<String> scopes, String redirectUri);
86 87  
87 88 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java
... ... @@ -176,7 +176,8 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
176 176 }
177 177  
178 178 @Override
179   - public OAuth2ClientDO validOAuthClientFromCache(String clientId, String authorizedGrantType, Collection<String> scopes, String redirectUri) {
  179 + public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret,
  180 + String authorizedGrantType, Collection<String> scopes, String redirectUri) {
180 181 // 校验客户端存在、且开启
181 182 OAuth2ClientDO client = clientCache.get(clientId);
182 183 if (client == null) {
... ... @@ -186,6 +187,10 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
186 187 throw exception(OAUTH2_CLIENT_DISABLE);
187 188 }
188 189  
  190 + // 校验客户端密钥
  191 + if (StrUtil.isNotEmpty(clientSecret) && ObjectUtil.notEqual(client.getSecret(), clientSecret)) {
  192 + throw exception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR, clientSecret);
  193 + }
189 194 // 校验授权方式
190 195 if (StrUtil.isNotEmpty(authorizedGrantType) && !CollUtil.contains(client.getAuthorizedGrantTypes(), authorizedGrantType)) {
191 196 throw exception(OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS);
... ... @@ -196,7 +201,7 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
196 201 }
197 202 // 校验回调地址
198 203 if (StrUtil.isNotEmpty(redirectUri) && !StrUtils.startWithAny(redirectUri, client.getRedirectUris())) {
199   - throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH);
  204 + throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, redirectUri);
200 205 }
201 206 return client;
202 207 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java
... ... @@ -22,9 +22,35 @@ public interface OAuth2GrantService {
22 22 OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
23 23 String clientId, List<String> scopes);
24 24  
25   - // AuthorizationCodeTokenGranter
26   - String grantAuthorizationCode(Long userId, Integer userType,
27   - String clientId, List<String> scopes,
28   - String redirectUri, String state);
  25 + /**
  26 + * 授权码模式,第一阶段,获得 code 授权码
  27 + *
  28 + * 对应 Spring Security OAuth2 的 AuthorizationEndpoint 的 generateCode 方法
  29 + *
  30 + * @param userId 用户编号
  31 + * @param userType 用户类型
  32 + * @param clientId 客户端编号
  33 + * @param scopes 授权范围
  34 + * @param redirectUri 重定向 URI
  35 + * @param state 状态
  36 + * @return 授权码
  37 + */
  38 + String grantAuthorizationCodeForCode(Long userId, Integer userType,
  39 + String clientId, List<String> scopes,
  40 + String redirectUri, String state);
  41 +
  42 + /**
  43 + * 授权码模式,第二阶段,获得 accessToken 访问令牌
  44 + *
  45 + * 对应 Spring Security OAuth2 的 AuthorizationCodeTokenGranter 功能
  46 + *
  47 + * @param clientId 客户端编号
  48 + * @param code 授权码
  49 + * @param redirectUri 重定向 URI
  50 + * @param state 状态
  51 + * @return 访问令牌
  52 + */
  53 + OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code,
  54 + String redirectUri, String state);
29 55  
30 56 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java
1 1 package cn.iocoder.yudao.module.system.service.oauth2;
2 2  
  3 +import cn.hutool.core.util.StrUtil;
3 4 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
  5 +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2CodeDO;
  6 +import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants;
4 7 import org.springframework.stereotype.Service;
5 8  
6 9 import javax.annotation.Resource;
7 10 import java.util.List;
8 11  
  12 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  13 +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_GRANT_CODE_NOT_EXISTS;
  14 +import static java.util.Collections.singletonList;
  15 +
9 16 /**
10 17 * OAuth2 授予 Service 实现类
11 18 *
... ... @@ -24,10 +31,38 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService {
24 31 }
25 32  
26 33 @Override
27   - public String grantAuthorizationCode(Long userId, Integer userType,
28   - String clientId, List<String> scopes,
29   - String redirectUri, String state) {
  34 + public String grantAuthorizationCodeForCode(Long userId, Integer userType,
  35 + String clientId, List<String> scopes,
  36 + String redirectUri, String state) {
30 37 return "test";
31 38 }
32 39  
  40 + @Override
  41 + public OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code,
  42 + String redirectUri, String state) {
  43 + // TODO 消费 code
  44 + OAuth2CodeDO codeDO = new OAuth2CodeDO().setClientId("default").setRedirectUri("https://www.iocoder.cn").setState("")
  45 + .setUserId(1L).setUserType(2).setScopes(singletonList("user_info"));
  46 + if (codeDO == null) {
  47 + throw exception(OAUTH2_GRANT_CODE_NOT_EXISTS);
  48 + }
  49 + // 校验 clientId 是否匹配
  50 + if (!StrUtil.equals(clientId, codeDO.getClientId())) {
  51 + throw exception(ErrorCodeConstants.OAUTH2_GRANT_CLIENT_ID_MISMATCH);
  52 + }
  53 + // 校验 redirectUri 是否匹配
  54 + if (!StrUtil.equals(redirectUri, codeDO.getRedirectUri())) {
  55 + throw exception(ErrorCodeConstants.OAUTH2_GRANT_REDIRECT_URI_MISMATCH);
  56 + }
  57 + // 校验 state 是否匹配
  58 + state = StrUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串
  59 + if (!StrUtil.equals(state, codeDO.getState())) {
  60 + throw exception(ErrorCodeConstants.OAUTH2_GRANT_STATE_MISMATCH);
  61 + }
  62 +
  63 + // 创建访问令牌
  64 + return oauth2TokenService.createAccessToken(codeDO.getUserId(), codeDO.getUserType(),
  65 + codeDO.getClientId(), codeDO.getScopes());
  66 + }
  67 +
33 68 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java
... ... @@ -50,13 +50,12 @@ public class OAuth2Utils {
50 50 Map<String, Object> vars = new LinkedHashMap<String, Object>();
51 51 Map<String, String> keys = new HashMap<String, String>();
52 52 vars.put("access_token", accessToken);
53   - vars.put("token_type", SecurityFrameworkUtils.TOKEN_TYPE.toLowerCase());
  53 + vars.put("token_type", SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase());
54 54 if (state != null) {
55 55 vars.put("state", state);
56 56 }
57 57 if (expireTime != null) {
58   - long expires_in = (expireTime.getTime() - System.currentTimeMillis()) / 1000;
59   - vars.put("expires_in", expires_in);
  58 + vars.put("expires_in", getExpiresIn(expireTime));
60 59 }
61 60 if (CollUtil.isNotEmpty(scopes)) {
62 61 vars.put("scope", CollUtil.join(scopes, " "));
... ... @@ -83,4 +82,8 @@ public class OAuth2Utils {
83 82 return HttpUtils.append(redirectUri, query, null, !responseType.contains("code"));
84 83 }
85 84  
  85 + public static long getExpiresIn(Date expireTime) {
  86 + return (expireTime.getTime() - System.currentTimeMillis()) / 1000;
  87 + }
  88 +
86 89 }
... ...