Spring Security 3.2.1具有不同WebSecurityConfigurerAdapters的多个登录表单

时间:2014-04-03 18:01:36

标签: java spring spring-mvc spring-security

我在Spring MVC 4.0.4.RELEASE中使用Spring Security 3.2.1.RELEASE

我正在尝试为具有两个不同登录条目页面的Web应用程序设置Spring Security。我需要将页面区分开来,因为它们的样式和访问方式不同。

首次登录页面适用于管理员用户并保护管理页面/ admin / **

第二个登录页面适用于客户用户并保护客户页面/客户/**.

我试图设置两个WebSecurityConfigurerAdapter子类来配置各个HttpSecurity对象。

如果未经授权,CustomerFormLoginWebSecurity正在保护客户页面并重定向到客户登录页面。 如果未经授权,AdminFormLoginWebSecurity正在保护管理页面重定向到管理员登录页面。

不幸的是,似乎只强制执行第一个配置。我认为我错过了一些额外的东西,以使这两者都有效。

@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 个答案:

答案 0 :(得分:7)

重定向到登录页面的Spring登录链的组件是身份验证过滤器,使用http.formLogin()时插入的过滤器是DefaultLoginPageGeneratingFilter

如果没有提供登录页面网址,此过滤器会重定向到登录网址或构建默认的基本登录页面。

您需要的是一个自定义身份验证过滤器,其逻辑用于定义所需的登录页面,然后将其插入spring安全链中以代替单页身份验证过滤器。

考虑通过继承TwoPageLoginAuthenticationFilter并覆盖DefaultLoginPageGeneratingFilter来创建getLoginPageUrl(),如果这还不够,请复制代码并对其进行修改以满足您的需求。

此过滤器为GenericFilterBean,因此您可以这样声明:

@Bean
public Filter twoPageLoginAuthenticationFilter() {
    return new TwoPageLoginAuthenticationFilter();
}

然后尝试只构建一个http配置,不要设置formLogin(),而是设置:

http.addFilterBefore(twoPageLoginAuthenticationFilter, ConcurrentSessionFilter.class);

这会将两个表单身份验证过滤器插入链中的正确位置。

答案 1 :(得分:7)

我为多个登录页面提供的解决方案涉及单个http身份验证,但我提供了自己的实现

  • AuthenticationEntryPoint
  • AuthenticationFailureHandler
  • LogoutSuccessHandler

我需要的是这些实现能够依赖于请求路径中的令牌进行切换。

在我的网站中,网址中包含客户令牌的网页受到保护,需要用户在customer_signin页面上以CUSTOMER身份进行身份验证。 因此,如果想要转到页面/客户/主页,那么我需要重定向到customer_signin页面以进行首先进行身份验证。 如果我无法在customer_signin上进行身份验证,那么我应该返回带有错误参数的customer_signin。这样就可以显示一条消息 当我成功通过CUSTOMER认证然后希望注销时,LogoutSuccessHandler会将我带回customer_signin页面。

我对admin需要在admin_signin页面进行身份验证以访问网址中包含管理令牌的网页有类似的要求。

首先,我定义了一个允许我获取令牌列表的类(每种类型的登录页面都有一个)

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

然后我在PathLoginAuthenticationEntryPoint中使用它来根据请求uri中的令牌更改登录URL。

@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扩展AuthenticationException,以便您可以通常的方式处理它。

public class PathTokenNotFoundException extends AuthenticationException {

   public PathTokenNotFoundException(String msg) {
       super(msg);
    }

    public PathTokenNotFoundException(String msg, Throwable t) {
       super(msg, t);
    }
}

接下来,我提供AuthenticationFailureHandler的实现,该实现查看请求标头中的referer url,以确定将用户定向到哪个登录错误页面。

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

接下来,我提供LogoutSuccessHandler的实现,该实现将注销用户并将其重定向到正确的登录页面,具体取决于请求标头中的参考URL中的令牌。

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

最后一步是在安全配置中将它们连接在一起。

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

配置完成后,您需要一个控制器来指向实际的登录页面。下面的SigninControiller检查queryString是否有指示signin错误的值,然后设置用于控制错误消息的属性。

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

答案 2 :(得分:1)

也许这篇文章可以帮到你: Multiple login forms

这是弹簧安全性的不同版本,但同样的问题:只采取了第一种配置。

似乎已经通过更改两个登录页面之一的login-processing-url来解决,但是人们建议使用相同的URL处理,但使用ViewResolver使用不同的布局。如果您使用相同的机制对用户进行身份验证(验证机制是负责处理浏览器正在发送的凭据的事物),那么这是一种解决方案。

这篇文章似乎也说如果你改变你的loginProcessingUrl你会成功: Configuring Spring Security 3.x to have multiple entry points

答案 3 :(得分:0)

我也遇到了这个问题,发现我错过了第一个过滤部分。

这一个:

http.csrf().disable()
    .authorizeRequests()
    .antMatchers("/admin/**").hasRole("ADMIN")

应该是:

http.csrf().disable()
    .antMatcher("/admin/**")
    .authorizeRequests()
    .antMatchers("/admin/**").hasRole("ADMIN")

添加第一个过滤.antMatcher(&#34; / admin / **&#34;)将首先过滤它,以便它将使用AdminFormLoginWebSecurity而不是另一个。