自定义 Spring Security 应用程序中的无限循环

Posted

技术标签:

【中文标题】自定义 Spring Security 应用程序中的无限循环【英文标题】:Infinite loop in custom Spring Security application 【发布时间】:2015-04-06 17:48:23 【问题描述】:

我们尝试用现有的 Spring Security Basic Login 替换开源应用程序中的 REST-API,以实现使用令牌的自定义登录。我阅读了有关该主题的博文:http://javattitude.com/2014/06/07/spring-security-custom-token-based-rest-authentication/

当请求没有名为“Cookie”的标头时,我会得到正确的 401 - 未经授权的响应(预期行为)。当请求具有有效令牌时,我得到一个无限循环,导致java.lang.***Error

    Exception in thread "http-bio-8080-exec-45" java.lang.***Error
        at org.apache.tomcat.util.http.NamesEnumerator.<init>(MimeHeaders.java:402)
        at org.apache.tomcat.util.http.MimeHeaders.names(MimeHeaders.java:228)
        at org.apache.catalina.connector.Request.getHeaderNames(Request.java:2108)
        at org.apache.catalina.connector.RequestFacade.getHeaderNames(RequestFacade.java:726)
        at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
        at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
        at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
        at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
        at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
        at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
at org.activiti.rest.security.CustomTokenAuthenticationFilter.attemptAuthentication(CustomTokenAuthenticationFilter.java:43)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:211)
    at org.activiti.rest.security.CustomTokenAuthenticationFilter.doFilter(CustomTokenAuthenticationFilter.java:86)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:65)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:166)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:749)
    at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:487)
    at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:412)
    at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:339)
    at org.springframework.security.web.firewall.RequestWrapper$FirewalledRequestAwareRequestDispatcher.forward(RequestWrapper.java:132)
    at org.activiti.rest.security.TokenSimpleUrlAuthenticationSuccessHandler.onAuthenticationSuccess(TokenSimpleUrlAuthenticationSuccessHandler.java:30)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:331)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:298)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:235)
    at org.activiti.rest.security.CustomTokenAuthenticationFilter.doFilter(CustomTokenAuthenticationFilter.java:86)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:65)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:166)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:749)
    at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:487)
    at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:412)
    at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:339)
    at org.springframework.security.web.firewall.RequestWrapper$FirewalledRequestAwareRequestDispatcher.forward(RequestWrapper.java:132)
    at org.activiti.rest.security.TokenSimpleUrlAuthenticationSuccessHandler.onAuthenticationSuccess(TokenSimpleUrlAuthenticationSuccessHandler.java:30)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:331)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:298)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:235)
    at org.activiti.rest.security.CustomTokenAuthenticationFilter.doFilter(CustomTokenAuthenticationFilter.java:86)

我的 Spring 安全配置如下所示:

@Configuration
@EnableWebSecurity
@EnableWebMvcSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter 

  @Bean
  public AuthenticationProvider authenticationProvider() 
    return new BasicAuthenticationProvider();
  

  @Autowired
  AuthenticationProvider basicAuthenticationProvider;

  @Bean
  public CustomTokenAuthenticationFilter customTokenAuthenticationFilter()
      System.out.println("+++ create new CustomTokenAuthenticationFilter for path=/**");
      return new CustomTokenAuthenticationFilter("/**");
  ;

  @Autowired
  CustomTokenAuthenticationFilter customTokenAuthenticationFilter;

  @Override
  protected void configure(HttpSecurity http) throws Exception 
      System.out.println("init of http security START");
     http
     .authenticationProvider(authenticationProvider())
     .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
     .csrf().disable()
     .authorizeRequests()
       .anyRequest().authenticated()
     .and()//.authenticationProvider(basicAuthenticationProvider);
     .addFilterBefore(customTokenAuthenticationFilter,  BasicAuthenticationFilter.class)
     .httpBasic();
       //.and().addFilter(filter);
    System.out.println("init of http security DONE");
  

我已经尝试将 URL 映射从 /** 更改为 /activiti-rest/**,但随后,基本身份验证再次启动。

这是我的自定义令牌身份验证过滤器:

public class CustomTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter 

    private static final Logger logger = LoggerFactory.getLogger(CustomTokenAuthenticationFilter.class);
    public CustomTokenAuthenticationFilter(String defaultFilterProcessesUrl) 
        super(defaultFilterProcessesUrl);
        super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl));
        setAuthenticationManager(new NoOpAuthenticationManager());
        setAuthenticationSuccessHandler(new TokenSimpleUrlAuthenticationSuccessHandler());
    


    public final String HEADER_SECURITY_TOKEN = "Cookie";//"LdapToken"; 


    /**
     * Attempt to authenticate request - basically just pass over to another method to authenticate request headers 
     */
    @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException 
        Enumeration<String> headerNames = request.getHeaderNames();
        int i = 0;
        while (headerNames.hasMoreElements())
            String key = (String) headerNames.nextElement();
            String value = request.getHeader(key);
            System.out.println("+++ key["+i+"]" +key);
            System.out.println("+++ val["+i+"]" +value);
            i++;
        
        String token = request.getHeader(HEADER_SECURITY_TOKEN);
        logger.info("token found:"+token);
        System.out.println("+++ token found:"+token);
        AbstractAuthenticationToken userAuthenticationToken = authUserByToken(token);
        if(userAuthenticationToken == null) throw new AuthenticationServiceException(MessageFormat.format("Error | 0", "Bad Token"));
        System.out.println("+++ userAuthenticationToken:"+userAuthenticationToken.toString());
        return userAuthenticationToken;
    


    /**
     * authenticate the user based on token
     * @return
     */
    private AbstractAuthenticationToken authUserByToken(String token) 
        if(token==null) 
            System.out.println("+++ i shouldn't be null +++");
            return null;
        
        AbstractAuthenticationToken authToken = new JWTAuthenticationToken(token);
        try 
            return authToken;
         catch (Exception e) 
            System.out.println(e);
            logger.error("Authenticate user by token error: ", e);
        
        return authToken;
    


    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
            FilterChain chain) throws IOException, ServletException 
        System.out.println("++++++++++++++++++++++++++++++ doFilter ");
        super.doFilter(req, res, chain);
    

还有我的自定义成功处理程序。我认为这会导致无限循环,但我不知道为什么:

public class TokenSimpleUrlAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler 

    @Override
    protected String determineTargetUrl(HttpServletRequest request,
            HttpServletResponse response) 
        System.out.println("+++ yuhuuu determineTargetUrl+++");
        String context = request.getContextPath();
        String fullURL = request.getRequestURI();
        String url = fullURL.substring(fullURL.indexOf(context)+context.length());
        return url;
    

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException 
        System.out.println("+++ yuhuuu onAuthenticationSuccess+++");
        String url = determineTargetUrl(request,response);
        request.getRequestDispatcher(url).forward(request, response);
    

所有其他类(NoOpAuthenticationManager 和 RestAuthenticationEntryPoint)与这篇博文中的完全一样。

如果有人可以给我一个提示,可能会导致这个无限循环,那就太好了。正如我所说,它仅在请求具有有效令牌时发生。

感谢和最好的问候 本

【问题讨论】:

如果删除打印标题的代码会发生什么? 我认为问题在于 URL 匹配。我使用 /** 作为 URL 模式。例如,我调用 REST-URL localhost:8080/activiti-rest/service。我的 CustomFilter 被调用,因为它匹配 /**。 request.getRequestDispatcher(url).forward(request, response);在这种情况下 /service 具有作为 url 参数,它也与 URL 模式匹配。所以我的 CustomAuthenticationFilter 再次被调用。循环发生。但我不知道如何解决这个问题,因为所有 REST-URL 都应该受到保护。 如果您愿意探索另一种方法,您可以查看this app,它取消了自定义过滤器并利用核心 Spring Security 基础设施来保护 REST API 端点。 【参考方案1】:

您的编码方法是有效的。但是,我可以为您提供一种略有不同但可行的方法。在我开始解释解决方案之前,这里是代码:

WebSecurityConfig.java

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

@Override
protected void configure(HttpSecurity http) throws Exception 
    http.authorizeRequests().
    antMatchers("/restapi").hasRole("USER")
    .and().addFilterBefore(new SsoTokenAuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class).httpBasic()
    .and().authorizeRequests().antMatchers("/**").permitAll().anyRequest().authenticated();


@Override
protected void configure(AuthenticationManagerBuilder auth)
        throws Exception 
    // The order is important! During runtime Spring Security tries to find Provider-Implementations that
    // match the UsernamePasswordAuthenticationToken (which will be created later..). We must make sure
    // that daoAuthenticationProvider matches first. Why? Hard to explain, I figured it out with the debugger.
    auth.authenticationProvider(daoAuthenticationProvider());
    auth.authenticationProvider(tokenAuthenticationProvider());



@Bean
public AuthenticationProvider tokenAuthenticationProvider() 
    return new SsoTokenAuthenticationProvider();


@Bean
public AuthenticationProvider daoAuthenticationProvider() 
    // DaoAuthenticationProvider requires a userDetailsService object to be attached.
    // So we build one. This replaces the AuthenticationConfiguration, which is commented out below

    // Build the userDetailsService
    User userThatMustMatch = new User("michael", "password", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_RESTUSER"));
    Collection<UserDetails> users = new ArrayList<>();
    users.add(userThatMustMatch);
    InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager(users);  

    // Create the DaoAuthenticationProvider that will handle all HTTP BASIC AUTH requests
    DaoAuthenticationProvider daoAuthProvider = new DaoAuthenticationProvider();
    daoAuthProvider.setUserDetailsService(userDetailsService);
    return daoAuthProvider;

SsoTokenAuthenticationFilter.java

public class SsoTokenAuthenticationFilter extends GenericFilterBean 

public final String HEADER_SECURITY_COOKIE = "LdapToken"; 

private AuthenticationManager authenticationManager;
private AuthenticationDetailsSource<HttpServletRequest,?> ssoTokenAuthenticationDetailsSource = new SsoTokenWebAuthenticationDetailsSource();

public SsoTokenAuthenticationFilter(AuthenticationManager authenticationManager) 
    this.authenticationManager = authenticationManager;


@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;

    // check if SSO token is available. If not, pass down to next filter in chain
    try 
        Cookie[] cookies = httpRequest.getCookies();
        if (cookies == null)
            chain.doFilter(request, response);
            return;
        
        Cookie ssoCookie = null;
        for (int i = 0; i < cookies.length; i++) 
            if (cookies[i].getName().equals("ssoToken"))
                ssoCookie = cookies[i];
            
        if (ssoCookie == null)
            chain.doFilter(request, response);
            return;
        

        // SSO token found, now authenticate and afterwards pass down to next filter in chain
        authenticateWithSsoToken(httpRequest);
        logger.debug("now the AuthenticationFilter passes down to next filter in chain");
        chain.doFilter(request, response);
     catch (InternalAuthenticationServiceException internalAuthenticationServiceException) 
        SecurityContextHolder.clearContext();
        logger.error("Internal authentication service exception", internalAuthenticationServiceException);
        httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
     catch (AuthenticationException authenticationException) 
        SecurityContextHolder.clearContext();
        logger.debug("No or invalid SSO token");
        httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
     


private void authenticateWithSsoToken(HttpServletRequest request) throws IOException 
    System.out.println("+++ authenticateWithSSOToken +++");
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(null, null, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_RESTUSER"));
    authRequest.setDetails(ssoTokenAuthenticationDetailsSource.buildDetails(request));        

    // Delegate authentication to SsoTokenAuthenticationProvider, he will call the SsoTokenAuthenticationProvider <-- because of the configuration in WebSecurityConfig.java
    Authentication authResult = authenticationManager.authenticate(authRequest);

SsoTokenAuthenticationProvider.java

public class SsoTokenAuthenticationProvider implements AuthenticationProvider 

public SsoTokenAuthenticationProvider() 



@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException 
    SsoTokenWebAuthenticationDetails ssoTokenWebAuthenticationDetails = null;
    WebAuthenticationDetails webWebAuthenticationDetails = (WebAuthenticationDetails)authentication.getDetails();

    if (! (webWebAuthenticationDetails instanceof SsoTokenWebAuthenticationDetails))
        // ++++++++++++++++++++++++
        // BASIC authentication....
        // ++++++++++++++++++++++++
        UsernamePasswordAuthenticationToken emptyToken = new UsernamePasswordAuthenticationToken(null, null);
        emptyToken.setDetails(null);
        return emptyToken; //return null works, too.
    

    // ++++++++++++++++++++++++
    // LDAP authentication....
    // ++++++++++++++++++++++++
    ssoTokenWebAuthenticationDetails = (SsoTokenWebAuthenticationDetails)webWebAuthenticationDetails;       
    Cookie ssoTokenCookie = ssoTokenWebAuthenticationDetails.getSsoTokenCookie();

    // check if SSO cookie is available
    if (ssoTokenCookie == null) 
        return new UsernamePasswordAuthenticationToken(null, null); //do basic auth.
    
    String username = ssoTokenCookie.getValue();

    // Do your SSO token authentication here
    if (! username.equals("michael"))
        return new UsernamePasswordAuthenticationToken(null, null); //do basic auth.

    // Create new Authentication object. Name and password can be null (but you can set the values of course).
    // Be careful with your role names!
    // In WebSecurityConfig the role "USER" is automatically prefixed with String "ROLE_", so it is "ROLE_USER" in the end.
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(null, null, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_RESTUSER"));
    authRequest.setDetails(ssoTokenWebAuthenticationDetails);

    // Don't let spring decide.. you already have made the right decisions. Tell spring you have an authenticated user.
    // vielleicht ist dieses obere Kommentar auch bullshit... ich lese das morgen noch mal nach...
    SecurityContextHolder.getContext().setAuthentication(authentication);
    return authentication;


@Override
public boolean supports(Class<?> authentication) 
    return authentication.equals(UsernamePasswordAuthenticationToken.class);


SsoTokenWebAuthenticationDetailsS​​ource.java

public class SsoTokenWebAuthenticationDetailsSource extends
    WebAuthenticationDetailsSource 

@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) 
    return new SsoTokenWebAuthenticationDetails(context);



SsoTokenWebAuthenticationDetails.java

public class SsoTokenWebAuthenticationDetails extends WebAuthenticationDetails 
private static final long serialVersionUID = 1234567890L;

private Cookie ssoTokenCookie;

public SsoTokenWebAuthenticationDetails(HttpServletRequest request) 
    super(request);
    // Fetch cookie from request
    Cookie[] cookies = request.getCookies();

    Cookie ssoTokenCookie = null;
    for (int i = 0; i < cookies.length; i++) 
        if (cookies[i].getName().equals("SSOToken"))
            ssoTokenCookie= cookies[i];
        
    this.setSsoTokenCookie(ssoTokenCookie);


public Cookie getSsoTokenCookie() 
    return ssoTokenCookie;


public void setSsoTokenCookie(Cookie ssoTokenCookie) 
    this.ssoTokenCookie = ssoTokenCookie;


我用一句话描述解决方案:

    Config 类保护任何具有ROLE_USER 角色的/restapi 控制器。可以使用 httpBasic 身份验证来完成身份验证,但在此之前您可以尝试基本身份验证。您必须尝试通过 ssoTokenCookie(如果可用)对用户进行身份验证。因此,您在基本身份验证之前将SsoTokenAuthenticationFilter 设置为过滤器。已应用。 在过滤器中,您检查请求中是否有可用的 ssoTokenCookie。 如果是,则将身份验证委托给标准 spring AuthenticationManagerAuthenticationManager 知道您自己的 SsoTokenAuthenticationProvider 实现并将身份验证委托给它。在这里,提供 cookie 信息很重要。这可以通过使用自定义的WebAuthenticationDetails 来完成。 如果不是,则将工作传递给链中的下一个过滤器。毫不奇怪,标准的BasicAuthenticationFilter 将被调用。因为您告诉 Spring 在WebSecurityConfig.java 中使用标准daoAuthenticationProvider,所以当在基本身份验证中输入正确的凭据时,Spring 可以对用户进行身份验证。对话框

【讨论】:

非常感谢。工作得很好。此外,我用以下部分修复了我的代码: static final String FILTER_APPLIED = "__spring_security_scpf_applied"; if (req.getAttribute(FILTER_APPLIED) != null) chain.doFilter(req, res);返回; req.setAttribute(FILTER_APPLIED, Boolean.TRUE);这样,我的递归就消失了。

以上是关于自定义 Spring Security 应用程序中的无限循环的主要内容,如果未能解决你的问题,请参考以下文章

spring boot spring security 基于自定义令牌的身份验证和自定义授权

如何在自定义 Spring Security 登录成功处理程序中注入会话 bean

如何在 Spring Security 中使用自定义角色/权限?

如何使用自定义 JAAS 堆栈配置 Spring Security?

如何在spring-security的SecurityContext中存储自定义信息?

如何使用 Spring Security 自定义登录页面?