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
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 List pathsToMatch;
}
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.paths-to-match=/api/public
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
- 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;
}
}
- 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();
}
@Bean
@Lazy
public FilterRegistrationBean publicEndpointsFilter() {
return new FilterRegistrationBean(new PublicEndpointsFilter(publicPortConfig.getPort(),
publicPortConfig.getPathsToMatch()));
}
}
5a) Filters to handle public endpoints.
public class PublicEndpointsFilter implements Filter {
private static final Logger logger = LogManager.getLogger(PublicEndpointsFilter.class);
int publicPort;
List pathsToMatch;
/**
* Default Constructor
*
* @param publicPort
* @param pathsToMatch
*/
public PublicEndpointsFilter(int publicPort, List 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;
}
- 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)");
}
}
- 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.
- 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
- Public Ports are configured in firewall to allow traffic
Thanks