Merge pull request #13347 from thingsboard/fix/auth-db-error

Handle uncaught auth exceptions
This commit is contained in:
Viacheslav Klimov 2025-05-09 13:39:53 +03:00 committed by GitHub
commit f03f41fb72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 80 additions and 11 deletions

View File

@ -47,6 +47,7 @@ import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.AuthExceptionHandler;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter; import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider;
@ -129,6 +130,9 @@ public class ThingsboardSecurityConfiguration {
@Autowired @Autowired
private RateLimitProcessingFilter rateLimitProcessingFilter; private RateLimitProcessingFilter rateLimitProcessingFilter;
@Autowired
private AuthExceptionHandler authExceptionHandler;
@Bean @Bean
protected PayloadSizeFilter payloadSizeFilter() { protected PayloadSizeFilter payloadSizeFilter() {
return new PayloadSizeFilter(maxPayloadSizeConfig); return new PayloadSizeFilter(maxPayloadSizeConfig);
@ -235,7 +239,8 @@ public class ThingsboardSecurityConfiguration {
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(payloadSizeFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(payloadSizeFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(authExceptionHandler, buildRestLoginProcessingFilter().getClass());
if (oauth2Configuration != null) { if (oauth2Configuration != null) {
http.oauth2Login(login -> login http.oauth2Login(login -> login
.authorizationEndpoint(config -> config .authorizationEndpoint(config -> config

View File

@ -437,14 +437,7 @@ public abstract class BaseController {
} else if (exception instanceof AsyncRequestTimeoutException) { } else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL); return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof DataAccessException) { } else if (exception instanceof DataAccessException) {
if (!logControllerErrorStackTrace) { // not to log the error twice return new ThingsboardException(exception, ThingsboardErrorCode.DATABASE);
log.warn("Database error: {} - {}", exception.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(exception));
}
if (cause instanceof ConstraintViolationException) {
return new ThingsboardException(ExceptionUtils.getRootCause(exception).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else {
return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL);
}
} else if (exception instanceof EntityVersionMismatchException) { } else if (exception instanceof EntityVersionMismatchException) {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT); return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT);
} }

View File

@ -21,7 +21,9 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
@ -139,6 +141,8 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
ThingsboardException thingsboardException = (ThingsboardException) exception; ThingsboardException thingsboardException = (ThingsboardException) exception;
if (thingsboardException.getErrorCode() == ThingsboardErrorCode.SUBSCRIPTION_VIOLATION) { if (thingsboardException.getErrorCode() == ThingsboardErrorCode.SUBSCRIPTION_VIOLATION) {
handleSubscriptionException((ThingsboardException) exception, response); handleSubscriptionException((ThingsboardException) exception, response);
} else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.DATABASE) {
handleDatabaseException(thingsboardException.getCause(), response);
} else { } else {
handleThingsboardException((ThingsboardException) exception, response); handleThingsboardException((ThingsboardException) exception, response);
} }
@ -148,8 +152,10 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
handleAccessDeniedException(response); handleAccessDeniedException(response);
} else if (exception instanceof AuthenticationException) { } else if (exception instanceof AuthenticationException) {
handleAuthenticationException((AuthenticationException) exception, response); handleAuthenticationException((AuthenticationException) exception, response);
} else if (exception instanceof MaxPayloadSizeExceededException) { } else if (exception instanceof MaxPayloadSizeExceededException) {
handleMaxPayloadSizeExceededException(response, (MaxPayloadSizeExceededException) exception); handleMaxPayloadSizeExceededException(response, (MaxPayloadSizeExceededException) exception);
} else if (exception instanceof DataAccessException e) {
handleDatabaseException(e, response);
} else { } else {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(), JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(),
@ -201,6 +207,17 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
JacksonUtil.fromBytes(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class)); JacksonUtil.fromBytes(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class));
} }
private void handleDatabaseException(Throwable databaseException, HttpServletResponse response) throws IOException {
ThingsboardErrorResponse errorResponse;
if (databaseException instanceof ConstraintViolationException) {
errorResponse = ThingsboardErrorResponse.of(ExceptionUtils.getRootCause(databaseException).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST);
} else {
log.warn("Database error: {} - {}", databaseException.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(databaseException));
errorResponse = ThingsboardErrorResponse.of("Database error", ThingsboardErrorCode.DATABASE, HttpStatus.INTERNAL_SERVER_ERROR);
}
writeResponse(errorResponse, response);
}
private void handleAccessDeniedException(HttpServletResponse response) throws IOException { private void handleAccessDeniedException(HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.FORBIDDEN.value()); response.setStatus(HttpStatus.FORBIDDEN.value());
JacksonUtil.writeValue(response.getWriter(), JacksonUtil.writeValue(response.getWriter(),
@ -233,4 +250,11 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
} }
} }
// TODO: refactor this class to use this method instead of boilerplate JacksonUtil.writeValue(response.getWriter(), ...
private void writeResponse(ThingsboardErrorResponse errorResponse, HttpServletResponse response) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(errorResponse.getStatus());
JacksonUtil.writeValue(response.getWriter(), errorResponse);
}
} }

View File

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
@Component
@RequiredArgsConstructor
@Slf4j
public class AuthExceptionHandler extends OncePerRequestFilter {
private final ThingsboardErrorResponseHandler errorResponseHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
try {
filterChain.doFilter(request, response);
} catch (AuthenticationException e) {
throw e;
} catch (Exception e) {
errorResponseHandler.handle(e, response);
}
}
}

View File

@ -31,7 +31,8 @@ public enum ThingsboardErrorCode {
TOO_MANY_UPDATES(34), TOO_MANY_UPDATES(34),
VERSION_CONFLICT(35), VERSION_CONFLICT(35),
SUBSCRIPTION_VIOLATION(40), SUBSCRIPTION_VIOLATION(40),
PASSWORD_VIOLATION(45); PASSWORD_VIOLATION(45),
DATABASE(46);
private int errorCode; private int errorCode;