English 中文(简体)
Unable to get additional tomcat port on https using Spring Boot
原标题:

I have configured additional port to work on https for an REST endpoint. However, while sending request to this endpoint, application does not negotiate over https instead it uses only http. Framework is on Spring Boot 2.7.5.

@Configuration 
public class PublicPortConfig {
private static final Logger logger = LogManager.getLogger(PublicPortConfig.class);

@Value("${server.port.public:19280}")
private int port;

@Bean
@ConditionalOnProperty(name = "server.port.public")
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> publicServletContainerCustomizer (
        ServerProperties serverProperties) {
    logger.info("Configuring public port: {}", port);
    return tomcat -> {          
        Connector connector = new Connector();

        connector.setPort(port);
        connector.setScheme("https");
                    
        SSLHostConfig sslHostConfig = new SSLHostConfig();
        
               sslHostConfig.setCertificateKeystoreFile(serverProperties.getSsl().getKeyStore());
        sslHostConfig.setCertificateKeyPassword(serverProperties.getSsl().getKeyStorePassword());
        sslHostConfig.setCertificateKeystoreType(serverProperties.getSsl().getKeyStoreType());

        connector.addSslHostConfig(sslHostConfig);
        
        tomcat.addAdditionalTomcatConnectors(connector);
    };
}
}
@RestController
public class ExternalApiController {
    @GetMapping("/api/public")
    public ResponseEntity<String> hello() {
        return ResponseEntity.ok("Hello Customer (public)");
    }
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    logger.info("Configuring HttpSecurity via SecurityFilterChain...");
    // @formatter:off
    http.authorizeRequests()
            .antMatchers(AUTH_PATH_WHITELIST).permitAll()
            .antMatchers("/api/public/**").permitAll()
            .antMatchers("/api/admin/**").hasAnyRole("ADMIN")
            .antMatchers("/actuator/**").hasAnyRole("ADMIN", "ACTUATOR")
            .and()
                .authorizeRequests().anyRequest().fullyAuthenticated();
    http.sessionManagement()
             .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
             .maximumSessions(MAX_SESSION_ACTIVE);
    http.httpBasic()
        .realmName("API Auth via DB or LDAP")
        .and()
        .authenticationProvider(customAuthenticationProvider);
    http.csrf().csrfTokenRepository(cookieCsrfTokenRepository());
    /**
     * Force Disable for API
     */
    http.csrf().disable();
    // http.cors().disable();
    // @formatter:on

    return http.build();
}

ExternalApiController sends response when request is sent on http and not on https. All fully authenticated endpoints works perfectly well on https.

cURL gives following results

$ curl --insecure -H "Accept: application/json" -H "Content-Type: application/json" -X GET "https://localhost:19280/app/api/public"

Response: curl: (35) LibreSSL/3.3.6: error:1404B42E:SSL routines:ST_CONNECT:tlsv1 alert protocol version Java Backend Error: java.lang.IllegalArgumentException: Invalid character found in method name [0x160x030x010x01(0x010x000x01$0x030x03090xe9}0xef0x980x970x1b+0x820x0f0xc10xbc0xde0x8370xfceI0xdey0x100x1f0x99vJ0xa50x7f_0xd9Y ]. HTTP method names must be tokens

Where as http request using cURL

$ curl --insecure -H "Accept: application/json" -H "Content-Type: application/json" -X GET "http://localhost:19280/app/api/public"

Response: Hello Customer (public)

Application is started on 2 ports as per below log INFO 2023-06-06/17:08:18/GMT+05:30 [app-rest-api] (TomcatWebServer.java:220) (start) Tomcat started on port(s): 9280 (https) 19280 (https) with context path /app

Port 9280 is ok. However, anything I access on 19280 throws error

Appreciate any help. Thanks in advance

问题回答

Couple of months back I enabled https for our REST API in Spring boot,used self signed certificate for local testing generated using Java keytool

keytool -genkeypair -alias team_api -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore gaskit.p12 -storepass pwd -validity 3650

And then adding following properties in application.properties

server.ssl.key-store-type=${ssl_key_store_type}
# The path to the keystore containing the certificate
server.ssl.key-store=${ssl_key_store}
# The password used to generate the certificate
server.ssl.key-store-password=${ssl_key_store_Password}
# The alias mapped to the certificate
server.ssl.key-alias=${ssl_key_alias}
server.ssl.enabled=true

Finally, I am able to get Public / Private Api EndPoints by deep dive into Http11NioProtocol. Thanks to Spring Community Documents Spring Docs under Section: Enable Multiple Connectors with Tomcat for all explanations.

Please find complete solution below

  1. Create Configuration File for Public Api

    
    @Configuration
    @PropertySource("classpath:public-api.properties")
    @ConfigurationProperties(prefix = "public.ssl")
    @Data
    public class PublicApiConfig {
    private int port;
    private String keyStoreType;
    private String keyStore;
    private String keyStorePassword;
    private String keyAlias;
    private String pathPrefix;
    }
    
  2. Configuration File public-api.properties

    
    public.ssl.port=19280
    public.ssl.key-store-type=PKCS12
    public.ssl.key-store=ssl/your_certificate.p12
    public.ssl.key-store-password=your_password
    public.ssl.key-alias=tomcat
    public.ssl.path-prefix=/api/public
    
  3. Define server.public.ssl.port.enabled in your application.properties and value could be true (Public Access) or false.

# true for Public Api Access
server.public.ssl.port.enabled=true
  1. Create PublicPortCustomizer class
@Component
@ConditionalOnProperty(value = "server.public.ssl.port.enabled", havingValue = "true", matchIfMissing = false)
public class PublicPortCustomizer {

    private static final Logger logger = LogManager.getLogger(PublicPortCustomizer.class);

    @Autowired
    PublicApiConfig publicPortConfig;

    /**
     * Verify if KeyStore is Valid
     */
    private boolean isValidKeyStoreFile(String keyStoreFile, String keyStorePassword, String keyStoreType) {
        try {
            File file = new File(keyStoreFile);
            KeyStore keystore = KeyStore.getInstance(keyStoreType);
            FileInputStream fis = new FileInputStream(file);
            keystore.load(fis, keyStorePassword.toCharArray());
            return true;
        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        if (isValidKeyStoreFile(publicPortConfig.getKeyStore(), publicPortConfig.getKeyStorePassword(),
                publicPortConfig.getKeyStoreType())) {
            logger.info("Configuring Ssl Public for KeyStore: {}", publicPortConfig.getKeyStore());
            /**
             * Create Connector
             */
            Connector connector = createSslConnector();
            tomcat.addAdditionalTomcatConnectors(connector);
        } else {
            logger.fatal("KeyStore for Public: {} is INVALID", publicPortConfig.getKeyStore());
        }
        return tomcat;
    }

    /**
     * Create Ssl Connector
     * @return
     */
    private Connector createSslConnector() {
        logger.info("Configuring Additional Ssl Connector...");

        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");

        connector.setScheme("https");
        connector.setSecure(true);
        connector.setPort(publicPortConfig.getPort());

        Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();

        protocol.setSSLEnabled(true);
        protocol.setKeystoreFile(publicPortConfig.getKeyStore());
        protocol.setKeystorePass(publicPortConfig.getKeyStorePassword());
        protocol.setKeyAlias(publicPortConfig.getKeyAlias());

        return connector;
    }
}
  1. Define SpringSecurity where Public Api will be accessed through JWT which is controlled in Controller
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SpringSecurityConfig {

    @Autowired
    CustomAuthenticationProvider customAuthenticationProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .and()
                    .authorizeRequests().anyRequest().fullyAuthenticated();
        http.sessionManagement()
                 .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        http.httpBasic()
            .and()
            .authenticationProvider(customAuthenticationProvider);
        http.csrf().disable();
        http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
        return http.build();
    } 
 
  1. Sample Public / Private Rest Controller
@RestController
public class PublicApiHelloController {
    @GetMapping("/api/public")
    public ResponseEntity hello() {
        return ResponseEntity.ok("Hello Customer (public)");
    }
}
@RestController
public class PrivateApiHelloController {
    // Requires Authentication
    @GetMapping("/api/private")
    public ResponseEntity hello() {
        return ResponseEntity.ok("Hello Staff (private)");
    }
}
  1. Start Application and you should see
INFO  2023-06-08/08:19:27/GMT+05:30 [app-api] (DirectJDKLog.java:173) (log) Starting ProtocolHandler ["https-jsse-nio-9280"]
INFO  2023-06-08/08:19:27/GMT+05:30 [app-api] (DirectJDKLog.java:173) (log) Starting ProtocolHandler ["https-jsse-nio-19280"]
INFO  2023-06-08/08:19:27/GMT+05:30 [app-api] (TomcatWebServer.java:220) (start) Tomcat started on port(s): 9280 (https) 19280 (https) with context path  /app 

Make sure ProtocolHandler starts with https-jsse-nio for all https enabled.

  1. Public Api Request
curl --insecure -H  Accept: application/json  -H  Content-Type: application/json  -X GET https://localhost:19280/app/api/public

Response:: Hello Customer (public)

9)Private Api Request

curl --insecure -H  Accept: application/json  -H  Content-Type: application/json  -X GET https://localhost:9280/app/api/private

Response:: {"timestamp":1686133851868,"status":401,"error":"Unauthorized","message":"Unauthorized","path":"/app/api/private"}

Unauthorized as private expects Authentication

  1. if required you can create any Filter to handle public endpoints.

  2. Public Ports are configured in firewall to allow traffic

Thanks





相关问题
Spring Properties File

Hi have this j2ee web application developed using spring framework. I have a problem with rendering mnessages in nihongo characters from the properties file. I tried converting the file to ascii using ...

Logging a global ID in multiple components

I have a system which contains multiple applications connected together using JMS and Spring Integration. Messages get sent along a chain of applications. [App A] -> [App B] -> [App C] We set a ...

Java Library Size

If I m given two Java Libraries in Jar format, 1 having no bells and whistles, and the other having lots of them that will mostly go unused.... my question is: How will the larger, mostly unused ...

How to get the Array Class for a given Class in Java?

I have a Class variable that holds a certain type and I need to get a variable that holds the corresponding array class. The best I could come up with is this: Class arrayOfFooClass = java.lang....

SQLite , Derby vs file system

I m working on a Java desktop application that reads and writes from/to different files. I think a better solution would be to replace the file system by a SQLite database. How hard is it to migrate ...

热门标签