diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java index 7435cc76a..d36b2c308 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -1,11 +1,15 @@ package cn.iocoder.yudao.framework.common.util.http; +import cn.hutool.core.codec.Base64; import cn.hutool.core.map.TableMap; import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; +import javax.servlet.http.HttpServletRequest; import java.net.URI; import java.nio.charset.Charset; import java.util.Map; @@ -95,4 +99,28 @@ public class HttpUtils { return builder.build().toUriString(); } + public static String[] obtainBasicAuthorization(HttpServletRequest request) { + String clientId; + String clientSecret; + // 先从 Header 中获取 + String authorization = request.getHeader("Authorization"); + authorization = StrUtil.subAfter(authorization, "Basic ", true); + if (StringUtils.hasText(authorization)) { + authorization = Base64.decodeStr(authorization); + clientId = StrUtil.subBefore(authorization, ":", false); + clientSecret = StrUtil.subAfter(authorization, ":", false); + // 再从 Param 中获取 + } else { + clientId = request.getParameter("client_id"); + clientSecret = request.getParameter("client_secret"); + } + + // 如果两者非空,则返回 + if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) { + return new String[]{clientId, clientSecret}; + } + return null; + } + + } diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java index ba56e7ad4..5dc17b626 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java +++ b/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; */ public class SecurityFrameworkUtils { - public static final String TOKEN_TYPE = "Bearer"; + public static final String AUTHORIZATION_BEARER = "Bearer"; private SecurityFrameworkUtils() {} @@ -36,7 +36,7 @@ public class SecurityFrameworkUtils { if (!StringUtils.hasText(authorization)) { return null; } - int index = authorization.indexOf(TOKEN_TYPE + " "); + int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); if (index == -1) { // 未找到 return null; } diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java index a0d4ed009..24aa5e127 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java +++ b/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 { ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1002019000, "系统敏感词在所有标签中都不存在"); ErrorCode SENSITIVE_WORD_EXISTS = new ErrorCode(1002019001, "系统敏感词已在标签中存在"); - // ========== 系统敏感词 1002020000 ========= + // ========== OAuth2 客户端 1002020000 ========= ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在"); ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1002020001, "OAuth2 客户端编号已存在"); ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1002020002, "OAuth2 客户端已禁用"); ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1002020003, "不支持该授权类型"); ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1002020004, "授权范围过大"); - ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020005, "重定向地址不匹配"); + ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020005, "无效 redirect_uri: {}"); + ErrorCode OAUTH2_CLIENT_CLIENT_SECRET_ERROR = new ErrorCode(1002020006, "无效 client_secret: {}"); + + // ========== OAuth2 授权 1002021000 ========= + ErrorCode OAUTH2_GRANT_CLIENT_ID_MISMATCH = new ErrorCode(1002020000, "client_id 不匹配"); + ErrorCode OAUTH2_GRANT_REDIRECT_URI_MISMATCH = new ErrorCode(1002020001, "redirect_uri 不匹配"); + ErrorCode OAUTH2_GRANT_STATE_MISMATCH = new ErrorCode(1002020002, "state 不匹配"); + ErrorCode OAUTH2_GRANT_CODE_NOT_EXISTS = new ErrorCode(1002020003, "code 不存在"); } diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2GrantTypeEnum.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2GrantTypeEnum.java index f2394b7af..20eb8fce3 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2GrantTypeEnum.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/auth/OAuth2GrantTypeEnum.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.system.enums.auth; +import cn.hutool.core.util.ArrayUtil; import lombok.AllArgsConstructor; import lombok.Getter; @@ -21,4 +22,8 @@ public enum OAuth2GrantTypeEnum { private final String grantType; + public static OAuth2GrantTypeEnum getByGranType(String grantType) { + return ArrayUtil.firstMatch(o -> o.getGrantType().equals(grantType), values()); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.http index cd4d0f3b2..ce7cf83d8 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.http +++ b/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}} tenant-id: {{adminTenentId}} response_type=code&client_id=default&scope={"user_info": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true + +### 请求 /system/oauth2/token + code 接口 => 成功 +POST {{baseUrl}}/system/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +grant_type=authorization_code&redirect_uri=https://www.iocoder.cn diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java similarity index 68% rename from yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.java rename to yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java index e528890a1..5d7e0bbd2 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java @@ -1,12 +1,16 @@ package cn.iocoder.yudao.module.system.controller.admin.oauth2; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; +import cn.iocoder.yudao.module.system.convert.oauth2.OAuth2OpenConvert; import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO; import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum; @@ -23,6 +27,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; import java.util.Collections; import java.util.List; import java.util.Map; @@ -33,21 +38,17 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; -@Api(tags = "管理后台 - OAuth2.0 授权") +@Api(tags = "管理后台 - OAuth2.0 授权") // 提供给外部应用调用为主 @RestController @RequestMapping("/system/oauth2") @Validated @Slf4j -public class OAuth2Controller { - -// POST oauth/token TokenEndpoint:Password、Implicit、Code、Refresh Token +public class OAuth2OpenController { // POST oauth/check_token CheckTokenEndpoint // DELETE oauth/token ConsumerTokenServices#revokeToken -// GET oauth/authorize AuthorizationEndpoint - @Resource private OAuth2GrantService oauth2GrantService; @Resource @@ -55,6 +56,56 @@ public class OAuth2Controller { @Resource private OAuth2ApproveService oauth2ApproveService; + @PostMapping("/token") + @ApiOperation(value = "获得访问令牌", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用") + @ApiImplicitParams({ + @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型", example = "code", dataTypeClass = String.class), + @ApiImplicitParam(name = "code", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), + @ApiImplicitParam(name = "redirect_uri", value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class), + @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class) + }) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult postAccessToken(HttpServletRequest request, + @RequestParam("grant_type") String grantType, + @RequestParam(value = "code", required = false) String code, // 授权码模式 + @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式 + @RequestParam(value = "state", required = false) String state) { // 授权码模式 + // 授权类型 + OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType); + if (grantTypeEnum == null) { + throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType)); + } + if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) { + throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式"); + } + + // 校验客户端 + String[] clientIdAndSecret = HttpUtils.obtainBasicAuthorization(request); + if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) { + throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递"); + } + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], grantType, null, null); + + // 根据授权模式,获取访问令牌 + OAuth2AccessTokenDO accessTokenDO = null; + switch (grantTypeEnum) { + case AUTHORIZATION_CODE: + accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state); + break; + case PASSWORD: + break; + case CLIENT_CREDENTIALS: + break; + case REFRESH_TOKEN: + break; + default: + throw new IllegalArgumentException("未知授权类型:" + grantType); + } + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 + return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO)); + } + + // GET oauth/authorize AuthorizationEndpoint TODO @GetMapping("/authorize") @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用") @ApiImplicitParams({ @@ -75,12 +126,23 @@ public class OAuth2Controller { // 1.1 校验 responseType 是否满足 code 或者 token 值 OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 - oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes, redirectUri); + oauth2ClientService.validOAuthClientFromCache(clientId, null, + grantTypeEnum.getGrantType(), scopes, redirectUri); // 3. 不满足自动授权,则返回授权相关的展示信息 return null; } + /** + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法 + * + * 场景一:【自动授权 autoApprove = true】 + * 刚进入 authorize.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权 + * 场景二:【手动授权 autoApprove = false】 + * 在 authorize.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false + * + * 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理 + */ @PostMapping("/authorize") @ApiOperation(value = "申请授权", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【提交】调用") @ApiImplicitParams({ @@ -92,9 +154,6 @@ public class OAuth2Controller { @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class) }) @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 - // 场景一:【自动授权 autoApprove = true】刚进入 authorize.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权 - // 场景二:【手动授权 autoApprove = false】在 authorize.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false - // 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理 public CommonResult approveOrDeny(@RequestParam("response_type") String responseType, @RequestParam("client_id") String clientId, @RequestParam(value = "scope", required = false) String scope, @@ -110,7 +169,8 @@ public class OAuth2Controller { // 1.1 校验 responseType 是否满足 code 或者 token 值 OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 - OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null, + grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); // 2.1 假设 approved 为 null,说明是场景一 if (Boolean.TRUE.equals(autoApprove)) { @@ -159,7 +219,7 @@ public class OAuth2Controller { private String getAuthorizationCodeRedirect(Long userId, OAuth2ClientDO client, List scopes, String redirectUri, String state) { // 1. 创建 code 授权码 - String authorizationCode = oauth2GrantService.grantAuthorizationCode(userId,getUserType(), client.getClientId(), scopes, + String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(userId,getUserType(), client.getClientId(), scopes, redirectUri, state); // 2. 拼接重定向的 URL return OAuth2Utils.buildAuthorizationCodeRedirectUri(redirectUri, authorizationCode, state); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java new file mode 100644 index 000000000..b9c697a85 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel("管理后台 - 访问令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenAccessTokenRespVO { + + @ApiModelProperty(value = "访问令牌", required = true, example = "tudou") + @JsonProperty("access_token") + private String accessToken; + + @ApiModelProperty(value = "刷新令牌", required = true, example = "nice") + @JsonProperty("refresh_token") + private String refreshToken; + + @ApiModelProperty(value = "令牌类型", required = true, example = "bearer") + @JsonProperty("token_type") + private String tokenType; + + @ApiModelProperty(value = "过期时间", required = true, example = "42430", notes = "单位:秒") + @JsonProperty("expires_in") + private Long expiresIn; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java new file mode 100644 index 000000000..a873c7f28 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.system.convert.oauth2; + +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface OAuth2OpenConvert { + + OAuth2OpenConvert INSTANCE = Mappers.getMapper(OAuth2OpenConvert.class); + + default OAuth2OpenAccessTokenRespVO convert(OAuth2AccessTokenDO bean) { + OAuth2OpenAccessTokenRespVO respVO = convert0(bean); + respVO.setTokenType(SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); + respVO.setExpiresIn(OAuth2Utils.getExpiresIn(bean.getExpiresTime())); + return respVO; + } + + OAuth2OpenAccessTokenRespVO convert0(OAuth2AccessTokenDO bean); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java index 1233a2d81..94be75462 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java +++ b/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 { registry.antMatchers(buildAdminApi("/system/tenant/get-id-by-name")).permitAll(); // 短信回调 API registry.antMatchers(buildAdminApi("/system/sms/callback/**")).permitAll(); + // OAuth2 API + registry.antMatchers(buildAdminApi("/system/oauth2/token")).permitAll(); } }; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientService.java index 3c1408c40..a147fcc14 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientService.java +++ b/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 { * @return 客户端 */ default OAuth2ClientDO validOAuthClientFromCache(String clientId) { - return validOAuthClientFromCache(clientId, null, null, null); + return validOAuthClientFromCache(clientId, null, null, null, null); } /** @@ -76,12 +76,13 @@ public interface OAuth2ClientService { * 非空时,进行校验 * * @param clientId 客户端编号 + * @param clientSecret 客户端密钥 * @param authorizedGrantType 授权方式 * @param scopes 授权范围 * @param redirectUri 重定向地址 * @return 客户端 */ - OAuth2ClientDO validOAuthClientFromCache(String clientId, String authorizedGrantType, - Collection scopes, String redirectUri); + OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, + String authorizedGrantType, Collection scopes, String redirectUri); } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java index 194d6364e..627197256 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2ClientServiceImpl.java +++ b/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 { } @Override - public OAuth2ClientDO validOAuthClientFromCache(String clientId, String authorizedGrantType, Collection scopes, String redirectUri) { + public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, + String authorizedGrantType, Collection scopes, String redirectUri) { // 校验客户端存在、且开启 OAuth2ClientDO client = clientCache.get(clientId); if (client == null) { @@ -186,6 +187,10 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService { throw exception(OAUTH2_CLIENT_DISABLE); } + // 校验客户端密钥 + if (StrUtil.isNotEmpty(clientSecret) && ObjectUtil.notEqual(client.getSecret(), clientSecret)) { + throw exception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR, clientSecret); + } // 校验授权方式 if (StrUtil.isNotEmpty(authorizedGrantType) && !CollUtil.contains(client.getAuthorizedGrantTypes(), authorizedGrantType)) { throw exception(OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS); @@ -196,7 +201,7 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService { } // 校验回调地址 if (StrUtil.isNotEmpty(redirectUri) && !StrUtils.startWithAny(redirectUri, client.getRedirectUris())) { - throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH); + throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, redirectUri); } return client; } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java index 11c743d2e..92d1da337 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java +++ b/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 { OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, String clientId, List scopes); - // AuthorizationCodeTokenGranter - String grantAuthorizationCode(Long userId, Integer userType, - String clientId, List scopes, - String redirectUri, String state); + /** + * 授权码模式,第一阶段,获得 code 授权码 + * + * 对应 Spring Security OAuth2 的 AuthorizationEndpoint 的 generateCode 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 授权码 + */ + String grantAuthorizationCodeForCode(Long userId, Integer userType, + String clientId, List scopes, + String redirectUri, String state); + + /** + * 授权码模式,第二阶段,获得 accessToken 访问令牌 + * + * 对应 Spring Security OAuth2 的 AuthorizationCodeTokenGranter 功能 + * + * @param clientId 客户端编号 + * @param code 授权码 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, + String redirectUri, String state); } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java index 22dad3c88..377dde2fa 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java @@ -1,11 +1,18 @@ package cn.iocoder.yudao.module.system.service.oauth2; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; +import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2CodeDO; +import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.OAUTH2_GRANT_CODE_NOT_EXISTS; +import static java.util.Collections.singletonList; + /** * OAuth2 授予 Service 实现类 * @@ -24,10 +31,38 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { } @Override - public String grantAuthorizationCode(Long userId, Integer userType, - String clientId, List scopes, - String redirectUri, String state) { + public String grantAuthorizationCodeForCode(Long userId, Integer userType, + String clientId, List scopes, + String redirectUri, String state) { return "test"; } + @Override + public OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, + String redirectUri, String state) { + // TODO 消费 code + OAuth2CodeDO codeDO = new OAuth2CodeDO().setClientId("default").setRedirectUri("https://www.iocoder.cn").setState("") + .setUserId(1L).setUserType(2).setScopes(singletonList("user_info")); + if (codeDO == null) { + throw exception(OAUTH2_GRANT_CODE_NOT_EXISTS); + } + // 校验 clientId 是否匹配 + if (!StrUtil.equals(clientId, codeDO.getClientId())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_CLIENT_ID_MISMATCH); + } + // 校验 redirectUri 是否匹配 + if (!StrUtil.equals(redirectUri, codeDO.getRedirectUri())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_REDIRECT_URI_MISMATCH); + } + // 校验 state 是否匹配 + state = StrUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串 + if (!StrUtil.equals(state, codeDO.getState())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_STATE_MISMATCH); + } + + // 创建访问令牌 + return oauth2TokenService.createAccessToken(codeDO.getUserId(), codeDO.getUserType(), + codeDO.getClientId(), codeDO.getScopes()); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java index 0cdc88c5f..e3246505b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java +++ b/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 { Map vars = new LinkedHashMap(); Map keys = new HashMap(); vars.put("access_token", accessToken); - vars.put("token_type", SecurityFrameworkUtils.TOKEN_TYPE.toLowerCase()); + vars.put("token_type", SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); if (state != null) { vars.put("state", state); } if (expireTime != null) { - long expires_in = (expireTime.getTime() - System.currentTimeMillis()) / 1000; - vars.put("expires_in", expires_in); + vars.put("expires_in", getExpiresIn(expireTime)); } if (CollUtil.isNotEmpty(scopes)) { vars.put("scope", CollUtil.join(scopes, " ")); @@ -83,4 +82,8 @@ public class OAuth2Utils { return HttpUtils.append(redirectUri, query, null, !responseType.contains("code")); } + public static long getExpiresIn(Date expireTime) { + return (expireTime.getTime() - System.currentTimeMillis()) / 1000; + } + }