We configured our new micro service (using Spring-Boot) in a way that the official API is on port 8080 (which is be mapped outside of our virtual network to normal HTTPS on port 443), while some management functions are on a secondary HTTP port 7979. These are only used inside the virtual network, and used for monitoring, loadbalancing etc.
All API access needs to be secured with OAuth, while the management functions should be accessible freely inside the network. So we configured Spring security this way (http is a HttpSecurity object):
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.authorizeRequests()
.antMatchers("/info").anonymous()
.antMatchers("/health").anonymous()
.antMatchers(HttpMethod.GET, "/warehouses/**").access(oauthScopeRead)
.antMatchers(HttpMethod.PUT, "/warehouses/**").access(oauthScopeWrite)
.anyRequest().denyAll();
This has the effect on both ports: /info
and /health
are unauthorized, while /warehouses
needs authentication, and everything else also needs authentication (returns 401, but when invoking with authentication, it returns 403).
As there is no /info
or /health
on the public port, these return 404 for unauthorized users, while everything else returns 401. I'm unsatisfied with this and would like to have
I couldn't find anything about ports in the Spring Security Javadocs or reference documentation.
What can I do here?
You need to declare SecurityFilterChain and WebSecurityCustomizer beans instead of overriding methods of WebSecurityConfigurerAdapter class. NOTE: If you don't want to change your current code, you should keep Spring Boot version lower than 2.7. 0 or Spring Security version older than 5.7. 1.
When using Java configuration, the way to define multiple security realms is to have multiple @Configuration classes that extend the WebSecurityConfigurerAdapter base class – each with its own security configuration. These classes can be static and placed inside the main config.
I found a solution:
The authorizeRequests()
method here returns an ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
, which has (from its ancestor class AbstractRequestMatcherRegistry) beside some antMatchers
methods also a generic requestMatchers()
method, which takes one or more RequestMatcher
objects. It turns out this is an interface I can implement myself:
/**
* A request matcher which matches just a port.
*
* @param port the port to match.
*
* @return the new matcher.
*/
private RequestMatcher forPort(final int port) {
return (HttpServletRequest request) -> port == request.getLocalPort();
}
(This is Java 8 syntax, with previous Java versions you'll had to write an anyonymous class here.)
While requestMatchers
takes several such matchers, it looks like those are connected by OR (at least this example suggests this), thus I used an AndRequestMatcher to connect it to the matcher for the path (and HTTP method)).
The final code looked like this:
@Value("${management.port}")
private int managementPort;
@Value("${server.port}")
private int apiPort;
/**
* Configure scopes for specific controller/httpmethods/roles here.
*/
@Override
public void configure(final HttpSecurity http) throws Exception {
//J-
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.authorizeRequests()
.requestMatchers(forPortAndPath(managementPort, "/info")).anonymous()
.requestMatchers(forPortAndPath(managementPort, "/health")).anonymous()
.requestMatchers(forPortAndPath(apiPort, HttpMethod.GET, "/warehouses/**")).access(oauthScopeRead)
.requestMatchers(forPortAndPath(apiPort, HttpMethod.PUT, "/warehouses/**")).access(oauthScopeWrite)
.anyRequest().denyAll();
//J+
}
/**
* Creates a request matcher which only matches requests for a specific local port and path (using an
* {@link AntPathRequestMatcher} for the path part).
*
* @param port the port to match
* @param pathPattern the pattern for the path.
*
* @return the new request matcher.
*/
private RequestMatcher forPortAndPath(final int port, @Nonnull final String pathPattern) {
return new AndRequestMatcher(forPort(port), new AntPathRequestMatcher(pathPattern));
}
/**
* Creates a request matcher which only matches requests for a specific local port, path and request method (using
* an {@link AntPathRequestMatcher} for the path part).
*
* @param port the port to match
* @param pathPattern the pattern for the path.
* @param method the HttpMethod to match. Requests for other methods will not be matched.
*
* @return the new request matcher.
*/
private RequestMatcher forPortAndPath(final int port, @Nonnull final HttpMethod method,
@Nonnull final String pathPattern) {
return new AndRequestMatcher(forPort(port), new AntPathRequestMatcher(pathPattern, method.name()));
}
/**
* A request matcher which matches just a port.
*
* @param port the port to match.
*
* @return the new matcher.
*/
private RequestMatcher forPort(final int port) {
return (HttpServletRequest request) -> { return port == request.getLocalPort(); };
}
This does not fully reflect the question: the managementPort has here just "/info" and "/health" public reachable, instead of everything.
You could use this
.requestMatchers(forPort(managementPort)).anonymous()
to make this port fully unauthorized.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With