0728 ljc 升级为OIDC版本,若出现问题可选择回退版本,version 1.0

This commit is contained in:
HopeLi 2025-07-28 16:02:09 +08:00
parent cb22cd43c6
commit 28ce63a0e1
9 changed files with 413 additions and 33 deletions

View File

@ -112,4 +112,15 @@ public interface OAuth2GrantService {
*/ */
boolean revokeToken(String clientId, String accessToken); boolean revokeToken(String clientId, String accessToken);
/**
* 生成 ID Token
*
* @param userId 用户ID
* @param userType 用户类型
* @param clientId 客户端ID
* @param scopes 授权范围
* @return ID Token
*/
String grantIDToken(Long userId, Integer userType, String clientId, List<String> scopes, String nonce);
} }

View File

@ -1,20 +1,29 @@
package cd.casic.framework.tenant.core.service; package cd.casic.framework.tenant.core.service;
import cd.casic.framework.commons.enums.UserTypeEnum;
import cd.casic.framework.datapermission.service.user.AdminUserService;
import cd.casic.framework.datapermission.service.user.OAuth2TokenService; import cd.casic.framework.datapermission.service.user.OAuth2TokenService;
import cd.casic.framework.security.dal.oauth2.OAuth2AccessTokenDO; import cd.casic.framework.security.dal.oauth2.OAuth2AccessTokenDO;
import cd.casic.framework.security.dal.oauth2.OAuth2CodeDO; import cd.casic.framework.security.dal.oauth2.OAuth2CodeDO;
import cd.casic.framework.security.dal.user.AdminUserDO; import cd.casic.framework.security.dal.user.AdminUserDO;
import cd.casic.framework.security.oauth2.OAuth2CodeService; import cd.casic.framework.security.oauth2.OAuth2CodeService;
import cd.casic.module.system.enums.ErrorCodeConstants;
import cn.hutool.core.lang.Assert; 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 cd.casic.framework.commons.enums.UserTypeEnum; import com.nimbusds.jose.JOSEObjectType;
import cd.casic.module.system.enums.ErrorCodeConstants; import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.Resource; import java.util.Date;
import java.util.List; import java.util.List;
import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception; import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exception;
@ -24,6 +33,7 @@ import static cd.casic.framework.commons.exception.util.ServiceExceptionUtil.exc
* *
* @author mianbin modified from yudao * @author mianbin modified from yudao
*/ */
@Slf4j
@Service @Service
public class OAuth2GrantServiceImpl implements OAuth2GrantService { public class OAuth2GrantServiceImpl implements OAuth2GrantService {
@ -33,6 +43,8 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService {
private OAuth2CodeService oauth2CodeService; private OAuth2CodeService oauth2CodeService;
@Resource @Resource
private AdminAuthService adminAuthService; private AdminAuthService adminAuthService;
@Resource
private AdminUserService adminUserService;
@Override @Override
public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
@ -104,4 +116,61 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService {
return oauth2TokenService.removeAccessToken(accessToken) != null; return oauth2TokenService.removeAccessToken(accessToken) != null;
} }
@Override
public String grantIDToken(Long userId, Integer userType, String clientId, List<String> scopes, String nonce) {
try {
// 获取用户信息
AdminUserDO user = adminUserService.getUser(userId);
if (user == null) {
throw new IllegalArgumentException("用户不存在");
}
// 使用 nimbus-jose-jwt 库生成 ID Token
// JWT 头部
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256)
.type(JOSEObjectType.JWT)
.build();
// JWT 载荷
JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder()
.issuer("http://localhost:8080") // iss: 发行者
.subject(String.valueOf(userId)) // sub: 主题
.audience(clientId) // aud: 受众
.expirationTime(new Date(System.currentTimeMillis() + 3600000)) // exp: 过期时间 (1小时)
.issueTime(new Date()) // iat: 签发时间
.claim("auth_time", new Date()); // auth_time: 认证时间
// 添加 nonce如果提供
if (StrUtil.isNotBlank(nonce)) {
claimsBuilder.claim("nonce", nonce);
}
// 添加 OIDC 标准声明
if (scopes.contains("profile")) {
claimsBuilder.claim("name", user.getNickname());
claimsBuilder.claim("preferred_username", user.getUsername());
}
if (scopes.contains("email")) {
claimsBuilder.claim("email", user.getEmail());
claimsBuilder.claim("email_verified", StrUtil.isNotBlank(user.getEmail()));
}
JWTClaimsSet claimsSet = claimsBuilder.build();
// 签名
SignedJWT signedJWT = new SignedJWT(header, claimsSet);
// 创建 HMAC signer (实际项目中应使用配置的密钥)
JWSSigner signer = new MACSigner("your-oidc-secret-key-min-32-chars"); // 密钥至少32字符
signedJWT.sign(signer);
return signedJWT.serialize();
} catch (Exception e) {
log.error("生成 ID Token 失败", e);
return "";
}
}
} }

View File

@ -106,4 +106,14 @@ public class OAuth2ClientDO extends BaseDO {
*/ */
private String additionalInformation; private String additionalInformation;
/**
* 是否支持 OIDC
*/
private Boolean oidcSupport = false;
/**
* 默认响应类型
*/
private String defaultResponseType = "code";
} }

View File

@ -75,6 +75,12 @@ public class OAuth2ClientSaveReqVO {
@Schema(description = "附加信息", example = "{yunai: true}") @Schema(description = "附加信息", example = "{yunai: true}")
private String additionalInformation; private String additionalInformation;
@Schema(description = "是否支持 OIDC", example = "true")
private Boolean oidcSupport = false;
@Schema(description = "默认响应类型", example = "code id_token")
private String defaultResponseType = "code";
@AssertTrue(message = "附加信息必须是 JSON 格式") @AssertTrue(message = "附加信息必须是 JSON 格式")
public boolean isAdditionalInformationJson() { public boolean isAdditionalInformationJson() {
return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation); return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation);

View File

@ -1,38 +1,41 @@
package cd.casic.module.system.controller.admin.oauth2; package cd.casic.module.system.controller.admin.oauth2;
import cd.casic.framework.security.dal.oauth2.OAuth2AccessTokenDO;
import cd.casic.framework.security.dal.oauth2.OAuth2ApproveDO;
import cd.casic.framework.security.dal.oauth2.OAuth2ClientDO;
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 cd.casic.framework.commons.enums.UserTypeEnum; import cd.casic.framework.commons.enums.UserTypeEnum;
import cd.casic.framework.commons.pojo.CommonResult; import cd.casic.framework.commons.pojo.CommonResult;
import cd.casic.framework.commons.util.http.HttpUtils; import cd.casic.framework.commons.util.http.HttpUtils;
import cd.casic.framework.commons.util.json.JsonUtils; import cd.casic.framework.commons.util.json.JsonUtils;
import cd.casic.framework.datapermission.service.user.AdminUserService;
import cd.casic.framework.datapermission.service.user.OAuth2TokenService;
import cd.casic.framework.security.dal.oauth2.OAuth2AccessTokenDO;
import cd.casic.framework.security.dal.oauth2.OAuth2ApproveDO;
import cd.casic.framework.security.dal.oauth2.OAuth2ClientDO;
import cd.casic.framework.security.oauth2.OAuth2ApproveService;
import cd.casic.framework.security.oauth2.OAuth2ClientService;
import cd.casic.framework.security.vo.vo.open.OAuth2OpenAccessTokenRespVO; import cd.casic.framework.security.vo.vo.open.OAuth2OpenAccessTokenRespVO;
import cd.casic.framework.security.vo.vo.open.OAuth2OpenAuthorizeInfoRespVO; import cd.casic.framework.security.vo.vo.open.OAuth2OpenAuthorizeInfoRespVO;
import cd.casic.framework.security.vo.vo.open.OAuth2OpenCheckTokenRespVO; import cd.casic.framework.security.vo.vo.open.OAuth2OpenCheckTokenRespVO;
import cd.casic.framework.tenant.core.service.OAuth2GrantService;
import cd.casic.module.system.convert.oauth2.OAuth2OpenConvert; import cd.casic.module.system.convert.oauth2.OAuth2OpenConvert;
import cd.casic.module.system.enums.oauth2.OAuth2GrantTypeEnum; import cd.casic.module.system.enums.oauth2.OAuth2GrantTypeEnum;
import cd.casic.framework.security.oauth2.OAuth2ApproveService;
import cd.casic.framework.security.oauth2.OAuth2ClientService;
import cd.casic.framework.tenant.core.service.OAuth2GrantService;
import cd.casic.framework.datapermission.service.user.OAuth2TokenService;
import cd.casic.module.system.util.oauth2.OAuth2Utils; import cd.casic.module.system.util.oauth2.OAuth2Utils;
import io.swagger.v3.oas.annotations.tags.Tag; 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 io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource; import java.time.ZoneId;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -69,6 +72,8 @@ public class OAuth2OpenController {
private OAuth2ApproveService oauth2ApproveService; private OAuth2ApproveService oauth2ApproveService;
@Resource @Resource
private OAuth2TokenService oauth2TokenService; private OAuth2TokenService oauth2TokenService;
@Resource
private AdminUserService adminUserService;
/** /**
* 对应 Spring Security OAuth TokenEndpoint 类的 postAccessToken 方法 * 对应 Spring Security OAuth TokenEndpoint 类的 postAccessToken 方法
@ -181,13 +186,36 @@ public class OAuth2OpenController {
@GetMapping("/authorize") @GetMapping("/authorize")
@Operation(summary = "获得授权信息", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用") @Operation(summary = "获得授权信息", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用")
@Parameter(name = "clientId", required = true, description = "客户端编号", example = "tudou") @Parameter(name = "clientId", required = true, description = "客户端编号", example = "tudou")
public CommonResult<OAuth2OpenAuthorizeInfoRespVO> authorize(@RequestParam("clientId") String clientId) { public CommonResult<OAuth2OpenAuthorizeInfoRespVO> authorize(@RequestParam("clientId") String clientId,
@RequestParam(value = "response_type", required = false) String responseType) {
// // 0. 校验用户已经登录通过 Spring Security 实现
//
// // 1. 获得 Client 客户端的信息
// OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId);
// // 2. 获得用户已经授权的信息
// List<OAuth2ApproveDO> approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId);
// // 拼接返回
// return success(OAuth2OpenConvert.INSTANCE.convert(client, approves));
// 0. 校验用户已经登录通过 Spring Security 实现 // 0. 校验用户已经登录通过 Spring Security 实现
// 1. 获得 Client 客户端的信息 // 1. 获得 Client 客户端的信息
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId); OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId);
// 2. 获得用户已经授权的信息
// 2. 检查是否为 OIDC 请求
boolean isOIDCRequest = isOIDCRequest(responseType, client);
// 3. 获得用户已经授权的信息
List<OAuth2ApproveDO> approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId); List<OAuth2ApproveDO> approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId);
// 4. 如果是 OIDC 请求确保 openid scope 被包含
if (isOIDCRequest) {
// 可以在这里添加特殊的处理逻辑
}
// 拼接返回 // 拼接返回
return success(OAuth2OpenConvert.INSTANCE.convert(client, approves)); return success(OAuth2OpenConvert.INSTANCE.convert(client, approves));
} }
@ -217,8 +245,10 @@ public class OAuth2OpenController {
@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 = "auto_approve") Boolean autoApprove, @RequestParam(value = "auto_approve") Boolean autoApprove,
@RequestParam(value = "state", required = false) String state) { @RequestParam(value = "state", required = false) String state,
@RequestParam(value = "nonce", required = false) String nonce) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class); Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class);
scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap()); scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap());
// 0. 校验用户已经登录通过 Spring Security 实现 // 0. 校验用户已经登录通过 Spring Security 实现
@ -229,6 +259,9 @@ public class OAuth2OpenController {
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null, OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null,
grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);
// 检查是否为 OIDC 请求
boolean isOIDCRequest = checkOIDCRequest(responseType, scopes);
// 2.1 假设 approved null说明是场景一 // 2.1 假设 approved null说明是场景一
if (Boolean.TRUE.equals(autoApprove)) { if (Boolean.TRUE.equals(autoApprove)) {
// 如果无法自动授权通过则返回空 url前端不进行跳转 // 如果无法自动授权通过则返回空 url前端不进行跳转
@ -243,12 +276,17 @@ public class OAuth2OpenController {
} }
} }
// 3.1 如果是 code 授权码模式则发放 code 授权码并重定向 // 3. 根据不同的响应类型处理
List<String> approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue); List<String> approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue);
if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) { if (isOIDCRequest && responseType.contains("id_token")) {
// 处理包含 ID Token 的响应类型
return handleOIDCResponse(grantTypeEnum, getLoginUserId(), client, approveScopes, redirectUri, state, responseType, nonce);
} else if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) {
return success(getAuthorizationCodeRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); return success(getAuthorizationCodeRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
} else if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) {
return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
} }
// 3.2 如果是 token 则是 implicit 简化模式则发送 accessToken 访问令牌并重定向
return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state));
} }
@ -294,4 +332,85 @@ public class OAuth2OpenController {
return clientIdAndSecret; return clientIdAndSecret;
} }
// 添加判断是否为 OIDC 请求的方法
private boolean isOIDCRequest(String responseType, OAuth2ClientDO client) {
// 检查客户端是否支持 OIDC
if (client.getOidcSupport() == null || !client.getOidcSupport()) {
return false;
}
// 检查 response_type 是否包含 OIDC 相关类型
if (responseType != null &&
(responseType.contains("id_token") || "token".equals(responseType))) {
return true;
}
// 检查客户端默认响应类型
if (client.getDefaultResponseType() != null &&
(client.getDefaultResponseType().contains("id_token") ||
"token".equals(client.getDefaultResponseType()))) {
return true;
}
return false;
}
// 检查是否为 OIDC 请求
private boolean checkOIDCRequest(String responseType, Map<String, Boolean> scopes) {
// 检查 scope 中是否包含 openid
if (scopes.containsKey("openid")) {
return true;
}
// 检查 responseType 是否包含 id_token
if (responseType != null && responseType.contains("id_token")) {
return true;
}
return false;
}
// 处理 OIDC 响应
private CommonResult<String> handleOIDCResponse(OAuth2GrantTypeEnum grantTypeEnum,
Long userId,
OAuth2ClientDO client,
List<String> scopes,
String redirectUri,
String state,
String responseType,
String nonce) {
if (responseType.equals("code id_token")) {
// Hybrid Flow
String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(
userId, getUserType(), client.getClientId(), scopes, redirectUri, state);
// 创建 ID Token
String idToken = createIDTokenWithNonce(userId, client.getClientId(), scopes, nonce);
return success(OAuth2Utils.buildHybridRedirectUri(redirectUri, authorizationCode, idToken, state));
} else if (responseType.equals("id_token")) {
// Implicit Flow with ID Token only
String idToken = createIDTokenWithNonce(userId, client.getClientId(), scopes, nonce);
return success(OAuth2Utils.buildIDTokenRedirectUri(redirectUri, idToken, state));
} else if (responseType.equals("id_token token")) {
// Implicit Flow with Access Token and ID Token
OAuth2AccessTokenDO accessTokenDO = oauth2GrantService.grantImplicit(
userId, getUserType(), client.getClientId(), scopes);
String idToken = createIDTokenWithNonce(userId, client.getClientId(), scopes, nonce);
return success(OAuth2Utils.buildImplicitWithIDTokenRedirectUri(
redirectUri, accessTokenDO.getAccessToken(), idToken, state, Date.from(accessTokenDO.getExpiresTime().atZone(ZoneId.systemDefault()).toInstant())));
}
// 当没有匹配的响应类型时抛出异常
throw exception0(BAD_REQUEST.getCode(), StrUtil.format("不支持的 OIDC 响应类型: {}", responseType));
}
// 创建 ID Token
// 添加带 nonce ID Token 创建方法
private String createIDTokenWithNonce(Long userId, String clientId, List<String> scopes, String nonce) {
return oauth2GrantService.grantIDToken(userId, getUserType(), clientId, scopes, nonce);
}
} }

View File

@ -0,0 +1,56 @@
package cd.casic.module.system.controller.admin.oauth2;
import cd.casic.framework.commons.pojo.CommonResult;
import cd.casic.framework.datapermission.service.user.AdminUserService;
import cd.casic.framework.security.core.util.SecurityFrameworkUtils;
import cd.casic.framework.security.dal.user.AdminUserDO;
import cd.casic.module.system.dal.dataobject.oidc.OIDCUserInfoRespVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author HopeLi
* @version v1.0
* @ClassName OIDCUserInfoController
* @Date: 2025/7/28 10:56
* @Description:
*/
@Tag(name = "管理后台 - OIDC 用户信息")
@RestController
@RequestMapping("/system/oidc")
@Validated
@Slf4j
public class OIDCUserInfoController {
@Resource
private AdminUserService userService;
@GetMapping("/userinfo")
@Operation(summary = "获取 OIDC 用户信息")
@PreAuthorize("@ss.hasScope('openid')")
public CommonResult<OIDCUserInfoRespVO> getUserInfo() {
// 获得用户基本信息
Long userId = SecurityFrameworkUtils.getLoginUserId();
AdminUserDO user = userService.getUser(userId);
if (user == null) {
return CommonResult.error(404, "用户不存在");
}
OIDCUserInfoRespVO userInfo = new OIDCUserInfoRespVO();
userInfo.setSub(String.valueOf(user.getId()));
userInfo.setName(user.getNickname());
userInfo.setNickname(user.getNickname());
userInfo.setEmail(user.getEmail());
// 如果有头像字段可以设置
// userInfo.setPicture(user.getAvatar());
return CommonResult.success(userInfo);
}
}

View File

@ -0,0 +1,33 @@
package cd.casic.module.system.dal.dataobject.oidc;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* @author HopeLi
* @version v1.0
* @ClassName OIDCUserInfoRespVO
* @Date: 2025/7/28 10:21
* @Description:
*/
@Schema(description = "OIDC 用户信息 Response VO")
@Data
public class OIDCUserInfoRespVO {
@Schema(description = "用户子标识", example = "123")
private String sub;
@Schema(description = "用户名", example = "tudou")
private String name;
@Schema(description = "用户昵称", example = "土豆")
private String nickname;
@Schema(description = "邮箱", example = "tudou@iocoder.cn")
private String email;
@Schema(description = "头像地址", example = "https://www.iocoder.cn/avatar.jpg")
private String picture;
// 可根据需要添加更多标准字段
}

View File

@ -100,4 +100,80 @@ public class OAuth2Utils {
return StrUtil.split(scope, ' '); return StrUtil.split(scope, ' ');
} }
/**
* 判断是否为 OIDC 请求
*/
public static boolean isOIDCRequest(String responseType, String scope) {
// 检查 scope 中是否包含 openid
if (StrUtil.isNotBlank(scope) && scope.contains("openid")) {
return true;
}
// 检查 responseType 是否包含 id_token
if (StrUtil.isNotBlank(responseType) && responseType.contains("id_token")) {
return true;
}
return false;
}
/**
* 构建带有 ID Token 的重定向 URI (response_type=id_token)
*/
public static String buildIDTokenRedirectUri(String redirectUri, String idToken, String state) {
StringBuilder sb = new StringBuilder();
sb.append(redirectUri);
if (!redirectUri.contains("?")) {
sb.append("?");
} else {
sb.append("&");
}
sb.append("id_token=").append(idToken);
if (StrUtil.isNotBlank(state)) {
sb.append("&state=").append(state);
}
return sb.toString();
}
/**
* 构建带有 Access Token ID Token 的重定向 URI (response_type=id_token token)
*/
public static String buildImplicitWithIDTokenRedirectUri(String redirectUri, String accessToken,
String idToken, String state, Date expiresTime) {
StringBuilder sb = new StringBuilder();
sb.append(redirectUri);
if (!redirectUri.contains("?")) {
sb.append("?");
} else {
sb.append("&");
}
sb.append("access_token=").append(accessToken);
sb.append("&id_token=").append(idToken);
sb.append("&token_type=Bearer");
sb.append("&expires_in=").append((expiresTime.getTime() - System.currentTimeMillis()) / 1000);
if (StrUtil.isNotBlank(state)) {
sb.append("&state=").append(state);
}
return sb.toString();
}
/**
* 构建混合流程的重定向 URI (response_type=code id_token)
*/
public static String buildHybridRedirectUri(String redirectUri, String code, String idToken, String state) {
StringBuilder sb = new StringBuilder();
sb.append(redirectUri);
if (!redirectUri.contains("?")) {
sb.append("?");
} else {
sb.append("&");
}
sb.append("code=").append(code);
sb.append("&id_token=").append(idToken);
if (StrUtil.isNotBlank(state)) {
sb.append("&state=").append(state);
}
return sb.toString();
}
} }

View File

@ -205,7 +205,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves); when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves);
// 调用 // 调用
CommonResult<OAuth2OpenAuthorizeInfoRespVO> result = oauth2OpenController.authorize(clientId); CommonResult<OAuth2OpenAuthorizeInfoRespVO> result = oauth2OpenController.authorize(clientId,null);
// 断言 // 断言
assertEquals(0, result.getCode()); assertEquals(0, result.getCode());
assertPojoEquals(client, result.getData().getClient()); assertPojoEquals(client, result.getData().getClient());
@ -218,7 +218,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
public void testApproveOrDeny_grantTypeError() { public void testApproveOrDeny_grantTypeError() {
// 调用并断言 // 调用并断言
assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null, assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null,
null, null, null, null), null, null, null, null,null),
new ErrorCode(400, "response_type 参数值只允许 code 和 token")); new ErrorCode(400, "response_type 参数值只允许 code 和 token"));
} }
@ -237,7 +237,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
// 调用 // 调用
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId, CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
scope, redirectUri, true, state); scope, redirectUri, true, state,null);
// 断言 // 断言
assertEquals(0, result.getCode()); assertEquals(0, result.getCode());
assertNull(result.getData()); assertNull(result.getData());
@ -258,7 +258,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
// 调用 // 调用
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId, CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
scope, redirectUri, false, state); scope, redirectUri, false, state,null);
// 断言 // 断言
assertEquals(0, result.getCode()); assertEquals(0, result.getCode());
assertEquals("https://www.iocoder.cn#error=access_denied&error_description=User%20denied%20access&state=test", result.getData()); assertEquals("https://www.iocoder.cn#error=access_denied&error_description=User%20denied%20access&state=test", result.getData());
@ -287,7 +287,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
// 调用 // 调用
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId, CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
scope, redirectUri, true, state); scope, redirectUri, true, state,null);
// 断言 // 断言
assertEquals(0, result.getCode()); assertEquals(0, result.getCode());
assertThat(result.getData(), anyOf( // 29 30 都有一定概率主要是时间计算 assertThat(result.getData(), anyOf( // 29 30 都有一定概率主要是时间计算
@ -319,7 +319,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
// 调用 // 调用
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId, CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
scope, redirectUri, false, state); scope, redirectUri, false, state,null);
// 断言 // 断言
assertEquals(0, result.getCode()); assertEquals(0, result.getCode());
assertEquals("https://www.iocoder.cn?code=test_code&state=test", result.getData()); assertEquals("https://www.iocoder.cn?code=test_code&state=test", result.getData());