完成 oauth2 implicit 简化模式的实现

This commit is contained in:
YunaiV 2022-05-14 23:47:34 +08:00
parent 7d1deab48b
commit 6ca88277d8
17 changed files with 208 additions and 35 deletions

View File

@ -3,8 +3,12 @@ package cn.iocoder.yudao.framework.common.util.http;
import cn.hutool.core.map.TableMap; import cn.hutool.core.map.TableMap;
import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Map;
/** /**
* HTTP 工具类 * HTTP 工具类
@ -25,4 +29,70 @@ public class HttpUtils {
return builder.build(); return builder.build();
} }
private String append(String base, Map<String, ?> query, boolean fragment) {
return append(base, query, null, fragment);
}
/**
* 拼接 URL
*
* copy from Spring Security OAuth2 AuthorizationEndpoint 类的 append 方法
*
* @param base 基础 URL
* @param query 查询参数
* @param keys query key对应的原本的 key 的映射例如说 query 里有个 key xx实际它的 key extra_xx则通过 keys 里添加这个映射
* @param fragment URL fragment即拼接到 #
* @return 拼接后的 URL
*/
public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
UriComponentsBuilder template = UriComponentsBuilder.newInstance();
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
URI redirectUri;
try {
// assume it's encoded to start with (if it came in over the wire)
redirectUri = builder.build(true).toUri();
} catch (Exception e) {
// ... but allow client registrations to contain hard-coded non-encoded values
redirectUri = builder.build().toUri();
builder = UriComponentsBuilder.fromUri(redirectUri);
}
template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
.userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
if (fragment) {
StringBuilder values = new StringBuilder();
if (redirectUri.getFragment() != null) {
String append = redirectUri.getFragment();
values.append(append);
}
for (String key : query.keySet()) {
if (values.length() > 0) {
values.append("&");
}
String name = key;
if (keys != null && keys.containsKey(key)) {
name = keys.get(key);
}
values.append(name).append("={").append(key).append("}");
}
if (values.length() > 0) {
template.fragment(values.toString());
}
UriComponents encoded = template.build().expand(query).encode();
builder.fragment(encoded.getFragment());
} else {
for (String key : query.keySet()) {
String name = key;
if (keys != null && keys.containsKey(key)) {
name = keys.get(key);
}
template.queryParam(name, "{" + key + "}");
}
template.fragment(redirectUri.getFragment());
UriComponents encoded = template.build().expand(query).encode();
builder.query(encoded.getQuery());
}
return builder.build().toUriString();
}
} }

View File

@ -20,6 +20,8 @@ import java.util.Collections;
*/ */
public class SecurityFrameworkUtils { public class SecurityFrameworkUtils {
public static final String TOKEN_TYPE = "Bearer";
private SecurityFrameworkUtils() {} private SecurityFrameworkUtils() {}
/** /**
@ -34,7 +36,7 @@ public class SecurityFrameworkUtils {
if (!StringUtils.hasText(authorization)) { if (!StringUtils.hasText(authorization)) {
return null; return null;
} }
int index = authorization.indexOf("Bearer "); int index = authorization.indexOf(TOKEN_TYPE + " ");
if (index == -1) { // 未找到 if (index == -1) { // 未找到
return null; return null;
} }

View File

@ -6,6 +6,7 @@ import lombok.Data;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
/** /**
* OAuth2.0 访问令牌创建 Request DTO * OAuth2.0 访问令牌创建 Request DTO
@ -31,5 +32,9 @@ public class OAuth2AccessTokenCreateReqDTO implements Serializable {
*/ */
@NotNull(message = "客户端编号不能为空") @NotNull(message = "客户端编号不能为空")
private String clientId; private String clientId;
/**
* 授权范围
*/
private List<String> scopes;
} }

View File

@ -129,6 +129,6 @@ public interface ErrorCodeConstants {
ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1002020002, "OAuth2 客户端已禁用"); ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1002020002, "OAuth2 客户端已禁用");
ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1002020003, "不支持该授权类型"); ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1002020003, "不支持该授权类型");
ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1002020004, "授权范围过大"); ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1002020004, "授权范围过大");
ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020004, "重定向地址不匹配"); ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020005, "重定向地址不匹配");
} }

View File

@ -24,7 +24,7 @@ public class OAuth2TokenApiImpl implements OAuth2TokenApi {
@Override @Override
public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) { public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) {
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken( OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(
reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId()); reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId(), reqDTO.getScopes());
return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO);
} }

View File

@ -0,0 +1,7 @@
### 请求 /system/oauth2/authorize 接口 => 成功
POST {{baseUrl}}/system/oauth2/authorize
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
response_type=token&client_id=default&scope={"user_info": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true

View File

@ -1,15 +1,19 @@
package cn.iocoder.yudao.module.system.controller.admin.oauth2; package cn.iocoder.yudao.module.system.controller.admin.oauth2;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
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; import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ApproveService; import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ApproveService;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService; import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2GrantService; import cn.iocoder.yudao.module.system.service.oauth2.OAuth2GrantService;
import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiImplicitParams;
@ -26,6 +30,7 @@ import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 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; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Api(tags = "管理后台 - OAuth2.0 授权") @Api(tags = "管理后台 - OAuth2.0 授权")
@ -72,16 +77,6 @@ public class OAuth2Controller {
// 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内
oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes, redirectUri); oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes, redirectUri);
// 2. 判断是否满足自动授权满足)
boolean approved = oauth2ApproveService.checkForPreApproval(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes);
if (approved) {
// 2.1 如果是 code 授权码模式则发放 code 授权码并重定向
if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) {
return success(getAuthorizationCodeRedirect());
}
return success(getImplicitGrantRedirect());
}
// 3. 不满足自动授权则返回授权相关的展示信息 // 3. 不满足自动授权则返回授权相关的展示信息
return null; return null;
} }
@ -104,7 +99,7 @@ public class OAuth2Controller {
@RequestParam("client_id") String clientId, @RequestParam("client_id") String clientId,
@RequestParam(value = "scope", required = false) String scope, @RequestParam(value = "scope", required = false) String scope,
@RequestParam("redirect_uri") String redirectUri, @RequestParam("redirect_uri") String redirectUri,
@RequestParam(value = "autoApprove") Boolean autoApprove, @RequestParam(value = "auto_approve") Boolean autoApprove,
@RequestParam(value = "state", required = false) String state) { @RequestParam(value = "state", required = false) String state) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class); Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class);
@ -115,27 +110,28 @@ public class OAuth2Controller {
// 1.1 校验 responseType 是否满足 code 或者 token // 1.1 校验 responseType 是否满足 code 或者 token
OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType);
// 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内
oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);
// 2.1 假设 approved null说明是场景一 // 2.1 假设 approved null说明是场景一
if (Boolean.TRUE.equals(autoApprove)) { if (Boolean.TRUE.equals(autoApprove)) {
// 如果无法自动授权通过则返回空 url前端不进行跳转 // 如果无法自动授权通过则返回空 url前端不进行跳转
if (!oauth2ApproveService.checkForPreApproval(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes.keySet())) { if (!oauth2ApproveService.checkForPreApproval(getLoginUserId(), getUserType(), clientId, scopes.keySet())) {
return success(null); return success(null);
} }
} else { // 2.2 假设 approved null说明是场景二 } else { // 2.2 假设 approved null说明是场景二
// 如果计算后不通过则跳转一个错误链接 // 如果计算后不通过则跳转一个错误链接
if (!oauth2ApproveService.updateAfterApproval(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes)) { if (!oauth2ApproveService.updateAfterApproval(getLoginUserId(), getUserType(), clientId, scopes)) {
return success("TODO"); return success("TODO");
} }
} }
// 3.1 如果是 code 授权码模式则发放 code 授权码并重定向 // 3.1 如果是 code 授权码模式则发放 code 授权码并重定向
List<String> approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue);
if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) { if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) {
return success(getAuthorizationCodeRedirect()); return success(getAuthorizationCodeRedirect());
} }
// 3.2 如果是 token 则是 implicit 简化模式则发送 accessToken 访问令牌并重定向 // 3.2 如果是 token 则是 implicit 简化模式则发送 accessToken 访问令牌并重定向
return success(getImplicitGrantRedirect()); return success(getImplicitGrantRedirect(getLoginUserId(), client, redirectUri, state, approveScopes));
} }
private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) { private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) {
@ -148,12 +144,22 @@ public class OAuth2Controller {
throw exception0(BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token"); throw exception0(BAD_REQUEST.getCode(), "response_type 参数值允许 code 和 token");
} }
private String getImplicitGrantRedirect() { private String getImplicitGrantRedirect(Long userId, OAuth2ClientDO client,
return ""; String redirectUri, String state, List<String> scopes) {
OAuth2AccessTokenDO accessTokenDO = oAuth2GrantService.grantImplicit(userId, getUserType(), client.getClientId(), scopes);
Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查
// 拼接 URL
// noinspection unchecked
return OAuth2Utils.buildImplicitRedirectUri(redirectUri, accessTokenDO.getAccessToken(), state, accessTokenDO.getExpiresTime(),
scopes, JsonUtils.parseObject(client.getAdditionalInformation(), Map.class));
} }
private String getAuthorizationCodeRedirect() { private String getAuthorizationCodeRedirect() {
return ""; return "";
} }
private Integer getUserType() {
return UserTypeEnum.ADMIN.getValue();
}
} }

View File

@ -3,13 +3,16 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* OAuth2 访问令牌 DO * OAuth2 访问令牌 DO
@ -55,6 +58,11 @@ public class OAuth2AccessTokenDO extends TenantBaseDO {
* 关联 {@link OAuth2ClientDO#getId()} * 关联 {@link OAuth2ClientDO#getId()}
*/ */
private String clientId; private String clientId;
/**
* 授权范围
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> scopes;
/** /**
* 过期时间 * 过期时间
*/ */

View File

@ -3,12 +3,15 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* OAuth2 刷新令牌 * OAuth2 刷新令牌
@ -47,6 +50,11 @@ public class OAuth2RefreshTokenDO extends BaseDO {
* 关联 {@link OAuth2ClientDO#getId()} * 关联 {@link OAuth2ClientDO#getId()}
*/ */
private String clientId; private String clientId;
/**
* 授权范围
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> scopes;
/** /**
* 过期时间 * 过期时间
*/ */

View File

@ -207,7 +207,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
// 创建访问令牌 // 创建访问令牌
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
OAuth2ClientConstants.CLIENT_ID_DEFAULT); OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
// 构建返回结果 // 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenDO); return AuthConvert.INSTANCE.convert(accessTokenDO);
} }

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.system.service.oauth2; package cn.iocoder.yudao.module.system.service.oauth2;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -181,7 +182,7 @@ public class OAuth2ClientServiceImpl implements OAuth2ClientService {
if (client == null) { if (client == null) {
throw exception(OAUTH2_CLIENT_EXISTS); throw exception(OAUTH2_CLIENT_EXISTS);
} }
if (Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { if (ObjectUtil.notEqual(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
throw exception(OAUTH2_CLIENT_DISABLE); throw exception(OAUTH2_CLIENT_DISABLE);
} }

View File

@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.service.oauth2;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
import java.util.Collection; import java.util.List;
/** /**
* OAuth2 授予 Service 接口 * OAuth2 授予 Service 接口
@ -20,11 +20,11 @@ public interface OAuth2GrantService {
// ImplicitTokenGranter // ImplicitTokenGranter
OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
String clientId, Collection<String> scopes); String clientId, List<String> scopes);
// AuthorizationCodeTokenGranter // AuthorizationCodeTokenGranter
String grantAuthorizationCode(Long userId, Integer userType, String grantAuthorizationCode(Long userId, Integer userType,
String clientId, Collection<String> scopes, String clientId, List<String> scopes,
String redirectUri, String state); String redirectUri, String state);
} }

View File

@ -3,7 +3,9 @@ package cn.iocoder.yudao.module.system.service.oauth2;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collection; import java.util.Collection;
import java.util.List;
/** /**
* OAuth2 授予 Service 实现类 * OAuth2 授予 Service 实现类
@ -13,15 +15,18 @@ import java.util.Collection;
@Service @Service
public class OAuth2GrantServiceImpl implements OAuth2GrantService { public class OAuth2GrantServiceImpl implements OAuth2GrantService {
@Resource
private OAuth2TokenService oauth2TokenService;
@Override @Override
public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
String clientId, Collection<String> scopes) { String clientId, List<String> scopes) {
return null; return oauth2TokenService.createAccessToken(userId, userType, clientId, scopes);
} }
@Override @Override
public String grantAuthorizationCode(Long userId, Integer userType, public String grantAuthorizationCode(Long userId, Integer userType,
String clientId, Collection<String> scopes, String clientId, List<String> scopes,
String redirectUri, String state) { String redirectUri, String state) {
return null; return null;
} }

View File

@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
import java.util.List;
/** /**
* OAuth2.0 Token Service 接口 * OAuth2.0 Token Service 接口
* *
@ -22,9 +24,10 @@ public interface OAuth2TokenService {
* @param userId 用户编号 * @param userId 用户编号
* @param userType 用户类型 * @param userType 用户类型
* @param clientId 客户端编号 * @param clientId 客户端编号
* @param scopes 授权范围
* @return 访问令牌的信息 * @return 访问令牌的信息
*/ */
OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId); OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes);
/** /**
* 刷新访问令牌 * 刷新访问令牌

View File

@ -45,10 +45,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
@Override @Override
@Transactional @Transactional
public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId) { public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) {
OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
// 创建刷新令牌 // 创建刷新令牌
OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO); OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes);
// 创建访问令牌 // 创建访问令牌
return createOAuth2AccessToken(refreshTokenDO, clientDO); return createOAuth2AccessToken(refreshTokenDO, clientDO);
} }
@ -134,7 +134,8 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken())
.setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getClientId()) .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType())
.setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
.setRefreshToken(refreshTokenDO.getRefreshToken()) .setRefreshToken(refreshTokenDO.getRefreshToken())
.setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds())); .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds()));
accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号避免缓存到 Redis 的时候无对应的租户编号 accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号避免缓存到 Redis 的时候无对应的租户编号
@ -144,9 +145,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
return accessTokenDO; return accessTokenDO;
} }
private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) { private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List<String> scopes) {
OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
.setUserId(userId).setUserType(userType).setClientId(clientDO.getClientId()) .setUserId(userId).setUserType(userType)
.setClientId(clientDO.getClientId()).setScopes(scopes)
.setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds())); .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds()));
oauth2RefreshTokenMapper.insert(refreshToken); oauth2RefreshTokenMapper.insert(refreshToken);
return refreshToken; return refreshToken;

View File

@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.system.util.oauth2;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import java.util.*;
/**
* OAuth2 相关的工具类
*
* @author 芋道源码
*/
public class OAuth2Utils {
/**
* 构建简化模式下重定向的 URI
*
* copy from Spring Security OAuth2 AuthorizationEndpoint 类的 appendAccessToken 方法
*
* @param redirectUri 重定向 URI
* @param accessToken 访问令牌
* @param state 状态
* @param expireTime 过期时间
* @param scopes 授权范围
* @param additionalInformation 附加信息
* @return 简化授权模式下的重定向 URI
*/
public static String buildImplicitRedirectUri(String redirectUri, String accessToken, String state, Date expireTime,
Collection<String> scopes, Map<String, Object> additionalInformation) {
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());
if (state != null) {
vars.put("state", state);
}
if (expireTime != null) {
long expires_in = (expireTime.getTime() - System.currentTimeMillis()) / 1000;
vars.put("expires_in", expires_in);
}
if (CollUtil.isNotEmpty(scopes)) {
vars.put("scope", CollUtil.join(scopes, " "));
}
for (String key : additionalInformation.keySet()) {
Object value = additionalInformation.get(key);
if (value != null) {
keys.put("extra_" + key, key);
vars.put("extra_" + key, value);
}
}
// Do not include the refresh token (even if there is one)
return HttpUtils.append(redirectUri, vars, keys, true);
}
}

View File

@ -201,7 +201,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
// mock 缓存登录用户到 Redis // mock 缓存登录用户到 Redis
OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue())); .setUserType(UserTypeEnum.ADMIN.getValue()));
when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"))) when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull()))
.thenReturn(accessTokenDO); .thenReturn(accessTokenDO);
// 调用, 并断言异常 // 调用, 并断言异常