Настройка
- Служба A → отправляет сообщение POST запрос к Службе B
- Сервису B → обрабатывает аутентификацию
- При сбое аутентификации Сервис B вызывает пользовательский RestAuthFailureHandler который создает содержательный ответ JSON (например, errorId, message и т. д.)
POST /serviceB/login HTTP/1.1 401 165
Итак:
- Код состояния HTTP — 401
- Тело ответа существует (165 байт)
- JSON правильно записан в обработчике ошибок
Проблема
Когда Служба A получает ответ:- Состояние HTTP правильно 401
- Тело ответа пусто
Эта настройка работала правильно до обновления (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
Мобильная версия