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";
}
}