added spring security for http transport, added large RequestSizeFilter for http transport

This commit is contained in:
dashevchenko 2024-07-31 16:46:18 +03:00
parent 64df0e16ff
commit 02f7001102
9 changed files with 120 additions and 46 deletions

View File

@ -17,6 +17,7 @@ package org.thingsboard.server.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
@ -56,6 +57,7 @@ import org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2Autho
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter; import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
import org.thingsboard.server.transport.http.config.RequestSizeFilter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -82,6 +84,11 @@ public class ThingsboardSecurityConfiguration {
public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";
public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download"; public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download";
@Value("${server.http.max_payload_size:16777216}")
private int maxPayloadSize;
@Value("${transport.http.max_payload_size:65536}")
private int httpTransportMaxPayloadSize;
@Autowired @Autowired
private ThingsboardErrorResponseHandler restAccessDeniedHandler; private ThingsboardErrorResponseHandler restAccessDeniedHandler;
@ -124,8 +131,15 @@ public class ThingsboardSecurityConfiguration {
@Autowired @Autowired
private RateLimitProcessingFilter rateLimitProcessingFilter; private RateLimitProcessingFilter rateLimitProcessingFilter;
@Autowired @Bean
private RequestSizeFilter requestSizeFilter; protected RequestSizeFilter httpTransportRequestSizeFilter() {
return new RequestSizeFilter(httpTransportMaxPayloadSize);
}
@Bean
protected RequestSizeFilter requestSizeFilter() {
return new RequestSizeFilter(maxPayloadSize);
}
@Bean @Bean
protected FilterRegistrationBean<ShallowEtagHeaderFilter> buildEtagFilter() throws Exception { protected FilterRegistrationBean<ShallowEtagHeaderFilter> buildEtagFilter() throws Exception {
@ -203,6 +217,20 @@ public class ThingsboardSecurityConfiguration {
} }
@Bean @Bean
@Order(1)
SecurityFilterChain httpTransportFilterChain(HttpSecurity http) throws Exception {
http
.securityMatchers(matchers -> matchers.requestMatchers(DEVICE_API_ENTRY_POINT))
.cors(cors -> {})
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(config -> config
.requestMatchers(DEVICE_API_ENTRY_POINT).permitAll())
.addFilterBefore(httpTransportRequestSizeFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers http.headers(headers -> headers
.cacheControl(config -> {}) .cacheControl(config -> {})
@ -214,7 +242,6 @@ public class ThingsboardSecurityConfiguration {
.authorizeHttpRequests(config -> config .authorizeHttpRequests(config -> config
.requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points (webjars included) .requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points (webjars included)
.requestMatchers( .requestMatchers(
DEVICE_API_ENTRY_POINT, // Device HTTP Transport API
FORM_BASED_LOGIN_ENTRY_POINT, // Login end-point FORM_BASED_LOGIN_ENTRY_POINT, // Login end-point
PUBLIC_LOGIN_ENTRY_POINT, // Public login end-point PUBLIC_LOGIN_ENTRY_POINT, // Public login end-point
TOKEN_REFRESH_ENTRY_POINT, // Token refresh end-point TOKEN_REFRESH_ENTRY_POINT, // Token refresh end-point
@ -228,7 +255,7 @@ public class ThingsboardSecurityConfiguration {
.addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(requestSizeFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(requestSizeFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
if (oauth2Configuration != null) { if (oauth2Configuration != null) {
http.oauth2Login(login -> login http.oauth2Login(login -> login

View File

@ -50,6 +50,10 @@ server:
key_alias: "${SSL_KEY_ALIAS:tomcat}" key_alias: "${SSL_KEY_ALIAS:tomcat}"
# Password used to access the key # Password used to access the key
key_password: "${SSL_KEY_PASSWORD:thingsboard}" key_password: "${SSL_KEY_PASSWORD:thingsboard}"
# HTTP settings
http:
# Maximum request size
max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:16777216}" # max payload size in bytes
# HTTP/2 support (takes effect only if server SSL is enabled) # HTTP/2 support (takes effect only if server SSL is enabled)
http2: http2:
# Enable/disable HTTP/2 support # Enable/disable HTTP/2 support
@ -959,7 +963,7 @@ transport:
request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}"
# HTTP maximum request processing timeout in milliseconds # HTTP maximum request processing timeout in milliseconds
max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}" max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}"
# Maximum request size # HTTP maximum request size
max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:65536}" # max payload size in bytes max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:65536}" # max payload size in bytes
# Local MQTT transport parameters # Local MQTT transport parameters
mqtt: mqtt:

View File

@ -40,7 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
"transport.http.max_payload_size=10000" "server.http.max_payload_size=10000"
}) })
public class RpcControllerTest extends AbstractControllerTest { public class RpcControllerTest extends AbstractControllerTest {

View File

@ -15,9 +15,15 @@
*/ */
package org.thingsboard.server.common.msg.tools; package org.thingsboard.server.common.msg.tools;
import lombok.Getter;
public class MaxPayloadSizeExceededException extends RuntimeException { public class MaxPayloadSizeExceededException extends RuntimeException {
public MaxPayloadSizeExceededException() { @Getter
super("Payload size exceeds the limit"); private final int limit;
public MaxPayloadSizeExceededException(int limit) {
super("Payload size exceeds the limit " + limit);
this.limit = limit;
} }
} }

View File

@ -45,6 +45,10 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>

View File

@ -136,9 +136,6 @@ public class DeviceApiController implements TbTransportService {
private static final String ACCESS_TOKEN_PARAM_DESCRIPTION = "Your device access token."; private static final String ACCESS_TOKEN_PARAM_DESCRIPTION = "Your device access token.";
@Value("${transport.http.max_payload_size:65536}")
private int maxPayloadSize;
@Autowired @Autowired
private HttpTransportContext transportContext; private HttpTransportContext transportContext;
@ -289,7 +286,6 @@ public class DeviceApiController implements TbTransportService {
@PathVariable("requestId") Integer requestId, @PathVariable("requestId") Integer requestId,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Reply to the RPC request, JSON. For example: {\"status\":\"success\"}", required = true) @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Reply to the RPC request, JSON. For example: {\"status\":\"success\"}", required = true)
@RequestBody String json, HttpServletRequest httpServletRequest) { @RequestBody String json, HttpServletRequest httpServletRequest) {
checkPayloadSize(httpServletRequest);
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>(); DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
@ -320,7 +316,6 @@ public class DeviceApiController implements TbTransportService {
@PathVariable("deviceToken") String deviceToken, @PathVariable("deviceToken") String deviceToken,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The RPC request JSON", required = true) @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The RPC request JSON", required = true)
@RequestBody String json, HttpServletRequest httpServletRequest) { @RequestBody String json, HttpServletRequest httpServletRequest) {
checkPayloadSize(httpServletRequest);
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>(); DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
@ -443,12 +438,6 @@ public class DeviceApiController implements TbTransportService {
return responseWriter; return responseWriter;
} }
private void checkPayloadSize(HttpServletRequest httpServletRequest) {
if (httpServletRequest.getContentLength() > maxPayloadSize) {
throw new MaxPayloadSizeExceededException();
}
}
private DeferredResult<ResponseEntity> getOtaPackageCallback(String deviceToken, String title, String version, int size, int chunk, OtaPackageType firmwareType) { private DeferredResult<ResponseEntity> getOtaPackageCallback(String deviceToken, String title, String version, int size, int chunk, OtaPackageType firmwareType) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>(); DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.config; package org.thingsboard.server.transport.http.config;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
@ -21,51 +21,31 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException; import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import java.io.IOException; import java.io.IOException;
import java.util.List;
@Slf4j @Slf4j
@Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class RequestSizeFilter extends OncePerRequestFilter { public class RequestSizeFilter extends OncePerRequestFilter {
private final List<String> urls = List.of("/api/plugins/rpc/**", "/api/rpc/**"); private final int maxPayloadSize;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final ThingsboardErrorResponseHandler errorResponseHandler;
@Value("${transport.http.max_payload_size:65536}")
private int maxPayloadSize;
@Override @Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request.getContentLength() > maxPayloadSize) { if (request.getContentLength() > maxPayloadSize) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(), log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(), request.getRemoteAddr(), request.getContentLength());
request.getRemoteAddr(), request.getContentLength());
} }
errorResponseHandler.handle(new MaxPayloadSizeExceededException(), response); handleMaxPayloadSizeExceededException(response, new MaxPayloadSizeExceededException(maxPayloadSize));
return; return;
} }
chain.doFilter(request, response); chain.doFilter(request, response);
} }
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
for (String url : urls) {
if (pathMatcher.match(url, request.getRequestURI())) {
return false;
}
}
return true;
}
@Override @Override
protected boolean shouldNotFilterAsyncDispatch() { protected boolean shouldNotFilterAsyncDispatch() {
return false; return false;
@ -75,4 +55,9 @@ public class RequestSizeFilter extends OncePerRequestFilter {
protected boolean shouldNotFilterErrorDispatch() { protected boolean shouldNotFilterErrorDispatch() {
return false; return false;
} }
private void handleMaxPayloadSizeExceededException(HttpServletResponse response, MaxPayloadSizeExceededException exception) throws IOException {
response.setStatus(HttpStatus.PAYLOAD_TOO_LARGE.value());
JacksonUtil.writeValue(response.getWriter(), exception.getMessage());
}
} }

View File

@ -0,0 +1,59 @@
/**
* Copyright © 2016-2024 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.transport.http.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@Order(SecurityProperties.BASIC_AUTH_ORDER)
@ConditionalOnExpression("('${service.type:null}'=='tb-transport')")
public class TransportSecurityConfiguration {
public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**";
@Value("${transport.http.max_payload_size:65536}")
private int maxPayloadSize;
@Bean
protected RequestSizeFilter httpTransportRequestSizeFilter() {
return new RequestSizeFilter(maxPayloadSize);
}
@Bean
SecurityFilterChain httpTransportFilterChain(HttpSecurity http) throws Exception {
http
.securityMatchers(matchers -> matchers.requestMatchers(DEVICE_API_ENTRY_POINT))
.cors(cors -> {
})
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(config -> config
.requestMatchers(DEVICE_API_ENTRY_POINT).permitAll())
.addFilterBefore(httpTransportRequestSizeFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

View File

@ -170,7 +170,7 @@ transport:
request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}"
# HTTP maximum request processing timeout in milliseconds # HTTP maximum request processing timeout in milliseconds
max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}" max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}"
# Maximum request size # HTTP maximum request size
max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:65536}" # max payload size in bytes max_payload_size: "${HTTP_MAX_PAYLOAD_SIZE:65536}" # max payload size in bytes
sessions: sessions:
# Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device.