英文:
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
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
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$0x030x030\90xe9}0xef0x980x970x1b+0x820x0f0xc10xbc0xde0x8370xfceI0xdey0x100x1f0x99vJ0xa50x7f_0xd9Y ].
HTTP method names must be tokens
Whereas 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.
英文:
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$0x030x030\90xe9}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.
答案1
得分: 2
几个月前,我在Spring Boot中为我们的REST API启用了https,并在本地测试中使用了自签名证书,该证书是使用Java keytool生成的。
keytool -genkeypair -alias team_api -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore gaskit.p12 -storepass pwd -validity 3650
然后,在application.properties
文件中添加以下属性:
server.ssl.key-store-type=${ssl_key_store_type}
# 包含证书的密钥库的路径
server.ssl.key-store=${ssl_key_store}
# 用于生成证书的密码
server.ssl.key-store-password=${ssl_key_store_Password}
# 映射到证书的别名
server.ssl.key-alias=${ssl_key_alias}
server.ssl.enabled=true
英文:
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
答案2
得分: 1
最终,通过深入研究Http11NioProtocol,我成功地获得了公共/私有Api终端点。感谢Spring社区文档[Spring Docs][1]在“在Tomcat中启用多个连接器”部分提供的所有解释。
请在下方找到完整的解决方案:
1)创建用于公共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 List<String> pathsToMatch;
}
2)公共Api的配置文件 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.paths-to-match=/api/public
3)在你的application.properties中定义`server.public.ssl.port.enabled`,其值可以是true(公共访问)或false。
# true表示公共Api访问
server.public.ssl.port.enabled=true
4)创建PublicPortCustomizer类
@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;
/**
* 验证KeyStore是否有效
*/
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("为KeyStore配置公共Ssl: {}", publicPortConfig.getKeyStore());
/**
* 创建Connector
*/
Connector connector = createSslConnector();
tomcat.addAdditionalTomcatConnectors(connector);
} else {
logger.fatal("公共KeyStore: {} 是无效的", publicPortConfig.getKeyStore());
}
return tomcat;
}
/**
* 创建Ssl Connector
* @return
*/
private Connector createSslConnector() {
logger.info("配置额外的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;
}
}
5)定义SpringSecurity,其中公共Api将通过JWT访问,该JWT在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();
}
@Bean
@Lazy
public FilterRegistrationBean<PublicEndpointsFilter> publicEndpointsFilter() {
return new FilterRegistrationBean<>(new PublicEndpointsFilter(publicPortConfig.getPort(),
publicPortConfig.getPathsToMatch()));
}
}
5a)处理公共终端点的过滤器。
public class PublicEndpointsFilter implements Filter {
private static final Logger logger = LogManager.getLogger(PublicEndpointsFilter.class);
int publicPort;
List<String> pathsToMatch;
/**
* 默认构造函数
*
* @param publicPort
* @param pathsToMatch
*/
public PublicEndpointsFilter(int publicPort, List<String> pathsToMatch) {
this.publicPort = publicPort;
this.pathsToMatch = pathsToMatch;
logger.info("为publicPort: {} 配置过滤器 pathsToMatch: {}", publicPort, pathsToMatch);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
boolean trustedEndPoint = isRequestForTrustedEndpoint(servletRequest);
int status = HttpServletResponse.SC_OK;
///////////////////////
if (servletRequest.getLocalPort() == publicPort && !trustedEndPoint) {
status = HttpServletResponse.SC_FORBIDDEN;
}
if (status != HttpServletResponse.SC_OK) {
logger.warn("在路径上拒绝请求: {} publicPort: {} localPort: {} 状态: {}", request.getRequestURI(),
publicPort, servletRequest.getLocalPort(), status);
response.setStatus(status);
servletResponse.getOutputStream().close();
return;
}
///////////////////////
if (servletRequest.getLocalPort() != publicPort && trustedEndPoint) {
status = HttpServletResponse.SC_FORBIDDEN;
}
if (status != HttpServletResponse.SC_OK) {
logger.warn("在路径上拒绝请求: {} publicPort: {} localPort: {} 状态: {}", request.getRequestURI(),
publicPort, servletRequest.getLocalPort(), status);
response.setStatus(status);
servletResponse.getOutputStream().close();
return;
}
logger.debug("在路径上进行过滤委托: {} localPort: {} 状态: {}", request.getRequestURI(),
request.getLocal
<details>
<summary>英文:</summary>
Finally, I am able to get Public / Private Api EndPoints by deep dive into Http11NioProtocol. Thanks to Spring Community Documents [Spring Docs][1] under Section: Enable Multiple Connectors with Tomcat for all explanations.
Please find complete solution below
1) Create Configuration File for Public Api <pre><code>
@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 List<String> pathsToMatch;
}
</code></pre>
2) Configuration File public-api.properties <pre><code>
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.paths-to-match=/api/public
</code></pre>
3) Define `server.public.ssl.port.enabled` in your application.properties and value could be true (Public Access) or false.
<pre><code># true for Public Api Access
server.public.ssl.port.enabled=true</code></pre>
4) Create PublicPortCustomizer class
<pre><code>@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;
}
}</code></pre>
[1]: https://docs.spring.io/spring-boot/docs/2.0.0.M6/reference/html/howto-embedded-web-servers.html
5) Define SpringSecurity where Public Api will be accessed through JWT which is controlled in Controller
<pre><code>@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();
}
@Bean
@Lazy
public FilterRegistrationBean<PublicEndpointsFilter> publicEndpointsFilter() {
return new FilterRegistrationBean<>(new PublicEndpointsFilter(publicPortConfig.getPort(),
publicPortConfig.getPathsToMatch()));
}
}
</code></pre>
5a) Filters to handle public endpoints.
<pre><code>public class PublicEndpointsFilter implements Filter {
private static final Logger logger = LogManager.getLogger(PublicEndpointsFilter.class);
int publicPort;
List<String> pathsToMatch;
/**
* Default Constructor
*
* @param publicPort
* @param pathsToMatch
*/
public PublicEndpointsFilter(int publicPort, List<String> pathsToMatch) {
this.publicPort = publicPort;
this.pathsToMatch = pathsToMatch;
logger.info("Configuring Filter for publicPort: {} pathsToMatch: {}", publicPort, pathsToMatch);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
boolean trustedEndPoint = isRequestForTrustedEndpoint(servletRequest);
int status = HttpServletResponse.SC_OK;
///////////////////////
if (servletRequest.getLocalPort() == publicPort && !trustedEndPoint) {
status = HttpServletResponse.SC_FORBIDDEN;
}
if (status != HttpServletResponse.SC_OK) {
logger.warn("Denying request on path: {} publicPort: {} localPort: {} status: {}", request.getRequestURI(),
publicPort, servletRequest.getLocalPort(), status);
response.setStatus(status);
servletResponse.getOutputStream().close();
return;
}
///////////////////////
if (servletRequest.getLocalPort() != publicPort && trustedEndPoint) {
status = HttpServletResponse.SC_FORBIDDEN;
}
if (status != HttpServletResponse.SC_OK) {
logger.warn("Denying request on path: {} publicPort: {} localPort: {} status: {}", request.getRequestURI(),
publicPort, servletRequest.getLocalPort(), status);
response.setStatus(status);
servletResponse.getOutputStream().close();
return;
}
logger.debug("Filter Delegate on path: {} localPort: {} status: {}", request.getRequestURI(),
request.getLocalPort(), status);
filterChain.doFilter(servletRequest, response);
}
/**
* Verify Path as required for Public Access
*
* @param servletRequest
* @return
*/
private boolean isRequestForTrustedEndpoint(ServletRequest servletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
boolean ok = false;
for (String pathToMatch : pathsToMatch) {
String requestPath = request.getContextPath() + pathToMatch;
ok = request.getRequestURI().startsWith(requestPath);
logger.debug("Verified path: {} localPort: {}", requestPath, servletRequest.getLocalPort());
}
return ok;
}</code></pre>
6) Sample Public / Private Rest Controller
<pre><code>@RestController
public class PublicApiHelloController {
@GetMapping("/api/public")
public ResponseEntity<String> hello() {
return ResponseEntity.ok("Hello Customer (public)");
}
}
@RestController
public class PrivateApiHelloController {
// Requires Authentication
@GetMapping("/api/private")
public ResponseEntity<String> hello() {
return ResponseEntity.ok("Hello Staff (private)");
}
}</code></pre>
7) Start Application and you should see
<pre><code>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'
</code></pre>
Make sure ProtocolHandler starts with https-jsse-nio for all https enabled.
8) Public Api Request
<pre><code>curl --insecure -H 'Accept: application/json' -H 'Content-Type: application/json' -X GET https://localhost:19280/app/api/public</code></pre>
Response:: Hello Customer (public)
9)Private Api Request
<pre><code>curl --insecure -H 'Accept: application/json' -H 'Content-Type: application/json' -X GET https://localhost:9280/app/api/private</code></pre>
Response:: {"timestamp":1686133851868,"status":401,"error":"Unauthorized","message":"Unauthorized","path":"/app/api/private"}
Unauthorized as private expects Authentication
10) Public Ports are configured in firewall to allow traffic
Thanks
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论