Enabled CSRF in my Spring MVC application using Spring security 3.2.
My spring-security.xml
<http>
<intercept-url pattern="/**/verify" requires-channel="https"/>
<intercept-url pattern="/**/login*" requires-channel="http"/>
...
...
<csrf />
</http>
Trying to disable CSRF for requests that contain 'verify' in request URL.
MySecurityConfig.java
#Configuration
#EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
private CsrfMatcher csrfRequestMatcher = new CsrfMatcher();
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().requireCsrfProtectionMatcher(csrfRequestMatcher);
}
class CsrfMatcher implements RequestMatcher {
#Override
public boolean matches(HttpServletRequest request) {
if (request.getRequestURL().indexOf("verify") != -1)
return false;
else if (request.getRequestURL().indexOf("homePage") != -1)
return false;
return true;
}
}
}
Csrf filter validates CSRF token that is submitted from 'verify' and Invalid token exception (403) is thrown as I'm submitting request to https from http. How can I disable csrf token authentication in such a scenario ?
I know this is not a direct answer, but people (as me) usually don't specify spring's version when searching for this kinds of questions.
So, since spring security a method exists that lets ignore some routes:
The following will ensure CSRF protection ignores:
Any GET, HEAD, TRACE, OPTIONS (this is the default)
We also explicitly state to ignore any request that starts with "/sockjs/"
http
.csrf()
.ignoringAntMatchers("/sockjs/**")
.and()
...
I hope that my answer can help someone else. I found this question searching for How to disable CSFR for specfic URLs in Spring Boot.
I used the solution described here:
http://blog.netgloo.com/2014/09/28/spring-boot-enable-the-csrf-check-selectively-only-for-some-requests/
This is the Spring Security configuration that allow me to disable the CSFR control on some URLs:
#Configuration
#EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// Build the request matcher for CSFR protection
RequestMatcher csrfRequestMatcher = new RequestMatcher() {
// Disable CSFR protection on the following urls:
private AntPathRequestMatcher[] requestMatchers = {
new AntPathRequestMatcher("/login"),
new AntPathRequestMatcher("/logout"),
new AntPathRequestMatcher("/verify/**")
};
#Override
public boolean matches(HttpServletRequest request) {
// If the request match one url the CSFR protection will be disabled
for (AntPathRequestMatcher rm : requestMatchers) {
if (rm.matches(request)) { return false; }
}
return true;
} // method matches
}; // new RequestMatcher
// Set security configurations
http
// Disable the csrf protection on some request matches
.csrf()
.requireCsrfProtectionMatcher(csrfRequestMatcher)
.and()
// Other configurations for the http object
// ...
return;
} // method configure
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
// Authentication manager configuration
// ...
}
}
It works with Spring Boot 1.2.2 (and Spring Security 3.2.6).
I am using Spring Security v4.1. After a lot of reading and testing, I disable the CSRF security feature for specific URLs using XML configuration.
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<http pattern="/files/**" security="none" create-session="stateless"/>
<http>
<intercept-url pattern="/admin/**" access="hasAuthority('GenericUser')" />
<intercept-url pattern="/**" access="permitAll" />
<form-login
login-page="/login"
login-processing-url="/login"
authentication-failure-url="/login"
default-target-url="/admin/"
password-parameter="password"
username-parameter="username"
/>
<logout delete-cookies="JSESSIONID" logout-success-url="/login" logout-url="/admin/logout" />
<http-basic />
<csrf request-matcher-ref="csrfMatcher"/>
</http>
<beans:bean id="csrfMatcher" class="org.springframework.security.web.util.matcher.OrRequestMatcher">
<beans:constructor-arg>
<util:list value-type="org.springframework.security.web.util.matcher.RequestMatcher">
<beans:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<beans:constructor-arg name="pattern" value="/rest/**"/>
<beans:constructor-arg name="httpMethod" value="POST"/>
</beans:bean>
<beans:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<beans:constructor-arg name="pattern" value="/rest/**"/>
<beans:constructor-arg name="httpMethod" value="PUT"/>
</beans:bean>
<beans:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<beans:constructor-arg name="pattern" value="/rest/**"/>
<beans:constructor-arg name="httpMethod" value="DELETE"/>
</beans:bean>
</util:list>
</beans:constructor-arg>
</beans:bean>
//...
</beans:bean>
With the above configuration, I enable the CSRF security only for POST|PUT|DELETE requests of all URLs which start with /rest/.
Explicitly disable for specific url patterns and enable for some url patterns.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#EnableWebSecurity
public class SecurityConfig {
#Configuration
#Order
public static class GeneralWebSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers("/rest/**").and()
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/home/**","/search/**","/geo/**").authenticated().and().csrf()
.and().formLogin().loginPage("/login")
.usernameParameter("username").passwordParameter("password")
.and().exceptionHandling().accessDeniedPage("/error")
.and().sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
}
}
}
<http ...>
<csrf request-matcher-ref="csrfMatcher"/>
<headers>
<frame-options policy="SAMEORIGIN"/>
</headers>
...
</http>
<b:bean id="csrfMatcher"
class="AndRequestMatcher">
<b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
<b:constructor-arg>
<b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
<b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<b:constructor-arg value="/chat/**"/>
</b:bean>
</b:bean>
</b:constructor-arg>
</b:bean>
mean of
http
.csrf()
// ignore our stomp endpoints since they are protected using Stomp headers
.ignoringAntMatchers("/chat/**")
example from :
https://docs.spring.io/spring-security/site/docs/4.1.x/reference/htmlsingle/
Use security="none".
for e.g in spring-security-config.xml
<security:intercept-url pattern="/*/verify" security="none" />
Related
I need to allow a health check api to be available without JWT authentication. Way it is setup is below.
Adding intercept-url for pattern '/health' will not work as filters are called before intercept.
How can i make this security apply only when pattern is not '/health'.
<security:http entry-point-ref="apiAuthenticationEntryPoint"
authentication-manager-ref="authenticationManager"
create-session="never" >
<security:intercept-url pattern="/health" access="permitAll"/>
<security:custom-filter ref="apiAuthenticationFilter"
position="PRE_AUTH_FILTER"/>
<security:access-denied-handler ref="accessDeniedHandler"/>
</security:http>
<security:global-method-security jsr250-annotations="enabled"
secured-annotations="enabled"
pre-post-annotations="enabled"/>
<security:authentication-manager id="authenticationManager">
<security:authentication-provider ref="accessTokenAuthenticationProvider"/>
<security:authentication-provider ref="jwtAuthenticationProvider"/>
</security:authentication-manager>
Try to convert it to xml view. It should work for you
#EnableWebSecurity
class ResourceServerConfiguration : WebSecurityConfigurerAdapter() {
private val ROLE = "ROLE_USER"
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.antMatchers("/health/**").permitAll()
.antMatchers("/api/**").hasAnyAuthority(ROLE)
.anyRequest().denyAll()
.and()
.httpBasic()
}
}
I am working on a Spring Boot project, which uses LDAP in Spring Security for authentication.
I need to automate the login once the user hits the login page based on the roles in LDAP group provided in Spring Security.
If user has any role in the group mentioned in LDAP, then it must redirect to the corresponding page after login. (i.e page1 in my example).
I have been searching 2 days in a row for this for any online documentation or an example, but in vain. All I could find is using a jdbcDataSource or hard coding the username and password in Controller and later validating it when login or through Spring using web.xml. But not via LDAP. Any help would be much helpful.
This is how my Spring Security XML looks:
<?xml version="1.0" encoding="UTF-8" ?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/login" access="permitAll" />
<intercept-url pattern="/logout" access="permitAll" />
<intercept-url pattern="/webjars/**" access="permitAll" />
<intercept-url pattern="/page1" access="hasAnyRole('GP1','GP2')" />
<intercept-url pattern="/page2" access="hasAnyRole('GP1','GP2')" />
<intercept-url pattern="/page3" access="hasAnyRole('GP1','GP2')" />
<intercept-url pattern="/**" access="permitAll" />
<form-login default-target-url="/page1" login-page="/login"
always-use-default-target="true" />
<access-denied-handler error-page="/403.html" />
<csrf disabled="true" />
<logout logout-url="/logout" />
</http>
<authentication-manager alias="authenticationManager"
erase-credentials="false">
<authentication-provider ref="ldapAuthProvider" />
</authentication-manager>
<ldap-server id="contextSource" url="ldap://url"
manager-dn="mymanagerdn" manager-password="mymanagerpswd" />
<beans:bean id="ldapAuthProvider"
class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
<beans:constructor-arg>
<beans:bean id="bindAuthenticator"
class="org.springframework.security.ldap.authentication.BindAuthenticator">
<beans:constructor-arg ref="contextSource" />
<beans:property name="userSearch" ref="userSearch" />
</beans:bean>
</beans:constructor-arg>
<beans:constructor-arg>
<beans:bean
class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
<beans:constructor-arg ref="contextSource" />
<beans:constructor-arg value="myDCvalues" />
<beans:property name="searchSubtree" value="true" />
<beans:property name="ignorePartialResultException"
value="true" />
<beans:property name="groupSearchFilter" value="(member={0})" />
</beans:bean>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="userSearch"
class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
<beans:constructor-arg index="0"
value="myDCvalues" />
<beans:constructor-arg index="1"
value="(sAMAccountName={0})" />
<beans:constructor-arg index="2" ref="contextSource" />
<beans:property name="searchSubtree" value="true" />
</beans:bean>
</beans:beans>
My WebController:
package com.myPackage;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
#Controller
public class WebController extends WebMvcConfigurerAdapter {
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/page1").setViewName("page1");
registry.addViewController("/page2").setViewName("page2");
registry.addViewController("/page3").setViewName("page3");
registry.addViewController("/login").setViewName("login");
registry.addViewController("/403").setViewName("error/403");
}
#GetMapping("/page1")
public String page1(HttpSession session) {
return "page1";
}
#GetMapping("/page2")
public String page2(HttpSession session) {
return "page2";
}
#GetMapping("/page3")
public String page3(HttpSession session) {
return "page3";
}
#GetMapping("/login")
public String login() {
return "login";
}
#GetMapping("/403")
public String error403() {
return "error/403";
}
#Bean
public ViewResolver getViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("templates/");
return resolver;
}
#Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
private String getCredentials() {
String credential = null;
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
credential = userDetails.getUsername().toString();
return credential;
}
}
For your convenience, this answer comes with a complete and working sample, including an LDAP server, populated with users and groups and an integration test, so that you can run these tests yourself.
Assume you have the following user in LDAP
dn: cn=marissa,ou=Users,dc=test,dc=com
changetype: add
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: marissa
userPassword: koala
uid: 20f459e0-e30b-4d1f-998c-3ded7f769db1
mail: marissa#test.com
sn: Marissa
The username is marissa and the password is koala.
Let's start with the test case:
#Test
#DisplayName("ldap login works")
void doLogin() throws Exception {
mvc.perform(
MockMvcRequestBuilders.post("/login")
.param("username", "marissa")
.param("password", "koala")
.with(csrf())
)
.andExpect(status().is3xxRedirection())
.andExpect(authenticated())
;
}
From this test, we can deduce that
LDAP uses form login, username/password
The form has CSRF protection
So let's configure your Spring Boot application using Java config
The classic sample file, SecurityConfig.java
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
The security configuration doesn't change.
We want users to be fully authenticated
We want to use form login
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin()
;
}
That's it, next we configure LDAP, again in your SecurityConfig.java we do this by calling the AuthenticationManagerBuilder. This is a bean that Spring Security configures. So we can access it using #Autowired
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.contextSource()
.url("ldap://localhost:389")
.managerDn("cn=admin,dc=test,dc=com")
.managerPassword("password")
.and()
.userSearchBase("ou=Users,dc=test,dc=com")
.userSearchFilter("cn={0}")
.groupSearchBase("dc=test,dc=com")
.groupSearchFilter("member={0}")
;
}
and that's it. Now, I used some code I wrote from the Cloud Foundry UAA project to create an in memory LDAP server for my integration tests.
So when the mock MVC integration test starts up, it starts an LDAP server to run against.
It really is that simple. You can now expand this sample to map LDAP groups to Spring Security authorities.
The LDAP Sample is available in my community repo: https://github.com/fhanik/spring-security-community.git
I have a fairly out-of-the-box Spring Security 3.2 J2EE xml config that I have almost finished converting to Java config.
The Before xml file:
<sec:global-method-security pre-post-annotations="enabled" />
<sec:authentication-manager />
<sec:http pattern="/css/**" security="none" />
<sec:http pattern="/js/**" security="none" />
....
<sec:http auto-config="true" create-session="never" use-expressions="true>
<sec:session-management session-fixation-protection="none" />
<sec:jee mappable-roles="admin,user" />
<sec:intercept-url pattern="/operations/admin/**" access="hasRole('ROLE_admin')" />
<sec:intercept-url pattern="/**" access="permitAll" />
</sec:http>
The self closing authentication-manager tag is my issue. It's picking up the PreAuthenticatedAuthenticationProvider created by the jee tag. I'm not quite sure how to replicate it in the Java Config:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
#ImportResource("classpath:security-context.xml")
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{
#Override
public void configure(WebSecurity web) throws Exception{
web
.ignoring.antMatchers("/css/**")
.and()
.ignoring.antMatchers("/js/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception{
http
.sessionManagement()
.sessionFixation().none()
.sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.csrf().disable()
.jee()
.mappableAuthorities("admin","user")
.and()
.authorizeRequests()
.antMatchers("/operations/admin/**").hasAuthority("admin")
.anyRequest.permitAll();
}
}
This is working now only because I am importing my old security-context.xml which has nothing in it except the authentication-manager tag.
I have playing around with declaring an AuthenticationManagerBuilder bean, but it seems like everything requires a specific reference to an AuthenticationProvider or UserDetailsService to work. The ProviderManager default constructor is deprecated.
I know that the jee() entry adds the PreAuthenticatedAuthenticationProvider to the sharedObjects inside HttpSecurity, so I could go through the trouble of getting the PreAuthenticatedAuthenticationProvider out of the sharedObjects to create an AuthenticationManager if necessary, but it seems like there ought to be a simple Java config counterpart to the self-closing xml tag that I am just missing.
Can you try this on your SpringSecurityConfig class :-
#Autowired
public void registerGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new AuthenticationManagerBeanDefinitionParser.NullAuthenticationProvider());
}
I am using spring basic authentication with the following settings in my security xml:
<http use-expressions="true" create-session="never" >
<intercept-url pattern="/**" method="GET" access="isAuthenticated()" />
<intercept-url pattern="/**" method="POST" access="isAuthenticated()" />
<intercept-url pattern="/**" method="PUT" access="isAuthenticated()" />
<intercept-url pattern="/**" method="DELETE" access="isAuthenticated()" />
<http-basic />
</http>
If authentication fails, the browser pop ups a prompt window to renter the user name and password.
Is there any way to make that prompt not pop up at all ?
Most probable the page that is used for authentication failure is also protected. You can try manually to set the failure page to one that is not protected like
<access-denied-handler error-page="/login.jsp"/>
together with
<intercept-url pattern="/*login*" access="hasRole('ROLE_ANONYMOUS')"/>
or
<intercept-url pattern='/*login*' filters='none'/>
or you can use the auto-config='true' attribute of the http element that will fix that for you.See more here
I have also had the same problem for the REST API throwing login dialog in the browser. As you have told , when the browser sees the response header as
WWW-Authenticate: Basic realm="Spring Security Application
It will prompt with a basic authentication dialog.For REST API based login , this is not ideal. Here is how I did it.Define a custom authentication entry point and in the commence
set the header as "FormBased"
response.setHeader("WWW-Authenticate", "FormBased");
application-context.xml configuration below
<security:http create-session="never" entry-point-ref="authenticationEntryPoint" authentication-manager-ref="authenticationManager">
<security:custom-filter ref="customRestFilter" position="BASIC_AUTH_FILTER" />
<security:intercept-url pattern="/api/**" access="ROLE_USER" />
</security:http>
<bean id="authenticationEntryPoint" class="com.tito.demo.workflow.security.RestAuthenticationEntryPoint">
</bean>
Custom entry point class below.
#Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static Logger logger = Logger.getLogger(RestAuthenticationEntryPoint.class);
public void commence( HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException ) throws IOException {
logger.debug("<--- Inside authentication entry point --->");
// this is very important for a REST based API login.
// WWW-Authenticate header should be set as FormBased , else browser will show login dialog with realm
response.setHeader("WWW-Authenticate", "FormBased");
response.setStatus( HttpServletResponse.SC_UNAUTHORIZED );
}
}
Note: I have used spring 3.2.5.Release
Now when the rest API is hit from a restclient like POSTMAN , the server will return 401 Unauthorized.
I have faced the same issue and what I did is create a custom RequestMatcher in my resource server. This prevents Outh2 from sending WWW-Authenticate header.
Example:
#Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new OAuthRequestedMatcher())
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
}
private static class OAuthRequestedMatcher implements RequestMatcher {
public boolean matches(HttpServletRequest request) {
String auth = request.getHeader("Authorization");
boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer");
boolean haveAccessToken = request.getParameter("access_token")!=null;
return haveOauth2Token || haveAccessToken;
}
}
I have implemented this security proccess in my project:
Spring Security 3 - MVC Integration Tutorial (Part 2).
My problem is that I need to turn it into an Ajax-based login.
What do I need to do in order to make this XML suitable with just returning string/JSON to the client?
I understand that the problem might be in the form-login tag.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<!-- This is where we configure Spring-Security -->
<http auto-config="true" use-expressions="true" access-denied-page="/Management/auth/denied" >
<intercept-url pattern="/Management/auth/login" access="permitAll"/>
<intercept-url pattern="/Management/main/admin" access="hasRole('ROLE_ADMIN')"/>
<intercept-url pattern="/Management/main/common" access="hasRole('ROLE_USER')"/>
<form-login
login-page="/Management/auth/login"
authentication-failure-url="/Management/auth/login?error=true"
default-target-url="/Management/main/common"/>
<logout
invalidate-session="true"
logout-success-url="/Management/auth/login"
logout-url="/Management/auth/logout"/>
</http>
<!-- Declare an authentication-manager to use a custom userDetailsService -->
<authentication-manager>
<authentication-provider user-service-ref="customUserDetailsService">
<password-encoder ref="passwordEncoder"/>
</authentication-provider>
</authentication-manager>
<!-- Use a Md5 encoder since the user's passwords are stored as Md5 in the database -->
<beans:bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="passwordEncoder"/>
<!-- A custom service where Spring will retrieve users and their corresponding access levels -->
<beans:bean id="customUserDetailsService" class="com.affiliates.service.CustomUserDetailsService"/>
</beans:beans>
This is an old post, but it still comes up as one of the top results for "spring security ajax login," so I figured I'd share my solution. It follows Spring Security standards and is pretty simple to setup, the trick is to have 2 <http> elements in your security configuration, one for REST/Ajax and one for the rest of the app (regular HTML pages). The order in which <http>'s appear is important, it has to go from more specific to more generic URLs, just like <url-intercept> elements inside of a <http>.
Step 1: Setup Two Separate <http>'s
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<!-- a shared request cache is required for multiple http elements -->
<beans:bean id="requestCache" class="org.springframework.security.web.savedrequest.HttpSessionRequestCache" />
<!-- remove security from static resources to avoid going through the security filter chain -->
<http pattern="/resources/**" security="none" />
<!-- http config for REST services (AJAX interface)
=================================================== -->
<http auto-config="true" use-expressions="true" pattern="/rest/**">
<!-- login configuration
login-processing-url="/rest/security/login-processing" front-end AJAX requests for authentication POST to this URL
login-page="/rest/security/login-page" means "authentication is required"
authentication-failure-url="/rest/security/authentication-failure" means "authentication failed, bad credentials or other security exception"
default-target-url="/rest/security/default-target" front-end AJAX requests are redirected here after success authentication
-->
<form-login
login-processing-url="/rest/security/login-processing"
login-page="/rest/security/login-page"
authentication-failure-url="/rest/security/authentication-failure"
default-target-url="/rest/security/default-target"
always-use-default-target="true" />
<logout logout-url="/rest/security/logout-url" />
<!-- REST services can be secured here, will respond with JSON instead of HTML -->
<intercept-url pattern="/rest/calendar/**" access="hasRole('ROLE_USER')" />
<!-- other REST intercept-urls go here -->
<!-- end it with a catch all -->
<intercept-url pattern="/rest/**" access="isAuthenticated()" />
<!-- reference to the shared request cache -->
<request-cache ref="requestCache"/>
</http>
<!-- http config for regular HTML pages
=================================================== -->
<http auto-config="true" use-expressions="true">
<form-login
login-processing-url="/security/j_spring_security_check"
login-page="/login"
authentication-failure-url="/login?login_error=t" />
<logout logout-url="/security/j_spring_security_logout" />
<intercept-url pattern="/calendar/**" access="hasRole('ROLE_USER')" />
<!-- other intercept-urls go here -->
<!-- in my app's case, the HTML config ends with permitting all users and requiring HTTPS
it is always a good idea to send sensitive information like passwords over HTTPS -->
<intercept-url pattern="/**" access="permitAll" requires-channel="https" />
<!-- reference to the shared request cache -->
<request-cache ref="requestCache"/>
</http>
<!-- authentication manager and other configuration go below -->
</beans:beans>
Step 2: REST Authentication Controller
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import flexjson.JSONSerializer;
#Controller
#RequestMapping(value = "/rest/security")
public class RestAuthenticationController {
public HttpHeaders getJsonHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
return headers;
}
#RequestMapping(value="/login-page", method = RequestMethod.GET)
public ResponseEntity<String> apiLoginPage() {
return new ResponseEntity<String>(getJsonHeaders(), HttpStatus.UNAUTHORIZED);
}
#RequestMapping(value="/authentication-failure", method = RequestMethod.GET)
public ResponseEntity<String> apiAuthenticationFailure() {
// return HttpStatus.OK to let your front-end know the request completed (no 401, it will cause you to go back to login again, loops, not good)
// include some message code to indicate unsuccessful login
return new ResponseEntity<String>("{\"success\" : false, \"message\" : \"authentication-failure\"}", getJsonHeaders(), HttpStatus.OK);
}
#RequestMapping(value="/default-target", method = RequestMethod.GET)
public ResponseEntity<String> apiDefaultTarget() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// exclude/include whatever fields you need
String userJson = new JSONSerializer().exclude("*.class", "*.password").serialize(authentication);
return new ResponseEntity<String>(userJson, getJsonHeaders(), HttpStatus.OK);
}
}
Step 3: Submit AJAX form and process the response, required jQuery's ajaxForm library
<form action="/rest/security/login-processing" method="POST">
...
</form>
$('form').ajaxForm({
success: function(response, statusText, xhr, $form) {
console.log(response);
if(response == null || response.username == null) {
alert("authentication failure");
} else {
// response is JSON version of the Spring's Authentication
alert("authentication success");
}
},
error: function(response, statusText, error, $form) {
if(response != null && response.message == "authentication-failure") {
alert("authentication failure");
}
}
});
Spring is shifting away from XML based configurations and towards Java #Configuration classes. Below is #Configuration version of the setup explained in the post above (Spring Security Ajax login). Steps 2 & 3 remain the same, replace Step 1 with this code. The order is once again important, more specific definitions needs to be loaded before more generic ones, use #Order(1) and #Order(2) to control that.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
#Configuration
#EnableWebSecurity
public class WebMvcSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Bean(name = "requestCache")
public RequestCache getRequestCache() {
return new HttpSessionRequestCache();
}
#Configuration
#Order(1)
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Autowired private RequestCache requestCache;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.regexMatcher("/rest.*")
.authorizeRequests()
.antMatchers("/rest/calendar/**")
.hasAuthority("ROLE_USER")
.antMatchers("/rest/**")
.permitAll()
.and()
.headers()
.xssProtection()
.and()
.logout()
.logoutUrl("/rest/security/logout-url")
.and()
.requestCache()
.requestCache(requestCache)
.and()
.formLogin()
.loginProcessingUrl("/rest/security/login-processing")
.loginPage("/rest/security/login-page")
.failureUrl("/rest/security/authentication-failure")
.defaultSuccessUrl("/rest/security/default-target", false)
.and()
.httpBasic();
}
}
#Configuration
#Order(2)
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Autowired private RequestCache requestCache;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.regexMatchers("/calendar/.*")
.hasAuthority("ROLE_USER")
.regexMatchers("/.*")
.permitAll()
.and()
.logout()
.logoutUrl("/security/j_spring_security_logout")
.and()
.requestCache()
.requestCache(requestCache)
.and()
.formLogin()
.loginProcessingUrl("/security/j_spring_security_check")
.loginPage("/login")
.failureUrl("/login?login_error=t" )
.and()
.httpBasic();
}
}
#Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/**")
.antMatchers("/sitemap.xml");
}
}
It depends on the implementation of your ajax-login. In any case, I guess you need to implement a custom filter. There are two good tutorials for using Spring Security with ExtJs:
Integrating Spring Security 3 with Extjs
Integrating Spring Security with ExtJS Login Page
It should work very similar for other Ajax login-forms.
You can use HttpServletRequest.login(username,password) to login, just like:
#Controller
#RequestMapping("/login")
public class AjaxLoginController {
#RequestMapping(method = RequestMethod.POST)
#ResponseBody
public String performLogin(
#RequestParam("username") String username,
#RequestParam("password") String password,
HttpServletRequest request, HttpServletResponse response) {
try {
request.login(username,password);
return "{\"status\": true}";
} catch (Exception e) {
return "{\"status\": false, \"error\": \"Bad Credentials\"}";
}
}