Infinite loop in custom Spring Security applicatio

2019-05-23 03:15发布

问题:

We try to substitute an existing Spring Security Basic Login for a REST-API in an Open Source Application to achieve a custom login with a token. I read this blogpost about the topic: http://javattitude.com/2014/06/07/spring-security-custom-token-based-rest-authentication/

When the request has no header named "Cookie", I get correcty a 401 - unauthorized response (expected behaviour). When the request has a valid token, I get an infinite loop causing a java.lang.StackOverflowError:

    Exception in thread "http-bio-8080-exec-45" java.lang.StackOverflowError
        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)

My Spring Security Configuration looks like this:

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

I already tried to change the URL-Mapping from /** to /activiti-rest/** but then, the Basic Authentication kicks in again.

This is my custom Token Authentication filter:

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

And my Custom Success handler. I think that this causes the infinite loop, but I cannot figure out, why:

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

All other classes (NoOpAuthenticationManager and RestAuthenticationEntryPoint) are exactly like in this blogpost.

Would be great I someone could give me a hint what could cause this infinite loop. As I said, it only occurs when the Request has a valid token.

Thanks and best regards Ben

回答1:

your coding approach is valid. However, I can provide you with a slightly different but working approach. Before I start to explain the solution, here is the code:

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

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

I describe the solution in a view words:

  1. The Config class secures any /restapi controller with role ROLE_USER. The authentication can be done using httpBasic authentication, but before you can try basic auth. you must try to authenticate the user by a ssoTokenCookie (if available). Therefore, you set the SsoTokenAuthenticationFilter as filter before basic auth. is applied.
  2. Inside the filter, you check if a ssoTokenCookie is available in request.
    • If yes, you delegate the authentication to the standard spring AuthenticationManager. The AuthenticationManager knows your own SsoTokenAuthenticationProvider implementation and delegates the authentication to it. Here, it is important to have the cookie information available. This can be done by use of a customized WebAuthenticationDetails.
    • if no, you pass down the work to the next filter in chain. It's no surprise, the standard BasicAuthenticationFilter will be called. Because you told Spring to use the standard daoAuthenticationProvider in WebSecurityConfig.java, Spring can authenticate the user when the proper credentials will be entered in the basic auth. dialog