I'm using Spring Security 3.2.1.RELEASE with Spring MVC 4.0.4.RELEASE
I'm trying to setup Spring Security for a web application that will have two distinct login entry pages. I need the pages to be distinct as they will be styled and accessed differently.
First login page is for Admin users and protects admin pages /admin/**
Second login page is for Customer users and protects customer pages /customer/**.
I've attempted to setup two subclasses of WebSecurityConfigurerAdapter configuring individual HttpSecurity objects.
CustomerFormLoginWebSecurity is protecting customer pages and redirecting to customer login page if not authorised.
The AdminFormLoginWebSecurity is protecting admin pages redirecting to admin login page if not authorised.
Unfortunately it seems that only the first of the configurations is enforced. I think that I am missing something extra to make these both work.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
public void registerGlobalAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("customer").password("password").roles("CUSTOMER").and()
.withUser("admin").password("password").roles("ADMIN");
}
@Configuration
@Order(1)
public static class CustomerFormLoginWebSecurity extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/", "/signin/**", "/error/**", "/templates/**", "/resources/**", "/webjars/**");
}
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/customer/**").hasRole("CUSTOMER")
.and()
.formLogin()
.loginPage("/customer_signin")
.failureUrl("/customer_signin?error=1")
.defaultSuccessUrl("/customer/home")
.loginProcessingUrl("/j_spring_security_check")
.usernameParameter("j_username").passwordParameter("j_password")
.and()
.logout()
.permitAll();
http.exceptionHandling().accessDeniedPage("/customer_signin");
}
}
@Configuration
public static class AdminFormLoginWebSecurity extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/", "/signin/**", "/error/**", "/templates/**", "/resources/**", "/webjars/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/admin_signin")
.failureUrl("/admin_signin?error=1")
.defaultSuccessUrl("/admin/home")
.loginProcessingUrl("/j_spring_security_check")
.usernameParameter("j_username").passwordParameter("j_password")
.and()
.logout()
.permitAll();
http.exceptionHandling().accessDeniedPage("/admin_signin");
}
}
}
The component of the spring login chain that redirects to a login page is the authentication filter, and the filter that get's plugged in when using http.formLogin()
is DefaultLoginPageGeneratingFilter
.
This filter either redirects to the login url or builds a default basic login page, if no login page url is provided.
What you need then is a custom authentication filter with the logic to define which login page is needed, and then plug it in the spring security chain in place of the single page authentication filter.
Consider creating a TwoPageLoginAuthenticationFilter
by subclassing DefaultLoginPageGeneratingFilter
and overriding getLoginPageUrl()
, and if that is not sufficient then copy the code and modify it to meet your needs.
This filter is a GenericFilterBean
, so you can declare it like this:
@Bean
public Filter twoPageLoginAuthenticationFilter() {
return new TwoPageLoginAuthenticationFilter();
}
then try building only one http configuration and don't set formLogin()
, but instead do:
http.addFilterBefore(twoPageLoginAuthenticationFilter, ConcurrentSessionFilter.class);
and this will plug the two form authentication filter in the right place in the chain.
The solution that I have come to for multiple login pages involves a single http authentication but I provide my own implementations of
AuthenticationEntryPoint
AuthenticationFailureHandler
LogoutSuccessHandler
What I needed was for these implementations to be able to switch dependent on a token in the request path.
In my website the pages with a customer token in the url are protected and require a user to authenticate as CUSTOMER at the customer_signin page.
So if wanted to goto a page /customer/home then I need to be redirected to the customer_signin page to authenticate first.
If I fail to authenticate on customer_signin then I should be returned to the customer_signin with an error paramater. So that a message can be displayed.
When I am successfully authenticated as a CUSTOMER and then wish to logout then the LogoutSuccessHandler should take me back to the customer_signin page.
I have a similar requirement for admins needing to authenticate at the admin_signin page to access a page with an admin token in the url.
First I defined a class that would allow me to take a list of tokens (one for each type of login page)
public class PathTokens {
private final List<String> tokens = new ArrayList<>();
public PathTokens(){};
public PathTokens(final List<String> tokens) {
this.tokens.addAll(tokens);
}
public boolean isTokenInPath(String path) {
if (path != null) {
for (String s : tokens) {
if (path.contains(s)) {
return true;
}
}
}
return false;
}
public String getTokenFromPath(String path) {
if (path != null) {
for (String s : tokens) {
if (path.contains(s)) {
return s;
}
}
}
return null;
}
public List<String> getTokens() {
return tokens;
}
}
I then use this in PathLoginAuthenticationEntryPoint
to change the login url depending on the token in the request uri.
@Component
public class PathLoginAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
private final PathTokens tokens;
@Autowired
public PathLoginAuthenticationEntryPoint(PathTokens tokens) {
// LoginUrlAuthenticationEntryPoint requires a default
super("/");
this.tokens = tokens;
}
/**
* @param request the request
* @param response the response
* @param exception the exception
* @return the URL (cannot be null or empty; defaults to {@link #getLoginFormUrl()})
*/
@Override
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
return getLoginUrlFromPath(request);
}
private String getLoginUrlFromPath(HttpServletRequest request) {
String requestUrl = request.getRequestURI();
if (tokens.isTokenInPath(requestUrl)) {
return "/" + tokens.getTokenFromPath(requestUrl) + "_signin";
}
throw new PathTokenNotFoundException("Token not found in request URL " + requestUrl + " when retrieving LoginUrl for login form");
}
}
PathTokenNotFoundException extends AuthenticationException so that you can handle it in the usual way.
public class PathTokenNotFoundException extends AuthenticationException {
public PathTokenNotFoundException(String msg) {
super(msg);
}
public PathTokenNotFoundException(String msg, Throwable t) {
super(msg, t);
}
}
Next I provide an implementation of AuthenticationFailureHandler
that looks at the referer url in the request header to determine which login error page to direct the user to.
@Component
public class PathUrlAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final PathTokens tokens;
@Autowired
public PathUrlAuthenticationFailureHandler(PathTokens tokens) {
super();
this.tokens = tokens;
}
/**
* Performs the redirect or forward to the {@code defaultFailureUrl associated with this path} if set, otherwise returns a 401 error code.
* <p/>
* If redirecting or forwarding, {@code saveException} will be called to cache the exception for use in
* the target view.
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
setDefaultFailureUrl(getFailureUrlFromPath(request));
super.onAuthenticationFailure(request, response, exception);
}
private String getFailureUrlFromPath(HttpServletRequest request) {
String refererUrl = request.getHeader("Referer");
if (tokens.isTokenInPath(refererUrl)) {
return "/" + tokens.getTokenFromPath(refererUrl) + "_signin?error=1";
}
throw new PathTokenNotFoundException("Token not found in referer URL " + refererUrl + " when retrieving failureUrl for login form");
}
}
Next I provide an implementation of LogoutSuccessHandler
that will logout the user and redirect them to the correct signin page depending on the token in ther referer url in the request header.
@Component
public class PathUrlLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final PathTokens tokens;
@Autowired
public PathUrlLogoutSuccessHandler(PathTokens tokens) {
super();
this.tokens = tokens;
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
setDefaultTargetUrl(getTargetUrlFromPath(request));
setAlwaysUseDefaultTargetUrl(true);
handle(request, response, authentication);
}
private String getTargetUrlFromPath(HttpServletRequest request) {
String refererUrl = request.getHeader("Referer");
if (tokens.isTokenInPath(refererUrl)) {
return "/" + tokens.getTokenFromPath(refererUrl) + "_signin";
}
throw new PathTokenNotFoundException("Token not found in referer URL " + refererUrl + " when retrieving logoutUrl.");
}
}
The final step is to wire them all together in the security configuration.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired PathLoginAuthenticationEntryPoint loginEntryPoint;
@Autowired PathUrlAuthenticationFailureHandler loginFailureHandler;
@Autowired
PathUrlLogoutSuccessHandler logoutSuccessHandler;
@Bean
public PathTokens pathTokens(){
return new PathTokens(Arrays.asList("customer", "admin"));
}
@Autowired
public void registerGlobalAuthentication(
AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("customer").password("password").roles("CUSTOMER").and()
.withUser("admin").password("password").roles("ADMIN");
}
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/", "/signin/**", "/error/**", "/templates/**", "/resources/**", "/webjars/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http .csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/customer/**").hasRole("CUSTOMER")
.and()
.formLogin()
.loginProcessingUrl("/j_spring_security_check")
.usernameParameter("j_username").passwordParameter("j_password")
.failureHandler(loginFailureHandler);
http.logout().logoutSuccessHandler(logoutSuccessHandler);
http.exceptionHandling().authenticationEntryPoint(loginEntryPoint);
http.exceptionHandling().accessDeniedPage("/accessDenied");
}
}
Once you have this configured you need a controller to to direct to the actual signin page. The SigninControiller below checks the queryString for a value that would indicate a signin error and then sets an attribute used to control an error message.
@Controller
@SessionAttributes("userRoles")
public class SigninController {
@RequestMapping(value = "customer_signin", method = RequestMethod.GET)
public String customerSignin(Model model, HttpServletRequest request) {
Set<String> userRoles = AuthorityUtils.authorityListToSet(SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("userRole", userRoles);
if(request.getQueryString() != null){
model.addAttribute("error", "1");
}
return "signin/customer_signin";
}
@RequestMapping(value = "admin_signin", method = RequestMethod.GET)
public String adminSignin(Model model, HttpServletRequest request) {
Set<String> userRoles = AuthorityUtils.authorityListToSet(SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("userRole", userRoles);
if(request.getQueryString() != null){
model.addAttribute("error", "1");
}
return "signin/admin_signin";
}
}
Maybe this post could help you :
Multiple login forms
It's a different version of spring security but the same problem : only the first configuration is taken.
It seems it has been solved by changing login-processing-url for one of the two login pages but people suggest to use the same url processing but a different layout using ViewResolver. It is a solution if you use the same mechanism to authenticate users (the authentication mechanism is the thing responsible for processing the credentials that the browser is sending).
This post also seems to say that if you change your loginProcessingUrl you will succeed :
Configuring Spring Security 3.x to have multiple entry points
I also encountered this problem and found out that I missed the first filtering part.
This one:
http.csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
Should be:
http.csrf().disable()
.antMatcher("/admin/**")
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
Adding the first filtering .antMatcher("/admin/**") will first filter it so that it will use the AdminFormLoginWebSecurity instead of the other one.