Commit 99ba7ccee8a59b52614ee01649c535272b8ff734
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 | } | ... | ... |