完成 oauth2 code 授权码模式的实现
parent
66034d26c0
commit
99ba7ccee8
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 不存在");
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<OAuth2OpenAccessTokenRespVO> 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<String> 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<String> 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);
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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<String> scopes, String redirectUri);
|
||||
OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret,
|
||||
String authorizedGrantType, Collection<String> scopes, String redirectUri);
|
||||
|
||||
}
|
||||
|
|
|
@ -176,7 +176,8 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public OAuth2ClientDO validOAuthClientFromCache(String clientId, String authorizedGrantType, Collection<String> scopes, String redirectUri) {
|
||||
public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret,
|
||||
String authorizedGrantType, Collection<String> 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;
|
||||
}
|
||||
|
|
|
@ -22,9 +22,35 @@ public interface OAuth2GrantService {
|
|||
OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
|
||||
String clientId, List<String> scopes);
|
||||
|
||||
// AuthorizationCodeTokenGranter
|
||||
String grantAuthorizationCode(Long userId, Integer userType,
|
||||
String clientId, List<String> 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<String> 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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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<String> scopes,
|
||||
String redirectUri, String state) {
|
||||
public String grantAuthorizationCodeForCode(Long userId, Integer userType,
|
||||
String clientId, List<String> 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -50,13 +50,12 @@ public class OAuth2Utils {
|
|||
Map<String, Object> vars = new LinkedHashMap<String, Object>();
|
||||
Map<String, String> keys = new HashMap<String, String>();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue