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

Posted

技术标签:

【中文标题】Spring Security 3.2.1 具有不同 WebSecurityConfigurerAdapters 的多个登录表单【英文标题】:Spring Security 3.2.1 Multiple login forms with distinct WebSecurityConfigurerAdapters 【发布时间】:2014-05-15 17:52:05 【问题描述】:

我正在使用 Spring Security 3.2.1.RELEASE 和 Spring MVC 4.0.4.RELEASE

我正在尝试为具有两个不同登录入口页面的 Web 应用程序设置 Spring Security。我需要页面是不同的,因为它们的样式和访问方式会有所不同。

第一个登录页面供管理员用户使用,并保护管理页面 /admin/**

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

我尝试设置 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");
        
    


【问题讨论】:

【参考方案1】:

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

如果没有提供登录页面 url,此过滤器要么重定向到登录 url,要么构建一个默认的基本登录页面。

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

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

这个过滤器是一个GenericFilterBean,所以你可以这样声明它:

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

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

http.addFilterBefore(twoPageLoginAuthenticationFilter, ConcurrentSessionFilter.class);

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

【讨论】:

当我们覆盖 getLoginPageUrl() 时,我们需要获取请求的 URL 以重定向到特定页面?【参考方案2】:

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

AuthenticationEntryPoint AuthenticationFailureHandler LogoutSuccessHandler

我需要的是让这些实现能够根据请求路径中的令牌进行切换。

在我的网站中,url 中带有客户令牌的页面受到保护,并要求用户在 customer_signin 页面上以客户身份进行身份验证。 因此,如果想转到 /customer/home 页面,则需要先重定向到 customer_signin 页面进行身份验证。 如果我未能在 customer_signin 上进行身份验证,那么我应该返回到 customer_signin 并带有错误参数。以便显示消息。 当我成功通过 CUSTOMER 身份验证然后希望注销时,LogoutSuccessHandler 应该将我带回 customer_signin 页面。

对于需要在 admin_signin 页面进行身份验证以访问 url 中具有管理员令牌的页面的管理员,我有类似的要求。

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

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 的实现,它将注销用户并根据请求标头中的引用网址中的令牌将他们重定向到正确的登录页面。

@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 中是否存在指示登录错误的值,然后设置用于控制错误消息的属性。

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

【讨论】:

在 PathTokens 类中你在哪里定义? user_signin 还是 admin_signin? 您正在使用 :- return new PathTokens(Arrays.asList("customer", "admin")); 填写路径标记 我们需要在控制器中有 admin_signin 和 customer_signin 映射吗? 我已经开始工作了,但是登录后如果我需要去 admin/home 或者客户到客户/home?请帮我解决这个问题 request.getHeader("Referer");在注销处理程序中为空【参考方案3】:

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

这是一个不同版本的 spring security 但同样的问题:只采用第一个配置。

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

这篇文章似乎还说,如果您更改 loginProcessingUrl,您将成功: Configuring Spring Security 3.x to have multiple entry points

【讨论】:

【参考方案4】:

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

这个:

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

应该是:

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

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

【讨论】:

以上是关于Spring Security 3.2.1 具有不同 WebSecurityConfigurerAdapters 的多个登录表单的主要内容,如果未能解决你的问题,请参考以下文章

具有会话获取在线用户的 Spring Security 返回空

Spring Security Oauth2 组

Spring Security - 不访问数据库

如何为具有 Spring Security 配置的 Spring Boot API 编写单元测试

具有Spring Security的多个用户

具有 CAS 身份验证和自定义授权的 Spring Security