added spring security for http transport, added large RequestSizeFilter for http transport
This commit is contained in:
		
							parent
							
								
									64df0e16ff
								
							
						
					
					
						commit
						02f7001102
					
				@ -17,6 +17,7 @@ package org.thingsboard.server.config;
 | 
			
		||||
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
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.security.SecurityProperties;
 | 
			
		||||
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.RestLoginProcessingFilter;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
 | 
			
		||||
import org.thingsboard.server.transport.http.config.RequestSizeFilter;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
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 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
 | 
			
		||||
    private ThingsboardErrorResponseHandler restAccessDeniedHandler;
 | 
			
		||||
 | 
			
		||||
@ -124,8 +131,15 @@ public class ThingsboardSecurityConfiguration {
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private RateLimitProcessingFilter rateLimitProcessingFilter;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private RequestSizeFilter requestSizeFilter;
 | 
			
		||||
    @Bean
 | 
			
		||||
    protected RequestSizeFilter httpTransportRequestSizeFilter() {
 | 
			
		||||
        return new RequestSizeFilter(httpTransportMaxPayloadSize);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    protected RequestSizeFilter requestSizeFilter() {
 | 
			
		||||
        return new RequestSizeFilter(maxPayloadSize);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    protected FilterRegistrationBean<ShallowEtagHeaderFilter> buildEtagFilter() throws Exception {
 | 
			
		||||
@ -203,6 +217,20 @@ public class ThingsboardSecurityConfiguration {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @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 {
 | 
			
		||||
        http.headers(headers -> headers
 | 
			
		||||
                        .cacheControl(config -> {})
 | 
			
		||||
@ -214,7 +242,6 @@ public class ThingsboardSecurityConfiguration {
 | 
			
		||||
                .authorizeHttpRequests(config -> config
 | 
			
		||||
                        .requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points (webjars included)
 | 
			
		||||
                        .requestMatchers(
 | 
			
		||||
                                DEVICE_API_ENTRY_POINT, // Device HTTP Transport API
 | 
			
		||||
                                FORM_BASED_LOGIN_ENTRY_POINT, // Login end-point
 | 
			
		||||
                                PUBLIC_LOGIN_ENTRY_POINT, // Public login end-point
 | 
			
		||||
                                TOKEN_REFRESH_ENTRY_POINT, // Token refresh end-point
 | 
			
		||||
@ -228,7 +255,7 @@ public class ThingsboardSecurityConfiguration {
 | 
			
		||||
                .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
 | 
			
		||||
                .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
 | 
			
		||||
                .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
 | 
			
		||||
                .addFilterBefore(requestSizeFilter, UsernamePasswordAuthenticationFilter.class)
 | 
			
		||||
                .addFilterBefore(requestSizeFilter(), UsernamePasswordAuthenticationFilter.class)
 | 
			
		||||
                .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
 | 
			
		||||
        if (oauth2Configuration != null) {
 | 
			
		||||
            http.oauth2Login(login -> login
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,10 @@ server:
 | 
			
		||||
        key_alias: "${SSL_KEY_ALIAS:tomcat}"
 | 
			
		||||
        # Password used to access the key
 | 
			
		||||
        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)
 | 
			
		||||
  http2:
 | 
			
		||||
    # Enable/disable HTTP/2 support
 | 
			
		||||
@ -959,7 +963,7 @@ transport:
 | 
			
		||||
    request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}"
 | 
			
		||||
    # HTTP maximum request processing timeout in milliseconds
 | 
			
		||||
    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
 | 
			
		||||
  # Local MQTT transport parameters
 | 
			
		||||
  mqtt:
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 | 
			
		||||
 | 
			
		||||
@DaoSqlTest
 | 
			
		||||
@TestPropertySource(properties = {
 | 
			
		||||
        "transport.http.max_payload_size=10000"
 | 
			
		||||
        "server.http.max_payload_size=10000"
 | 
			
		||||
})
 | 
			
		||||
public class RpcControllerTest extends AbstractControllerTest {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,9 +15,15 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.common.msg.tools;
 | 
			
		||||
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
 | 
			
		||||
public class MaxPayloadSizeExceededException extends RuntimeException {
 | 
			
		||||
 | 
			
		||||
    public MaxPayloadSizeExceededException() {
 | 
			
		||||
        super("Payload size exceeds the limit");
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final int limit;
 | 
			
		||||
 | 
			
		||||
    public MaxPayloadSizeExceededException(int limit) {
 | 
			
		||||
        super("Payload size exceeds the limit " + limit);
 | 
			
		||||
        this.limit = limit;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,10 @@
 | 
			
		||||
            <artifactId>spring-boot-starter-web</artifactId>
 | 
			
		||||
            <scope>provided</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.springframework.boot</groupId>
 | 
			
		||||
            <artifactId>spring-boot-starter-security</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.slf4j</groupId>
 | 
			
		||||
            <artifactId>slf4j-api</artifactId>
 | 
			
		||||
 | 
			
		||||
@ -136,9 +136,6 @@ public class DeviceApiController implements TbTransportService {
 | 
			
		||||
 | 
			
		||||
    private static final String ACCESS_TOKEN_PARAM_DESCRIPTION = "Your device access token.";
 | 
			
		||||
 | 
			
		||||
    @Value("${transport.http.max_payload_size:65536}")
 | 
			
		||||
    private int maxPayloadSize;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private HttpTransportContext transportContext;
 | 
			
		||||
 | 
			
		||||
@ -289,7 +286,6 @@ public class DeviceApiController implements TbTransportService {
 | 
			
		||||
            @PathVariable("requestId") Integer requestId,
 | 
			
		||||
            @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) {
 | 
			
		||||
        checkPayloadSize(httpServletRequest);
 | 
			
		||||
        DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
 | 
			
		||||
        transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
 | 
			
		||||
                new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
 | 
			
		||||
@ -320,7 +316,6 @@ public class DeviceApiController implements TbTransportService {
 | 
			
		||||
            @PathVariable("deviceToken") String deviceToken,
 | 
			
		||||
            @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The RPC request JSON", required = true)
 | 
			
		||||
            @RequestBody String json, HttpServletRequest httpServletRequest) {
 | 
			
		||||
        checkPayloadSize(httpServletRequest);
 | 
			
		||||
        DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
 | 
			
		||||
        transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
 | 
			
		||||
                new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
 | 
			
		||||
@ -443,12 +438,6 @@ public class DeviceApiController implements TbTransportService {
 | 
			
		||||
        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) {
 | 
			
		||||
        DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>();
 | 
			
		||||
        transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.config;
 | 
			
		||||
package org.thingsboard.server.transport.http.config;
 | 
			
		||||
 | 
			
		||||
import jakarta.servlet.FilterChain;
 | 
			
		||||
import jakarta.servlet.ServletException;
 | 
			
		||||
@ -21,51 +21,31 @@ import jakarta.servlet.http.HttpServletRequest;
 | 
			
		||||
import jakarta.servlet.http.HttpServletResponse;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.springframework.util.AntPathMatcher;
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
import org.springframework.web.filter.OncePerRequestFilter;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException;
 | 
			
		||||
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@Component
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class RequestSizeFilter extends OncePerRequestFilter {
 | 
			
		||||
 | 
			
		||||
    private final List<String> urls = List.of("/api/plugins/rpc/**", "/api/rpc/**");
 | 
			
		||||
    private final AntPathMatcher pathMatcher = new AntPathMatcher();
 | 
			
		||||
    private final ThingsboardErrorResponseHandler errorResponseHandler;
 | 
			
		||||
    
 | 
			
		||||
    @Value("${transport.http.max_payload_size:65536}")
 | 
			
		||||
    private int maxPayloadSize;
 | 
			
		||||
    private final int maxPayloadSize;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
 | 
			
		||||
        if (request.getContentLength() > maxPayloadSize) {
 | 
			
		||||
            if (log.isDebugEnabled()) {
 | 
			
		||||
                log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(),
 | 
			
		||||
                        request.getRemoteAddr(), request.getContentLength());
 | 
			
		||||
                log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(), request.getRemoteAddr(), request.getContentLength());
 | 
			
		||||
            }
 | 
			
		||||
            errorResponseHandler.handle(new MaxPayloadSizeExceededException(), response);
 | 
			
		||||
            handleMaxPayloadSizeExceededException(response, new MaxPayloadSizeExceededException(maxPayloadSize));
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        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
 | 
			
		||||
    protected boolean shouldNotFilterAsyncDispatch() {
 | 
			
		||||
        return false;
 | 
			
		||||
@ -75,4 +55,9 @@ public class RequestSizeFilter extends OncePerRequestFilter {
 | 
			
		||||
    protected boolean shouldNotFilterErrorDispatch() {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handleMaxPayloadSizeExceededException(HttpServletResponse response, MaxPayloadSizeExceededException exception) throws IOException {
 | 
			
		||||
        response.setStatus(HttpStatus.PAYLOAD_TOO_LARGE.value());
 | 
			
		||||
        JacksonUtil.writeValue(response.getWriter(), exception.getMessage());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -170,7 +170,7 @@ transport:
 | 
			
		||||
    request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}"
 | 
			
		||||
    # HTTP maximum request processing timeout in milliseconds
 | 
			
		||||
    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
 | 
			
		||||
  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.
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user