May 8, 2013
Spring Application without XML Config
Published by Yuan Ji on May 8, 2013 at 4:33:37 AM | 1 Comments
When Spring Framework was first released in 2003, as an IoC container, Spring Framework is using XML as the configuration file format ever since. Spring 2.0, which was released in 2006, introduced XML Schema-based configuration. And since Spring 2.5, annotation-based configuration was used. Later Spring 3.0 was released in 2009, with Java Config project being merged into the core framework.
I started using Spring since version 1, so I'm used to XML configuration. It is quite good compared to other config solution I used before. To me, it was awkward at beginning, then after a while, I got familiar with it and it became comfortable to read. And then XM Schema, Java annotation. Each upgrade makes my config file smaller and more clear to read. But when I found Spring Java Config project, I was immediately embracing it. In my opinion, configuration is part of programming, and config files should be treated as code.
In my JiwhizBlog application code, I use Java config as my configuration solution,
and I like the code very much. In some cases I still use Java annotation-based
config, like all my SpringMVC controller classes. I feel my controller classes
are very simple, and all dependencies are injected into controller through @Inject
annotation. Those controller classes are annotated with @Controller,
and I just put @ComponentScan(basePackages = "com.jiwhiz.blog.web") to my
/src/main/java/com/jiwhiz/blog/config/WebConfig class.
But there is still one XML config file left, for Spring Security, see /src/main/resources/security.xml. The reason is currently Spring Security 3.1.x release does not support Java config.
Spring Security team made tremendous efforts to get security configuration easy. You can see the huge progress after http namespace was introduced in Spring Security. The default config becomes extremely succinct, like the following minimal settings:
<http auto-config='true'>
<intercept-url pattern="/**" access="ROLE_USER" />
</http>
That lowered the entry barriers of Spring Security, and made developer's life easier. But, there always a but, the great conciseness brings a big cost for other less common usage of security settings. Because the default http namespace hides all the complexities behind, it is much harder to customize it to fit your special scenarios. For example, it took me a while to figure out how to make Remember Me work because I'm using Spring Social to do authentication. See my post: Add RememberMe Authentication With Spring Security
The final result is not bad, although I still want to get rid of the last piece of XML code. It is like itching on my back.
Recently I found Spring Security Lead Rob Winch is working on a project called Spring Security Java Config, and I want to give it a try although it is not released yet. After reading the examples, I figured out how to use it and created a branch for my JiwhizBlogWeb project. You can see the code at https://github.com/jiwhiz/JiwhizBlogWeb/tree/java-config
I broke my old SocialAndSecurityConfig into two config classes,
SocialConfig for social related. And
SecurityConfig for security.
My SecurityConfig is a sub-class of WebSecurityConfigurerAdapter, and annotated with
@EnableWebSecurity and @EnableGlobalMethodSecurity(prePostEnabled=true).
I moved security or Rememebr Me related bean to this class, and override some methods to replace
my old security.xml settings. The code is like this:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Inject
private Environment environment;
@Inject
private UserAdminService userAdminService;
@Inject
private RememberMeTokenRepository rememberMeTokenRepository;
@Inject
private UsersConnectionRepository usersConnectionRepository;
@Inject
private SocialAuthenticationServiceLocator socialAuthenticationServiceLocator;
@Bean
public SocialAuthenticationFilter socialAuthenticationFilter() throws Exception{
SocialAuthenticationFilter filter = new SocialAuthenticationFilter(authenticationManager(), userAdminService,
usersConnectionRepository, socialAuthenticationServiceLocator);
filter.setFilterProcessesUrl("/signin");
filter.setSignupUrl(null);
filter.setConnectionAddedRedirectUrl("/myAccount");
filter.setPostLoginUrl("/myAccount"); //always open account profile page after login
filter.setRememberMeServices(rememberMeServices());
return filter;
}
@Bean
public SocialAuthenticationProvider socialAuthenticationProvider(){
return new SocialAuthenticationProvider(usersConnectionRepository, userAdminService);
}
@Bean
public LoginUrlAuthenticationEntryPoint socialAuthenticationEntryPoint(){
return new LoginUrlAuthenticationEntryPoint("/signin");
}
@Bean
public RememberMeServices rememberMeServices(){
PersistentTokenBasedRememberMeServices rememberMeServices = new PersistentTokenBasedRememberMeServices(
environment.getProperty("application.key"), userAdminService, persistentTokenRepository());
rememberMeServices.setAlwaysRemember(true);
return rememberMeServices;
}
@Bean
public RememberMeAuthenticationProvider rememberMeAuthenticationProvider(){
RememberMeAuthenticationProvider rememberMeAuthenticationProvider =
new RememberMeAuthenticationProvider(environment.getProperty("application.key"));
return rememberMeAuthenticationProvider;
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new MongoPersistentTokenRepositoryImpl(rememberMeTokenRepository);
}
@Override
protected void ignoredRequests(IgnoredRequestRegistry ignoredRequests) {
ignoredRequests
.antMatchers("/resources/**");
}
@Override
protected void authorizeUrls(ExpressionUrlAuthorizations interceptUrls) {
interceptUrls
.antMatchers("/favicon.ico", "/robots.txt").permitAll()
.antMatchers("/presentation/**").hasRole("USER")
.antMatchers("/myAccount/**").hasRole("USER")
.antMatchers("/myPost/**").hasRole("AUTHOR")
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/**").permitAll();
}
@Override
protected void configure(HttpConfigurator http) throws Exception {
http
.authenticationEntryPoint(socialAuthenticationEntryPoint())
.addFilterBefore(socialAuthenticationFilter(), AbstractPreAuthenticatedProcessingFilter.class)
.logout()
.deleteCookies("JSESSIONID")
.logoutUrl("/signout")
.logoutSuccessUrl("/")
.permitAll()
.and()
.rememberMe()
.rememberMeServices(rememberMeServices());
}
@Override
protected void registerAuthentication(AuthenticationRegistry registry) throws Exception{
registry
.add(socialAuthenticationProvider())
.add(rememberMeAuthenticationProvider())
.add(userAdminService);
}
}
I feel the code is much easier to read. I like the way my config is written in Method Chaining style. Although Method Chaining is a very subjective coding style, see the discussion in Stack Overflow. I feel it fits very well in configuration, so the code is more readable. It looks like a DSL for security configuration, and it matches old http namespace XML code.
One caveat I found: for any hasRole(role) method call, the passing parameter role
string will be the name of the role without ROLE_ prefix. For example, admin user authority
is ROLE_ADMIN, and in security.xml, we protect admin pages by using
.antMatchers("/admin/**").hasRole("ADMIN").
Another important feature of Spring Security is
HTTP/HTTPS Channel Security. It seems very easy to set it in my SecurityConfig according to the
test code
NamespaceHttpPortMappingsTests.groovy . I can just add three lines of code to secure the admin pages with HTTPS only access:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpConfigurator http) throws Exception {
http
.authenticationEntryPoint(socialAuthenticationEntryPoint())
.addFilterBefore(socialAuthenticationFilter(), AbstractPreAuthenticatedProcessingFilter.class)
.logout()
.deleteCookies("JSESSIONID")
.logoutUrl("/signout")
.logoutSuccessUrl("/")
.permitAll()
.and()
.rememberMe()
.rememberMeServices(rememberMeServices())
.add()
.requiresChannel()
.antMatchers("/signin","/admin/**").requiresSecure()
.antMatchers("/**").requiresInsecure();
}
...
}
Since Cloud Foundry has issues with HTTPS, I cannot test it with my demo website on CloudFoundry.com. From the CloudFoundry.com support forum discussion Is it possible to visit my application via SSL (HTTPS)?:
Cloud Foundry is currently implemented to terminate SSL connections at the router, and all internal communication is unencrypted HTTP.
So I will test secure channel config after I upgrade my AppFog account.