0728 ljc 升级为OIDC版本,若出现问题可选择回退版本,version 1.0
This commit is contained in:
parent
cb22cd43c6
commit
28ce63a0e1
@ -112,4 +112,15 @@ public interface OAuth2GrantService {
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
@ -1,20 +1,29 @@
|
||||
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.security.dal.oauth2.OAuth2AccessTokenDO;
|
||||
import cd.casic.framework.security.dal.oauth2.OAuth2CodeDO;
|
||||
import cd.casic.framework.security.dal.user.AdminUserDO;
|
||||
import cd.casic.framework.security.oauth2.OAuth2CodeService;
|
||||
import cd.casic.module.system.enums.ErrorCodeConstants;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cd.casic.framework.commons.enums.UserTypeEnum;
|
||||
import cd.casic.module.system.enums.ErrorCodeConstants;
|
||||
import com.nimbusds.jose.JOSEObjectType;
|
||||
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 jakarta.annotation.Resource;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
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
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OAuth2GrantServiceImpl implements OAuth2GrantService {
|
||||
|
||||
@ -33,6 +43,8 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService {
|
||||
private OAuth2CodeService oauth2CodeService;
|
||||
@Resource
|
||||
private AdminAuthService adminAuthService;
|
||||
@Resource
|
||||
private AdminUserService adminUserService;
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType,
|
||||
@ -104,4 +116,61 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService {
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -106,4 +106,14 @@ public class OAuth2ClientDO extends BaseDO {
|
||||
*/
|
||||
private String additionalInformation;
|
||||
|
||||
/**
|
||||
* 是否支持 OIDC
|
||||
*/
|
||||
private Boolean oidcSupport = false;
|
||||
|
||||
/**
|
||||
* 默认响应类型
|
||||
*/
|
||||
private String defaultResponseType = "code";
|
||||
|
||||
}
|
||||
|
@ -75,6 +75,12 @@ public class OAuth2ClientSaveReqVO {
|
||||
@Schema(description = "附加信息", example = "{yunai: true}")
|
||||
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 格式")
|
||||
public boolean isAdditionalInformationJson() {
|
||||
return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation);
|
||||
|
@ -1,38 +1,41 @@
|
||||
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.pojo.CommonResult;
|
||||
import cd.casic.framework.commons.util.http.HttpUtils;
|
||||
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.OAuth2OpenAuthorizeInfoRespVO;
|
||||
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.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 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.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 org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -69,6 +72,8 @@ public class OAuth2OpenController {
|
||||
private OAuth2ApproveService oauth2ApproveService;
|
||||
@Resource
|
||||
private OAuth2TokenService oauth2TokenService;
|
||||
@Resource
|
||||
private AdminUserService adminUserService;
|
||||
|
||||
/**
|
||||
* 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法
|
||||
@ -181,13 +186,36 @@ public class OAuth2OpenController {
|
||||
@GetMapping("/authorize")
|
||||
@Operation(summary = "获得授权信息", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用")
|
||||
@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 实现
|
||||
|
||||
// 1. 获得 Client 客户端的信息
|
||||
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId);
|
||||
// 2. 获得用户已经授权的信息
|
||||
|
||||
// 2. 检查是否为 OIDC 请求
|
||||
boolean isOIDCRequest = isOIDCRequest(responseType, client);
|
||||
|
||||
// 3. 获得用户已经授权的信息
|
||||
List<OAuth2ApproveDO> approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId);
|
||||
|
||||
// 4. 如果是 OIDC 请求,确保 openid scope 被包含
|
||||
if (isOIDCRequest) {
|
||||
// 可以在这里添加特殊的处理逻辑
|
||||
}
|
||||
|
||||
// 拼接返回
|
||||
return success(OAuth2OpenConvert.INSTANCE.convert(client, approves));
|
||||
}
|
||||
@ -217,8 +245,10 @@ public class OAuth2OpenController {
|
||||
@RequestParam(value = "scope", required = false) String scope,
|
||||
@RequestParam("redirect_uri") String redirectUri,
|
||||
@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")
|
||||
|
||||
Map<String, Boolean> scopes = JsonUtils.parseObject(scope, Map.class);
|
||||
scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap());
|
||||
// 0. 校验用户已经登录。通过 Spring Security 实现
|
||||
@ -229,6 +259,9 @@ public class OAuth2OpenController {
|
||||
OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null,
|
||||
grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri);
|
||||
|
||||
// 检查是否为 OIDC 请求
|
||||
boolean isOIDCRequest = checkOIDCRequest(responseType, scopes);
|
||||
|
||||
// 2.1 假设 approved 为 null,说明是场景一
|
||||
if (Boolean.TRUE.equals(autoApprove)) {
|
||||
// 如果无法自动授权通过,则返回空 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);
|
||||
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));
|
||||
} 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));
|
||||
}
|
||||
|
||||
@ -294,4 +332,85 @@ public class OAuth2OpenController {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
// 可根据需要添加更多标准字段
|
||||
}
|
@ -100,4 +100,80 @@ public class OAuth2Utils {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
||||
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());
|
||||
assertPojoEquals(client, result.getData().getClient());
|
||||
@ -218,7 +218,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
||||
public void testApproveOrDeny_grantTypeError() {
|
||||
// 调用,并断言
|
||||
assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null,
|
||||
null, null, null, null),
|
||||
null, null, null, null,null),
|
||||
new ErrorCode(400, "response_type 参数值只允许 code 和 token"));
|
||||
}
|
||||
|
||||
@ -237,7 +237,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
||||
|
||||
// 调用
|
||||
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
||||
scope, redirectUri, true, state);
|
||||
scope, redirectUri, true, state,null);
|
||||
// 断言
|
||||
assertEquals(0, result.getCode());
|
||||
assertNull(result.getData());
|
||||
@ -258,7 +258,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
||||
|
||||
// 调用
|
||||
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
||||
scope, redirectUri, false, state);
|
||||
scope, redirectUri, false, state,null);
|
||||
// 断言
|
||||
assertEquals(0, result.getCode());
|
||||
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,
|
||||
scope, redirectUri, true, state);
|
||||
scope, redirectUri, true, state,null);
|
||||
// 断言
|
||||
assertEquals(0, result.getCode());
|
||||
assertThat(result.getData(), anyOf( // 29 和 30 都有一定概率,主要是时间计算
|
||||
@ -319,7 +319,7 @@ public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
|
||||
|
||||
// 调用
|
||||
CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
|
||||
scope, redirectUri, false, state);
|
||||
scope, redirectUri, false, state,null);
|
||||
// 断言
|
||||
assertEquals(0, result.getCode());
|
||||
assertEquals("https://www.iocoder.cn?code=test_code&state=test", result.getData());
|
||||
|
Loading…
x
Reference in New Issue
Block a user