Spring Boot 3.x – тело ответа 401 пусто при получении другой службойJAVA

Программисты JAVA общаются здесь
Ответить
Anonymous
 Spring Boot 3.x – тело ответа 401 пусто при получении другой службой

Сообщение Anonymous »

Мы обновляем Spring Boot с 2.7.0 до 3.5.6 и столкнулись с проблемой ошибочных ответов между двумя службами.

Настройка

  • Служба A → отправляет сообщение POST запрос к Службе B
  • Сервису B → обрабатывает аутентификацию
  • При сбое аутентификации Сервис B вызывает пользовательский RestAuthFailureHandler который создает содержательный ответ JSON (например, errorId, message и т. д.)
Пример ответа от Службы Б:
POST /serviceB/login HTTP/1.1 401 165

Итак:
  • Код состояния HTTP — 401
  • Тело ответа существует (165 байт)
  • JSON правильно записан в обработчике ошибок

Проблема

Когда Служба A получает ответ:
  • Состояние HTTP правильно 401
  • Тело ответа пусто
Из-за этого Служба A не может отобразить конкретное сообщение об ошибке и вместо этого показывает общую ошибку.
Эта настройка работала правильно до обновления (Spring Boot 2.7.x).

Вопрос

После обновления до Spring Boot 3.x:
  • Почему тело ответа 401 Unauthorized ответ недоступен для вызывающей службы?
  • Связано ли это с изменениями в Spring Security 6, фильтрах или обработке ответов?
  • Необходимы ли какие-либо дополнительные настройки для обеспечения распространения тела ответа в случае ошибок аутентификации?
Будем очень признательны за любые подсказки и примеры.
ServiceB
@Component
public class RestAuthFailureHandler implements AuthenticationFailureHandler {

private static final Logger log = LoggerFactory.getLogger(RestAuthFailureHandler.class);

@Autowired
private ObjectMapper mapper;

@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException {
log.info("Entered onAuthenticationFailure");

boolean committed = response.isCommitted();
log.info("Response committed state: {}", committed);

log.info("Exception type: {}, message: {}", exception.getClass().getName(), exception.getMessage());

if (committed) {
log.warn("Response already committed, unable to write error body");
return;
}

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");

ErrorInfo errorInfo;
if (exception instanceof RestAuthenticationException restAuthenticationException) {
errorInfo = restAuthenticationException.getErrorInfo();
log.info("Using ErrorInfo from RestAuthenticationException: {}", errorInfo);
} else {
errorInfo = new ErrorInfo(
ErrorCategory.APPLICATION_ERROR.name(),
UserAPIErrorCode.AUTHENTICATION.code(),
exception.getMessage()
);
log.info("Constructed new ErrorInfo: {}", errorInfo);
}

try {
log.info("Writing ErrorInfo to response");
mapper.writeValue(response.getWriter(), errorInfo);
response.getWriter().flush();
log.info("Successfully wrote ErrorInfo to response");
} catch (IOException e) {
log.error("IOException while writing ErrorInfo to response", e);
throw e;
}
}
}

ServiceB — конфигурация безопасности
// First formLogin() → /login
http.formLogin(form -> form
.loginProcessingUrl(LOGIN_PATH)
.usernameParameter("username")
.passwordParameter("password")
.successHandler(restAuthSuccessHandler)
.failureHandler(restAuthFailureHandler)
.permitAll()
);

СервисА
@Bean
@Primary
public RestTemplate restTemplate() {
return createRestTemplate(null);
}

@Bean
public RestTemplate proxiedRestTemplate() {
if (StringUtils.isNotBlank(proxyHost) && StringUtils.isNotBlank(proxyPort)) {
final Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, Integer.valueOf(proxyPort)));
return createRestTemplate(proxy);
} else {
return createRestTemplate(null);
}
}

private RestTemplate createRestTemplate(final Proxy proxy) {
RestTemplate restTemplate = new RestTemplate();
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setProxy(proxy);
requestFactory.setReadTimeout(readTimeOut);
requestFactory.setConnectTimeout(connectionTimeOut);
BufferingClientHttpRequestFactory bufferingClientHttpRequestFactory = new BufferingClientHttpRequestFactory(requestFactory);
restTemplate.setRequestFactory(bufferingClientHttpRequestFactory);
return restTemplate;
}

@Service
public class StatelessExchangeServiceImpl implements StatelessExchangeService {

private final Logger logger = LogManager.getLogger(this.getClass());

@Autowired
private JsonUtils jsonUtils;

public ResponseEntity exchange(final RestTemplate restTemplate,
final HttpMethod httpMethod,
final String serviceBaseUrl,
final String restEndpointUrl,
final Optional body,
final Optional requestParams,
final Class responseType,
final Map headersToOverride) {

HttpServletRequest request = null;

// This check added for the requests coming from session destroyed.
if (RequestContextHolder.getRequestAttributes() != null) {
request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
logger.debug("Request uri is {}", request.getRequestURI());
HeaderUtils.logHeaders(request.getRequestURI() , request);
}

HttpHeaders httpHeaders = new HttpHeaders();
if (request != null) {
httpHeaders = HeaderUtils.mapHeaders(request);
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
}

String url = serviceBaseUrl + restEndpointUrl;

final URI uri = addRequestParamsToUrl( url, requestParams );

logger.debug("Generated routing service URI: {}", uri);

HttpEntity requestEntity;
HeaderUtils.overrideHeaders(httpHeaders, headersToOverride);
if (body.isPresent()) {
requestEntity = new HttpEntity(body.get(), httpHeaders);
}
else{
requestEntity = new HttpEntity(null, httpHeaders);
}

final ResponseEntity responseEntity;
try {
logger.debug("Calling uri: {}, with method: {}, and headers: {}", uri.toString(), httpMethod.name(), requestEntity.getHeaders());
responseEntity = restTemplate.exchange(uri, httpMethod, requestEntity, responseType);
logger.debug("Response headers for uri: {}, are: ", uri.toString(), responseEntity.getHeaders());
}
catch (HttpClientErrorException hcee) {
logger.info("Internal call to rest service {} returned with status code: {}, message: {}", uri, hcee.getStatusCode().value(), hcee.getResponseBodyAsString(), hcee);
logger.info("Status={}, ResponseHeaders={}, Body='{}'",
hcee.getStatusCode(),
hcee.getResponseHeaders(),
hcee.getResponseBodyAsString()
);
if(!jsonUtils.isValidJson(hcee.getResponseBodyAsString())) {
ErrorInfo errorInfo = ErrorInfo.internalServiceError();
logger.info("ErrorId: {}, Internal call to rest service {} returned with status code: {}, message: {}", errorInfo.getErrorId(), uri, hcee.getStatusCode().value(), hcee.getResponseBodyAsString(), hcee);
throw new InternalRestCallException(HttpStatus.INTERNAL_SERVER_ERROR.value(), jsonUtils.toJson(errorInfo).get());
}
throw new InternalRestCallException(hcee.getStatusCode().value(), hcee.getResponseBodyAsString());
}
catch(HttpServerErrorException hsee){
ErrorInfo errorInfo = ErrorInfo.internalServiceError();
logger.info("ErrorId: {}, Internal call to rest service {} failed with status code: {}, message: {}", errorInfo.getErrorId(), uri, hsee.getStatusCode().value(), hsee.getResponseBodyAsString(), hsee);
throw new InternalRestCallException(hsee.getStatusCode().value(), jsonUtils.toJson(errorInfo).get());
}
catch (RestClientException rce) {
ErrorInfo errorInfo = ErrorInfo.internalServiceError();
logger.info("ErrorId: {}, Internal call to rest service {} failed", errorInfo.getErrorId(), uri, rce);
throw new InternalRestCallException(HttpStatus.INTERNAL_SERVER_ERROR.value(), jsonUtils.toJson(errorInfo).get());
}

logger.debug("Response status for uri: {} is {}", uri.toString(), responseEntity.getStatusCode().toString());

return responseEntity;

}

public URI addRequestParamsToUrl( final String url, final Optional requestParams ) {
final UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(url);
if (requestParams.isPresent()) {
final Map params = requestParams.get();
if (!params.isEmpty()) {
final LinkedMultiValueMap paramsMap = new LinkedMultiValueMap(params.size());
paramsMap.setAll(params);
uriBuilder.queryParams(paramsMap);
}
}

return uriBuilder.build().encode().toUri();
}
}

// Logs from service B where we are setting the response message in case of auth failure
{
"instant": {
"epochSecond": 1765986931,
"nanoOfSecond": 110147101
},
"thread": "https-jsse-nio-8443-exec-7",
"level": "INFO",
"loggerName": "com.zzz.sso.authentication.handler.RestAuthFailureHandler",
"message": "Using ErrorInfo from RestAuthenticationException: zzz.da.common.utils.error.ErrorInfo@2b5da880[category=APPLICATION_ERROR,code=not.authenticated,errorId=95bb4498-acac-411f-a574-7603b352da13,message=Your email address or password is incorrect.]",
"endOfBatch": false,
"loggerFqcn": "org.apache.logging.slf4j.Log4jLogger",
"threadId": 49,
"threadPriority": 5,
"source": {
"class": "com.zzz.sso.authentication.handler.RestAuthFailureHandler",
"method": "onAuthenticationFailure",
"file": "RestAuthFailureHandler.java",
"line": 52
}
}

//below are logs from serviceA where no body came through from serviceB.
"thread": "https-jsse-nio-8443-exec-3",
"level": "INFO",
"loggerName": "com.zzz.myaccount.service.impl.StatelessExchangeServiceImpl",
"message": "Internal call to rest service https://das-sso-api.qa.zzzdigitalapi.co ... tion/login returned with status code: 401, message: ",
"thrown": {
"message": "401 on POST request for \"https://das-sso-api.qa.zzzdigitalapi.co ... tion/login\": [no body]",
"name": "org.springframework.web.client.HttpClientErrorException.Unauthorized",
"extendedStackTrace": [
{
"class": "org.springframework.web.client.HttpClientErrorException",
"method": "create",
"file": "HttpClientErrorException.java",
"line": 106
},


Подробнее здесь: https://stackoverflow.com/questions/798 ... er-service
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «JAVA»