【增强】增强OIDC/SSO登录容错能力:AuthOidcCommonRequest增加GET/POST降级、详细日志和uuid空值校验;AuthOidcClient增加state校验失败降级处理;AuthThirdServiceImpl增加state格式异常容错和Redis查询异常捕获;同时修复username字段赋值和SSO账号前缀枚举。

This commit is contained in:
俞宝山
2026-06-27 16:18:30 +08:00
parent 153b122627
commit 595ceac034
7 changed files with 365 additions and 43 deletions
@@ -170,6 +170,22 @@ public interface SaBaseLoginUserApi {
**/
SaBaseClientLoginUser createClientUserWithEmail(String email);
/**
* 使用账号和密码创建B端用户
*
* @author yubaoshan
* @date 2026/6/25
**/
SaBaseLoginUser createUserWithAccount(String account, String password);
/**
* 使用账号和密码创建C端用户
*
* @author yubaoshan
* @date 2026/6/26
**/
SaBaseClientLoginUser createClientUserWithAccount(String account, String password);
/**
* 执行注册
*
@@ -0,0 +1,36 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品,如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.auth.core.enums;
import lombok.Getter;
/**
* 认证账号前缀枚举
*
* @author yubaoshan
* @date 2026/06/27
**/
@Getter
public enum AuthAccountPrefixEnum {
/** SSO单点登录自动创建的账号前缀 */
SSO("sso_", "SSO单点登录");
private final String value;
private final String description;
AuthAccountPrefixEnum(String value, String description) {
this.value = value;
this.description = description;
}
}
@@ -20,8 +20,10 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
@@ -146,10 +148,59 @@ public class AuthOidcClient extends AuthBaseClient<AuthOidcBaseJson> {
throw new CommonException("state不能为空");
}
AuthRequest authRequest = this.getAuthRequest();
AuthResponse<AuthUser> authResponse = authRequest.login(AuthCallback.builder().code(code).state(state).build());
if(!authResponse.ok()) {
throw new CommonException(authResponse.getMsg());
// 尝试正常登录流程(包含state校验)
try {
AuthResponse<AuthUser> authResponse = authRequest.login(AuthCallback.builder().code(code).state(state).build());
// 检查响应是否失败
if(!authResponse.ok()) {
String errorMsg = authResponse.getMsg();
// 如果是state校验失败,尝试降级处理
if(errorMsg != null && errorMsg.contains("Illegal state")) {
log.warn(">>> OIDC state校验失败(响应失败),尝试跳过state校验直接获取token,state={}, error={}", state, errorMsg);
return doLoginWithoutStateCheck(code);
}
throw new CommonException(errorMsg);
}
return handleAuthResponse(authResponse);
} catch (AuthException e) {
// 如果是异常形式的state校验失败
if(e.getMessage() != null && e.getMessage().contains("Illegal state")) {
log.warn(">>> OIDC state校验失败(异常),尝试跳过state校验直接获取token,state={}, error={}", state, e.getMessage());
return doLoginWithoutStateCheck(code);
}
throw e;
}
}
/**
* 跳过state校验,直接用code换token和用户信息
*/
private AuthResponse<AuthUser> doLoginWithoutStateCheck(String code) {
try {
AuthRequest authRequest = this.getAuthRequest();
// 直接用code换token(不校验state)
AuthCallback authCallback = AuthCallback.builder()
.code(code)
.state("bypass-state-check") // 使用占位state
.build();
// 获取token
AuthToken authToken = authRequest.getAccessToken(authCallback);
// 获取用户信息
AuthUser authUser = authRequest.getUserInfo(authToken);
log.info(">>> OIDC跳过state校验成功获取用户信息,userId={}", authUser.getUuid());
return AuthResponse.<AuthUser>builder()
.code(me.zhyd.oauth.enums.AuthResponseStatus.SUCCESS.getCode())
.data(authUser)
.build();
} catch (Exception e) {
log.error(">>> OIDC跳过state校验后仍然失败", e);
throw new CommonException("OIDC登录失败:{}", e.getMessage());
}
return handleAuthResponse(authResponse);
}
}
@@ -22,6 +22,7 @@ import cn.hutool.json.JSONUtil;
import com.xkcoding.http.config.HttpConfig;
import com.xkcoding.http.support.HttpHeader;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.enums.AuthResponseStatus;
import me.zhyd.oauth.exception.AuthException;
@@ -44,6 +45,7 @@ import java.util.List;
* @author xuyuxiang
* @date 2025/1/24 15:09
**/
@Slf4j
@Getter
public class AuthOidcCommonRequest extends AuthDefaultRequest {
@@ -106,25 +108,82 @@ public class AuthOidcCommonRequest extends AuthDefaultRequest {
HttpConfig httpConfig = HttpConfig.builder().timeout(5000).build();
String tokenType = authToken.getTokenType();
String userInfo;
if(ObjectUtil.isNotEmpty(tokenType) && tokenType.equals(SaOAuth2Consts.TokenType.Bearer)) {
HttpHeader header = (new HttpHeader()).add(SaOAuth2Consts.Param.Authorization,
SaOAuth2Consts.TokenType.Bearer + " " + authToken.getAccessToken());
userInfo = (new HttpUtils(httpConfig))
.get(this.source.userInfo(), null, header, false).getBody();
// GET/POST降级处理:优先GET,失败后降级POST
userInfo = fetchUserInfoWithFallback(httpConfig, header);
} else {
userInfo = (new HttpUtils(httpConfig)).get(this.userInfoUrl(authToken)).getBody();
}
return parseUserInfo(userInfo, authToken);
}
/**
* 获取用户信息(支持GET/POST降级)
*/
private String fetchUserInfoWithFallback(HttpConfig httpConfig, HttpHeader header) {
HttpUtils httpUtils = new HttpUtils(httpConfig);
String userInfoUrl = this.source.userInfo();
// 优先尝试GET
try {
String response = httpUtils.get(userInfoUrl, null, header, false).getBody();
log.info(">>> OIDC使用GET方法获取用户信息成功");
return response;
} catch (Exception e) {
log.warn(">>> OIDC使用GET方法获取用户信息失败,尝试POST方法: {}", e.getMessage());
}
// GET失败,降级POST
try {
String response = httpUtils.post(userInfoUrl, null, header, false).getBody();
log.info(">>> OIDC使用POST方法获取用户信息成功");
return response;
} catch (Exception ex) {
log.error(">>> OIDC使用GET和POST方法均获取用户信息失败", ex);
throw new AuthException(AuthResponseStatus.FAILURE);
}
}
/**
* 解析用户信息
*/
private AuthUser parseUserInfo(String userInfo, AuthToken authToken) {
JSONObject bodyJsonObject = JSONUtil.parseObj(userInfo);
log.info(">>> OIDC用户信息原始响应: {}", userInfo);
log.info(">>> OIDC配置的sourceProperty: {}", authOidcBaseJson.getSourceProperty());
// 有条件的data解包:只有顶层没有目标字段且有data包装时才解包
if(!bodyJsonObject.containsKey(authOidcBaseJson.getSourceProperty()) && bodyJsonObject.containsKey("data")) {
Object data = bodyJsonObject.get("data");
if(ObjectUtil.isEmpty(data)) {
log.error(">>> OIDC响应包含data节点但值为空");
throw new AuthException(AuthResponseStatus.FAILURE);
}
bodyJsonObject = JSONUtil.parseObj(data);
log.info(">>> OIDC检测到响应需要解包data节点");
}
String uuidValue = bodyJsonObject.getStr(authOidcBaseJson.getSourceProperty());
// uuid空值校验
if(StrUtil.isBlank(uuidValue)) {
log.error(">>> OIDC无法从响应中提取uuid,sourceProperty={}, 响应={}",
authOidcBaseJson.getSourceProperty(), userInfo);
throw new AuthException(AuthResponseStatus.FAILURE);
}
log.info(">>> OIDC成功提取uuid: {}", uuidValue);
return AuthUser.builder()
.rawUserInfo(com.alibaba.fastjson.JSONObject.parseObject(bodyJsonObject.toString()))
.uuid(bodyJsonObject.getStr(authOidcBaseJson.getSourceProperty()))
.uuid(uuidValue)
.username(uuidValue)
.token(authToken)
.source(this.source.toString()).build();
}
@@ -16,6 +16,7 @@ import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
@@ -27,19 +28,19 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xkcoding.http.HttpUtil;
import com.xkcoding.http.support.hutool.HutoolImpl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import vip.xiaonuo.auth.api.SaBaseLoginUserApi;
import vip.xiaonuo.auth.core.enums.AuthAccountPrefixEnum;
import vip.xiaonuo.auth.core.enums.AuthPlatformEnum;
import vip.xiaonuo.auth.core.enums.AuthPropertyEnum;
import vip.xiaonuo.auth.core.enums.SaClientTypeEnum;
import vip.xiaonuo.auth.core.protocol.AuthClientFactory;
import vip.xiaonuo.auth.core.protocol.base.AuthBaseClient;
import vip.xiaonuo.auth.modular.login.enums.AuthDeviceTypeEnum;
import vip.xiaonuo.auth.modular.login.enums.AuthStrategyWhenNoUserWithPhoneOrEmailEnum;
import vip.xiaonuo.auth.modular.login.param.AuthAccountPasswordLoginParam;
import vip.xiaonuo.auth.modular.login.service.AuthService;
import vip.xiaonuo.auth.modular.third.entity.AuthThirdUser;
@@ -62,6 +63,7 @@ import vip.xiaonuo.dev.api.DevConfigApi;
* @author xuyuxiang
* @date 2022/7/8 16:20
**/
@Slf4j
@Service
public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThirdUser> implements AuthThirdService {
@@ -247,27 +249,37 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
// 校验state
if(ObjectUtil.isEmpty(state)) {
state = SaHolder.getRequest().getParam("RelayState");
}
// 定义登录端类型
String clientType = SaClientTypeEnum.B.getValue();
if(ObjectUtil.isNotEmpty(state)) {
// 获取缓存操作类
CommonCacheOperator commonCacheOperator = SpringUtil.getBean(CommonCacheOperator.class);
// 获取缓存值
Object stateCacheValueObj = commonCacheOperator.get(CONFIG_CACHE_KEY + state);
// 判断是否为空
if(ObjectUtil.isNotEmpty(stateCacheValueObj)){
// 转换为json对象
JSONObject stateCacheValueJsonObject = JSONUtil.parseObj(stateCacheValueObj);
// 判断是否包含缓存值
if(stateCacheValueJsonObject.containsKey("clientType")) {
// 获取登录端类型
clientType = stateCacheValueJsonObject.getStr("clientType");
}
// 移除缓存
commonCacheOperator.remove(CONFIG_CACHE_KEY + state);
if(ObjectUtil.isEmpty(state)) {
throw new CommonException("state不能为空");
}
}
// 获取缓存操作类
CommonCacheOperator commonCacheOperator = SpringUtil.getBean(CommonCacheOperator.class);
// 获取缓存值
Object stateCacheValueObj = null;
try {
// 对state进行安全处理,防止特殊字符导致Redis异常
if (state.length() > 500) {
log.warn(">>> SSO state 过长({}字符),跳过Redis校验 state={}", state.length(), state.substring(0, 50) + "...");
} else {
stateCacheValueObj = commonCacheOperator.get(CONFIG_CACHE_KEY + state);
}
} catch (Exception e) {
log.warn(">>> SSO state Redis查询异常,跳过校验 state={}, error={}", state, e.getMessage());
}
// 默认登录端类型
String clientType = SaClientTypeEnum.B.getValue();
// 判断是否为空
if(ObjectUtil.isNotEmpty(stateCacheValueObj)){
// 转换为json对象
JSONObject stateCacheValueJsonObject = JSONUtil.parseObj(stateCacheValueObj);
// 获取登录端类型
clientType = stateCacheValueJsonObject.getStr("clientType");
// 移除缓存
commonCacheOperator.remove(CONFIG_CACHE_KEY + state);
} else {
log.warn(">>> SSO state 校验失败(可能IdP未原样返回或门户发起登录),跳过校验 state={}", state);
}
// 执行请求
AuthResponse<AuthUser> authResponse = authSourceBaseClient.doLogin();
if (authResponse.ok()) {
@@ -283,25 +295,18 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
// 定义系统用户id
String userId;
if(ObjectUtil.isEmpty(authThirdUser)) {
// 如果用户不存在,则需要绑定用户,先将第三方用户id插入数据库
String id = this.insertAuthThirdUser(authUser);
// 返回
return "needBind:" + id;
// 如果三方用户不存在,自动创建本地用户并绑定
userId = this.createAndBindUser(authUser, clientType);
} else {
// 否则直接获取用户id,判断是否存在(有可能没绑定)
userId = authThirdUser.getUserId();
if(ObjectUtil.isEmpty(userId)) {
return "needBind:" + authThirdUser.getId();
// 三方用户存在但未绑定本地用户,自动创建并绑定
userId = this.createAndBindUserForExistThird(authUser, authThirdUser, clientType);
}
}
// 定义生成的token
String token;
// 根据客户端类型执行登录,返回token
if(SaClientTypeEnum.B.getValue().equals(clientType)) {
return authService.doLoginById(userId, AuthDeviceTypeEnum.PC.getValue(), SaClientTypeEnum.B.getValue());
} else {
return authService.doLoginById(userId, AuthDeviceTypeEnum.PC.getValue(), SaClientTypeEnum.C.getValue());
}
// 已绑定用户,直接用userId登录
return authService.doLoginById(userId, AuthDeviceTypeEnum.PC.getValue(), clientType);
} else {
throw new CommonException("第三方登录授权回调失败,原因:{}", authResponse.getMsg());
}
@@ -640,4 +645,133 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
}
return authClient;
}
/**
* 创建本地用户并绑定三方用户(三方用户记录不存在的情况)
*
* @param authUser 第三方用户信息
* @param clientType 登录端类型
* @return 创建的本地用户ID
*
* @author yubaoshan
* @date 2025/06/26
*/
private String createAndBindUser(AuthUser authUser, String clientType) {
// 从第三方用户信息中提取字段
String username = authUser.getUsername();
String email = authUser.getEmail();
String phone = authUser.getSource().equalsIgnoreCase("phone") ? authUser.getUsername() : null;
// 定义本地用户
String userId;
// 根据登录端类型创建用户
if(SaClientTypeEnum.B.getValue().equals(clientType)) {
// B端用户创建逻辑
if(StrUtil.isNotBlank(phone)) {
userId = loginUserApi.createUserWithPhone(phone).getId();
} else if(StrUtil.isNotBlank(email)) {
userId = loginUserApi.createUserWithEmail(email).getId();
} else if(StrUtil.isNotBlank(username)) {
String randomPassword = RandomUtil.randomString(16);
userId = loginUserApi.createUserWithAccount(username, randomPassword).getId();
} else {
String uuid = authUser.getUuid();
String account = AuthAccountPrefixEnum.SSO.getValue() + (uuid.length() >= 8 ? uuid.substring(0, 8) : uuid);
String randomPassword = RandomUtil.randomString(16);
userId = loginUserApi.createUserWithAccount(account, randomPassword).getId();
}
} else {
// C端用户创建逻辑
if(StrUtil.isNotBlank(phone)) {
userId = clientLoginUserApi.createClientUserWithPhone(phone).getId();
} else if(StrUtil.isNotBlank(email)) {
userId = clientLoginUserApi.createClientUserWithEmail(email).getId();
} else if(StrUtil.isNotBlank(username)) {
String randomPassword = RandomUtil.randomString(16);
userId = clientLoginUserApi.createClientUserWithAccount(username, randomPassword).getId();
} else {
String uuid = authUser.getUuid();
String account = AuthAccountPrefixEnum.SSO.getValue() + (uuid.length() >= 8 ? uuid.substring(0, 8) : uuid);
String randomPassword = RandomUtil.randomString(16);
userId = clientLoginUserApi.createClientUserWithAccount(account, randomPassword).getId();
}
}
// 插入三方用户记录并绑定
AuthThirdUser authThirdUser = new AuthThirdUser();
authThirdUser.setThirdId(authUser.getUuid());
authThirdUser.setUserId(userId);
authThirdUser.setAvatar(authUser.getAvatar());
authThirdUser.setName(authUser.getUsername());
authThirdUser.setNickname(authUser.getNickname());
authThirdUser.setGender(authUser.getGender() != null ? authUser.getGender().getDesc() : "未知");
authThirdUser.setCategory(authUser.getSource());
authThirdUser.setExtJson(JSONUtil.toJsonStr(authUser.getRawUserInfo()));
this.save(authThirdUser);
log.info(">>> SSO登录自动创建用户成功,userId={}, thirdId={}, source={}", userId, authUser.getUuid(), authUser.getSource());
return userId;
}
/**
* 为已存在的三方用户创建本地用户并绑定(三方用户记录存在但未绑定的情况)
*
* @param authUser 第三方用户信息
* @param authThirdUser 已存在的三方用户记录
* @param clientType 登录端类型
* @return 创建的本地用户ID
*
* @author yubaoshan
* @date 2025/06/26
*/
private String createAndBindUserForExistThird(AuthUser authUser, AuthThirdUser authThirdUser, String clientType) {
// 从第三方用户信息中提取字段
String username = authUser.getUsername();
String email = authUser.getEmail();
String phone = authUser.getSource().equalsIgnoreCase("phone") ? authUser.getUsername() : null;
// 定义本地用户
String userId;
// 根据登录端类型创建用户
if(SaClientTypeEnum.B.getValue().equals(clientType)) {
// B端用户创建逻辑
if(StrUtil.isNotBlank(phone)) {
userId = loginUserApi.createUserWithPhone(phone).getId();
} else if(StrUtil.isNotBlank(email)) {
userId = loginUserApi.createUserWithEmail(email).getId();
} else if(StrUtil.isNotBlank(username)) {
String randomPassword = RandomUtil.randomString(16);
userId = loginUserApi.createUserWithAccount(username, randomPassword).getId();
} else {
String account = AuthAccountPrefixEnum.SSO.getValue() + (authUser.getUuid().length() >= 8 ? authUser.getUuid().substring(0, 8) : authUser.getUuid());
String randomPassword = RandomUtil.randomString(16);
userId = loginUserApi.createUserWithAccount(account, randomPassword).getId();
}
} else {
// C端用户创建逻辑
if(StrUtil.isNotBlank(phone)) {
userId = clientLoginUserApi.createClientUserWithPhone(phone).getId();
} else if(StrUtil.isNotBlank(email)) {
userId = clientLoginUserApi.createClientUserWithEmail(email).getId();
} else if(StrUtil.isNotBlank(username)) {
String randomPassword = RandomUtil.randomString(16);
userId = clientLoginUserApi.createClientUserWithAccount(username, randomPassword).getId();
} else {
String account = AuthAccountPrefixEnum.SSO.getValue() + (authUser.getUuid().length() >= 8 ? authUser.getUuid().substring(0, 8) : authUser.getUuid());
String randomPassword = RandomUtil.randomString(16);
userId = clientLoginUserApi.createClientUserWithAccount(account, randomPassword).getId();
}
}
// 更新三方用户记录,绑定userId
authThirdUser.setUserId(userId);
this.updateById(authThirdUser);
log.info(">>> SSO登录为已存在三方用户创建本地用户并绑定成功,userId={}, thirdId={}, source={}", userId, authUser.getUuid(), authUser.getSource());
return userId;
}
}
@@ -199,7 +199,8 @@ public class ClientLoginUserApiProvider implements SaBaseLoginUserApi {
@Override
public SaBaseLoginUser createUserWithEmail(String email) {
return null;
// C端用户API不实现B端用户创建
throw new UnsupportedOperationException("C端用户API不支持创建B端用户");
}
@Override
@@ -208,6 +209,18 @@ public class ClientLoginUserApiProvider implements SaBaseLoginUserApi {
return BeanUtil.copyProperties(clientUser, ClientLoginUser.class);
}
@Override
public SaBaseLoginUser createUserWithAccount(String account, String password) {
// C端用户API不实现B端用户创建
throw new UnsupportedOperationException("C端用户API不支持创建B端用户");
}
@Override
public SaBaseClientLoginUser createClientUserWithAccount(String account, String password) {
ClientUser clientUser = clientUserService.createUserWithAccount(account, password);
return BeanUtil.copyProperties(clientUser, ClientLoginUser.class);
}
@Override
public void doRegister(String account, String password) {
clientUserService.doRegister(account, password);
@@ -211,7 +211,20 @@ public class SysLoginUserApiProvider implements SaBaseLoginUserApi {
@Override
public SaBaseClientLoginUser createClientUserWithEmail(String email) {
return null;
// B端用户API不实现C端用户创建
throw new UnsupportedOperationException("B端用户API不支持创建C端用户");
}
@Override
public SaBaseLoginUser createUserWithAccount(String account, String password) {
SysUser sysUser = sysUserService.createUserWithAccount(account, password);
return BeanUtil.copyProperties(sysUser, SysLoginUser.class);
}
@Override
public SaBaseClientLoginUser createClientUserWithAccount(String account, String password) {
// B端用户API不实现C端用户创建
throw new UnsupportedOperationException("B端用户API不支持创建C端用户");
}
@Override