fix: normalize channel login open ids

This commit is contained in:
hy001 2026-05-18 14:04:17 +08:00
parent 2a7be2ca92
commit 916ce628be
7 changed files with 207 additions and 62 deletions

View File

@ -224,10 +224,19 @@ public class EndpointRestController {
*/
@PostMapping("/account/login/channel")
public TokenCredentialCO login(@RequestBody @Validated UserChannelCredentialCmd cmd, HttpServletRequest request) {
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(ResponseAssert.requiredSuccess(
appUserAccountClient.channelCredential(cmd)));
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
}
try {
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(ResponseAssert.requiredSuccess(
appUserAccountClient.channelCredential(cmd)));
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
} catch (ResponseException ex) {
if (isChannelCredentialDecodeError(ex)) {
log.warn("channel login credential decode error, return USER_NOT_REGISTERED. authType={}",
cmd.getAuthType());
throw new ResponseException(UserErrorCode.USER_NOT_REGISTERED);
}
throw ex;
}
}
/**
* 账号登录.
@ -287,6 +296,13 @@ public class EndpointRestController {
|| Objects.equals(ex.getErrorCode(), UserErrorCode.SOURCE_REGISTERED.getCode());
}
private boolean isChannelCredentialDecodeError(ResponseException ex) {
return Objects.equals(ex.getErrorCode(), ResponseErrorCode.NOT_ACCEPTABLE.getCode())
&& Objects.equals(ex.getErrorCodeName(), "ERROR_INNER_DECODER")
&& Objects.toString(ex.getExtValues(), "")
.contains("/app-user/account/channel/credential");
}
private TokenCredentialCO createTokenCredential(
UserAccountDTO account,
String reqSysOrigin,

View File

@ -12,6 +12,7 @@ import com.red.circle.tool.core.text.StringUtils;
import feign.Request;
import feign.Response;
import feign.codec.ErrorDecoder;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
@ -69,11 +70,14 @@ public class FeignErrorDecoder implements ErrorDecoder {
String errorCodeName = Objects.equals(response.status(), 429)
? "REQ_LIMIT_INNER_DECODER"
: "ERROR_INNER_DECODER";
return new ResponseException(response.status(), errorCodeName, getErrorMsg(methodKey, response));
Map<String, Object> extValues = new HashMap<>();
extValues.put("innerRequest", reqMapping(response));
extValues.put("innerMethodKey", methodKey);
return new ResponseException(response.status(), errorCodeName, getErrorMsg(response), extValues);
}
private String getErrorMsg(String methodKey, Response response) {
return StringUtils.isBlankOrElse(response.reason(), "") + reqMapping(response);
private String getErrorMsg(Response response) {
return StringUtils.isBlankOrElse(response.reason(), "Inner service error");
}
private ResponseException parseResponseException(String methodKey, Response response) {
@ -103,15 +107,44 @@ public class FeignErrorDecoder implements ErrorDecoder {
}
try (InputStream inputStream = response.body().asInputStream()) {
String responseCompression = ApplicationContextUtils.getApplicationContext()
.getEnvironment()
.getProperty("spring.cloud.openfeign.compression.response.enabled");
return JacksonUtils.readValue(inputStream, new TypeReference<>() {
}, Objects.equals(responseCompression, "true"));
byte[] bodyBytes = inputStream.readAllBytes();
if (bodyBytes.length == 0) {
log.warn("Feign error response body is empty, status={}, reason={}, request={}",
response.status(), response.reason(), reqMapping(response));
return null;
}
boolean gzipEncoded = isGzipEncoded(response);
ResultResponse<Void> responseBody = parseResponseBytes(response, bodyBytes, gzipEncoded);
if (Objects.nonNull(responseBody)) {
return responseBody;
}
return parseResponseBytes(response, bodyBytes, !gzipEncoded);
} catch (IOException | RuntimeException ex) {
log.warn("Parse feign error response body failed, status={}, reason={}, request={}",
log.warn("Read feign error response body failed, status={}, reason={}, request={}",
response.status(), response.reason(), reqMapping(response), ex);
return null;
}
}
private ResultResponse<Void> parseResponseBytes(
Response response,
byte[] bodyBytes,
boolean compression) {
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bodyBytes)) {
return JacksonUtils.readValue(inputStream, new TypeReference<>() {
}, compression);
} catch (IOException | RuntimeException ex) {
log.warn("Parse feign error response body failed, status={}, reason={}, request={}, compression={}",
response.status(), response.reason(), reqMapping(response), compression, ex);
return null;
}
}
private boolean isGzipEncoded(Response response) {
return response.headers().entrySet().stream()
.filter(entry -> Objects.nonNull(entry.getKey()))
.filter(entry -> "Content-Encoding".equalsIgnoreCase(entry.getKey()))
.flatMap(entry -> entry.getValue().stream())
.anyMatch(value -> "gzip".equalsIgnoreCase(value));
}
}

View File

@ -1,11 +1,12 @@
package com.red.circle.other.inner.model.cmd.user;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.other.inner.enums.user.AuthTypeEnum;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.other.inner.enums.user.AuthTypeEnum;
import com.red.circle.tool.core.text.StringUtils;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户认证检测.
@ -31,12 +32,16 @@ public class UserChannelCredentialCmd extends AppExtCommand {
*/
@NotBlank(message = "openId required.")
private String openId;
public String openId() {
if (openId.contains("_") && !AuthTypeEnum.HUAWEI.name().equals(authType.name())) {
return openId;
}
return this.getOpenId().concat("_").concat(this.requireReqSysOrigin());
}
}
public String openId() {
if (StringUtils.isBlank(openId)) {
return openId;
}
String suffix = "_".concat(requireReqSysOrigin());
if (openId.endsWith(suffix)) {
return openId;
}
return openId.concat(suffix);
}
}

View File

@ -99,17 +99,25 @@ public class CreateAccountCmd extends AppExtCommand {
&& Objects.equals(getMobile().getPhonePrefix(), 86);
}
public void changeOpenId() {
if (!this.checkTypeMobile()) {
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
StringUtils.isNotBlank(this.openId)
&& !Objects.equals(this.openId, "null"));
}
setOpenId(this.openId.concat("_").concat(requireReqSysOrigin()));
}
public String registerOpenId() {
return checkTypeMobile() ? this.mobile.getFullPhoneNumber() : this.openId;
}
}
public void changeOpenId() {
if (!this.checkTypeMobile()) {
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
StringUtils.isNotBlank(this.openId)
&& !Objects.equals(this.openId, "null"));
}
setOpenId(openIdWithReqSysOrigin(this.openId));
}
public String registerOpenId() {
return checkTypeMobile() ? this.mobile.getFullPhoneNumber() : this.openId;
}
private String openIdWithReqSysOrigin(String sourceOpenId) {
String suffix = "_".concat(requireReqSysOrigin());
if (sourceOpenId.endsWith(suffix)) {
return sourceOpenId;
}
return sourceOpenId.concat(suffix);
}
}

View File

@ -0,0 +1,42 @@
package com.red.circle.other.inner.model.cmd.user;
import static org.junit.Assert.assertEquals;
import com.red.circle.framework.core.dto.ReqSysOrigin;
import com.red.circle.other.inner.enums.user.AuthTypeEnum;
import com.red.circle.other.inner.model.cmd.user.account.CreateAccountCmd;
import org.junit.Test;
public class UserOpenIdNormalizeTest {
@Test
public void channelLoginAppendsOriginWhenGoogleOpenIdContainsUnderscore() {
UserChannelCredentialCmd cmd = new UserChannelCredentialCmd();
cmd.setAuthType(AuthTypeEnum.GOOGLE);
cmd.setOpenId("google_open_id");
cmd.setReqSysOrigin(ReqSysOrigin.of("LIKEI"));
assertEquals("google_open_id_LIKEI", cmd.openId());
}
@Test
public void channelLoginDoesNotAppendOriginTwice() {
UserChannelCredentialCmd cmd = new UserChannelCredentialCmd();
cmd.setAuthType(AuthTypeEnum.GOOGLE);
cmd.setOpenId("google_open_id_LIKEI");
cmd.setReqSysOrigin(ReqSysOrigin.of("LIKEI"));
assertEquals("google_open_id_LIKEI", cmd.openId());
}
@Test
public void accountCreateDoesNotAppendOriginTwice() {
CreateAccountCmd cmd = new CreateAccountCmd();
cmd.setType(AuthTypeEnum.GOOGLE.name());
cmd.setOpenId("google_open_id_LIKEI");
cmd.setReqSysOrigin(ReqSysOrigin.of("LIKEI"));
cmd.changeOpenId();
assertEquals("google_open_id_LIKEI", cmd.getOpenId());
}
}

View File

@ -40,15 +40,23 @@ public class BindUserAuthTypeCmd extends AppExtCommand {
return Objects.equals(type, AuthTypeEnum.MOBILE);
}
public String getActualOpenId() {
if (!isTypeMobile() && StringUtils.isBlank(openId)) {
return "";
public String getActualOpenId() {
if (!isTypeMobile() && StringUtils.isBlank(openId)) {
return "";
}
return isTypeMobile()
? mobile.getFullPhoneNumber()
: openId.concat("_").concat(requireReqSysOrigin());
}
}
return isTypeMobile()
? mobile.getFullPhoneNumber()
: openIdWithReqSysOrigin();
}
private String openIdWithReqSysOrigin() {
String suffix = "_".concat(requireReqSysOrigin());
if (openId.endsWith(suffix)) {
return openId;
}
return openId.concat(suffix);
}
}

View File

@ -12,6 +12,7 @@ import com.red.circle.tool.core.text.StringUtils;
import feign.Request;
import feign.Response;
import feign.codec.ErrorDecoder;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
@ -69,11 +70,14 @@ public class FeignErrorDecoder implements ErrorDecoder {
String errorCodeName = Objects.equals(response.status(), 429)
? "REQ_LIMIT_INNER_DECODER"
: "ERROR_INNER_DECODER";
return new ResponseException(response.status(), errorCodeName, getErrorMsg(methodKey, response));
Map<String, Object> extValues = new HashMap<>();
extValues.put("innerRequest", reqMapping(response));
extValues.put("innerMethodKey", methodKey);
return new ResponseException(response.status(), errorCodeName, getErrorMsg(response), extValues);
}
private String getErrorMsg(String methodKey, Response response) {
return StringUtils.isBlankOrElse(response.reason(), "") + reqMapping(response);
private String getErrorMsg(Response response) {
return StringUtils.isBlankOrElse(response.reason(), "Inner service error");
}
private ResponseException parseResponseException(String methodKey, Response response) {
@ -103,15 +107,44 @@ public class FeignErrorDecoder implements ErrorDecoder {
}
try (InputStream inputStream = response.body().asInputStream()) {
String responseCompression = ApplicationContextUtils.getApplicationContext()
.getEnvironment()
.getProperty("spring.cloud.openfeign.compression.response.enabled");
return JacksonUtils.readValue(inputStream, new TypeReference<>() {
}, Objects.equals(responseCompression, "true"));
byte[] bodyBytes = inputStream.readAllBytes();
if (bodyBytes.length == 0) {
log.warn("Feign error response body is empty, status={}, reason={}, request={}",
response.status(), response.reason(), reqMapping(response));
return null;
}
boolean gzipEncoded = isGzipEncoded(response);
ResultResponse<Void> responseBody = parseResponseBytes(response, bodyBytes, gzipEncoded);
if (Objects.nonNull(responseBody)) {
return responseBody;
}
return parseResponseBytes(response, bodyBytes, !gzipEncoded);
} catch (IOException | RuntimeException ex) {
log.warn("Parse feign error response body failed, status={}, reason={}, request={}",
log.warn("Read feign error response body failed, status={}, reason={}, request={}",
response.status(), response.reason(), reqMapping(response), ex);
return null;
}
}
private ResultResponse<Void> parseResponseBytes(
Response response,
byte[] bodyBytes,
boolean compression) {
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bodyBytes)) {
return JacksonUtils.readValue(inputStream, new TypeReference<>() {
}, compression);
} catch (IOException | RuntimeException ex) {
log.warn("Parse feign error response body failed, status={}, reason={}, request={}, compression={}",
response.status(), response.reason(), reqMapping(response), compression, ex);
return null;
}
}
private boolean isGzipEncoded(Response response) {
return response.headers().entrySet().stream()
.filter(entry -> Objects.nonNull(entry.getKey()))
.filter(entry -> "Content-Encoding".equalsIgnoreCase(entry.getKey()))
.flatMap(entry -> entry.getValue().stream())
.anyMatch(value -> "gzip".equalsIgnoreCase(value));
}
}