Spring Security 3.2.1 Multiple login forms with di

2020-02-08 21:57发布

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

}

4条回答
一夜七次
2楼-- · 2020-02-08 22:18

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.

查看更多
Viruses.
3楼-- · 2020-02-08 22:24

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";
    }
}
查看更多
Anthone
4楼-- · 2020-02-08 22:27

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

查看更多
疯言疯语
5楼-- · 2020-02-08 22:33

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.

查看更多
登录 后发表回答