Reputation: 464
After tinkering with different security frameworks, I've decided to go with Apache Shiro for my Spring Boot RestAPI, because it appears to offer the necessary flexibility without too much bureaucratic overhead. So far, I haven't done anything except adding the maven dependency to my project:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.1</version>
</dependency>
This forced me to define a Realm bean in order to get the application started:
@Bean
public Realm realm() {
return new TMTRealm();
}
The bean pretty much does nothing yet, except for implementing the Realm interface:
public class TMTRealm implements Realm {
private static final String Realm_Name = "realm_name";
@Override
public String getName() {
return Realm_Name;
}
@Override
public boolean supports(AuthenticationToken token) {
return false;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
So far so good. Except that now my RestAPI is violating CORS policy by not adding the 'Access-Control-Allow-Origin' header to any of its responses. I've noticed that Chrome doesn't send any dedicated OPTIONS request but two requests of the same method, GET in this case, with the first one failing as follows:
Access to XMLHttpRequest at 'http://localhost:8081/geo/country/names?typed=D&lang=en-US' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Without Shiro present it works perfectly fine, both the elegant way of using Spring's @CrossOrigin annotation on the controller and the brute force 'old school' way of defining a CorsFilter bean:
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("OPTIONS", "GET", "POST", "PUT", "DELETE"));
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
I've implemented Spring's ApplicationListener interface to hook into when the ApplicationContext is started and can thus see that the corsFilter bean is registered and present:
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("############ - application context started with beans: ");
final String[] beans = event.getApplicationContext().getBeanDefinitionNames();
Arrays.parallelSort(beans);
for (final String bean : beans) {
System.out.println(bean);
}
}
Output:
...
conditionEvaluationDeltaLoggingListener
conventionErrorViewResolver
corsFilter
countryController
...
But the filter is never called upon any request (I've set a breakpoint and System.out to prove it). I've also noticed that there are three Shiro beans present:
shiroEventBusAwareBeanPostProcessor
shiroFilterChainDefinition
shiroFilterFactoryBean
Therefore, I assume that probably the shiroFilterFactoryBean is breaking it somehow and needs extra attention and configuration. Unfortunately the Apache Shiro documentation doesn't seem to say anything about cross-origin requests, and I would assume that this is not (necessarily) part of Shiro's security concerns but rather of the underlying Restful API, that is Spring. Googling the issue didn't yield any helpful results either, so my suspicion is that I'm missing something big, or worse, something small and obvious here. While I'm trying to figure this out, any help or hint is greatly appreciated, thanks!
Upvotes: 0
Views: 1164
Reputation: 464
Awight, I figured it out. It's been a while that I was in filter-servlet land, so I didn't think of the order in which filters are executed. The naive way that I did it, the Shiro filter chain was always executed before my custom CorsFilter (and apparently the default Spring processor of @CrossOrigin annotation as well). Since I haven't yet configured Shiro yet, any request would be rejected as neither authenticated nor authorized, and so the CorsFilter was never executed causing a response without Access-Control-Allow-Origin header.
So, either I configure Shiro properly or just make sure to have the CorsFilter executed prior to the Shiro filter by using Spring's FilterRegistrationBean like this (setOrder to zero):
@Configuration
public class RestApiConfig {
@Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistrationBean() {
final FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>(this.corsFilter());
registration.setOrder(0);
return registration;
}
private CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("OPTIONS", "GET", "POST", "PUT", "DELETE"));
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Bean
public Realm realm() {
return new TMTRealm();
}
}
Edit:
D'uh, it was actually too easy for me to notice. All you have to do is to configure a ShiroFilterChainDefinition bean, so that it will refer to annotations on the controller classes or methods, like this:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
final DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/**", "anon");
return chainDefinition;
}
Just like it is described in the documentation. Now it works both with a CorsFilter bean or with Spring's @CrossOrigin annotation. If there is no Shiro annotation present on the controller method, the request will be passed through.
Upvotes: 3