用于预控制器过滤器的ExceptionHandler(Spring Security)

时间:2017-04-26 11:13:09

标签: java spring-mvc spring-security exception-handling

我对Spring安全级别的异常有疑问。

我使用SM_USER标头进行授权,并且验证请求通过DelegateRequestMatchingFilter(它有助于了解是否需要SM_USER)。

问题在于,如果没有SM_USER标头,那么super.doFilter(...)行会抛出一个PreAuthenticatedCredentialsNotFoundException无法使用控制器的标准ExceptionResolver进行处理,这就是为什么看起来很奇怪而且不同于所有应用程序抛出的其他异常。

我尝试为过滤器编写一个标有@ExceptionResolver注释的方法,但会被忽略。

如何为此过滤器插入ExceptionsResolver?

DelegateRequestMatchingFilter代码段

public class DelegateRequestMatchingFilter extends RequestHeaderAuthenticationFilter {

private RequestMatcher ignoredRequests;
public DelegateRequestMatchingFilter(RequestMatcher matcher) {
    super();
    super.setPrincipalRequestHeader("SM_USER");
    this.ignoredRequests = matcher;
}   

@Override
public void doFilter(ServletRequest req, ServletResponse resp,FilterChain chain) throws IOException, ServletException {

     HttpServletRequest httpReq = (HttpServletRequest) req;      
     if(ignoredRequests.matches(httpReq)) {
         chain.doFilter(req,resp);
     } else {
         super.doFilter(req,resp,chain);//<-- throws exception
     }       
}   
}

来自 ExceptionResolver 的片段无法解决问题,因为它仅适用于控制器

 @ControllerAdvice
public class ExceptionResolver extends AbstractHandlerExceptionResolver{


@Override
protected ModelAndView doResolveException(HttpServletRequest request,
        HttpServletResponse responce, Object handler, Exception exception) {

    ModelAndView toReturn = new ModelAndView();
    toReturn.setView(new MappingJackson2JsonView());
    toReturn.addObject("message", exception.getMessage());
    toReturn.addObject("exceptionClass", exception.getClass().getCanonicalName());
    return toReturn;
}

@ExceptionHandler(PreAuthenticatedCredentialsNotFoundException.class)       
public ResponseEntity<Result> handleBindException(PreAuthenticatedCredentialsNotFoundException e) {

    Result result = new Result("the identification header is missing");
    result.setException(PreAuthenticatedCredentialsNotFoundException.class);
    ResponseEntity<Result> response = new ResponseEntity<Result>(result, HttpStatus.FORBIDDEN);
    return response;
}
}

SecurityConfiguration

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{

private UserDetailsService userDetailsService;  
private PreAuthenticatedAuthenticationProvider preAuthenticatedProvider;



@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

    Map<String, List<GrantedAuthority>> rolesAuthorities = getRolesWithAutorities();

    userDetailsService = new CustomUserDetailsService(...);
    UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> wrapper = 
            new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(userDetailsService);
    preAuthenticatedProvider = new PreAuthenticatedAuthenticationProvider();
    preAuthenticatedProvider.setPreAuthenticatedUserDetailsService(wrapper);
    auth.authenticationProvider(preAuthenticatedProvider);
}   

@Bean
public SmUserFailureHandler smUserFailureHandler(){
    return new SmUserFailureHandler();
}

@Bean
public AccessDeniedHandler accessDeniedHandler(){
    AccessDeniedHandlerImpl handler = new AccessDeniedHandlerImpl();
    handler.setErrorPage("/errorPage/");        
    return handler;     
}

public RequestHeaderAuthenticationFilter siteMinderFilter() throws Exception
{

    RequestHeaderAuthenticationFilter siteMinderFilter = new DelegateRequestMatchingFilter(
            new OrRequestMatcher(
            new AntPathRequestMatcher("/uriToSkip/")));

    siteMinderFilter.setPrincipalRequestHeader("SM_USER");
    siteMinderFilter.setAuthenticationManager(authenticationManager());   
    return siteMinderFilter;
}

@Override
protected void configure(HttpSecurity http) throws Exception {  

 //...   
 http.addFilter(siteMinderFilter());
 ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
  //adding methods... 
  http = registry.and();

  http.csrf().disable();
  http.headers().cacheControl().disable();
  http
  .sessionManagement()
      .sessionCreationPolicy(SessionCreationPolicy.NEVER);    
}
}

如果我根据答案更改了我的代码,即使存在SM_USEr标头并且正确,我也会收到此异常

org.springframework.security.authentication.InsufficientAuthenticationException: Full authentication is required to access this resource
at org.springframework.security.web.access.ExceptionTranslationFilter.handleSpringSecurityException(ExceptionTranslationFilter.java:177)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:133)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:122)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:168)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:48)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:205)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:120)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:585)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:221)
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
at org.eclipse.jetty.server.Server.handle(Server.java:497)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:310)
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:257)
at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:540)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635)
at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555)
at java.lang.Thread.run(Thread.java:745)

1 个答案:

答案 0 :(得分:2)

首先,您应该在ExceptionTranslationFilter之后添加过滤器,此过滤器会在捕获它们时处理AccessDeniedExceptionAuthenticationException

http.addFilterAfter(siteMinderFilter(), ExceptionTranslationFilter.class);

接下来你需要注入一个AuthenticationEntryPoint来处理AuthenticationException,有一些已经存在AuthenticationEntryPoint你可以使用它,比如HttpStatusEntryPointLoginUrlAuthenticationEntryPoint, ...;

如果要返回JSON响应,则需要实现自己的AuthenticationEntryPoint,例如:

public class ExampleAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private static final Logger logger = LoggerFactory.getLogger(ExampleAuthenticationEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        logger.error(authException.getMessage(), authException);
        if (!response.isCommitted()) {
            Map<String, Object> responseMsg = new HashMap<String, Object>();
            responseMsg.put("message", authException.getMessage());
            ObjectMapper mapper = new ObjectMapper();
            try {
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                PrintWriter writer = response.getWriter();
                writer.write(mapper.writeValueAsString(responseMsg));
                writer.flush();
                writer.close();
            } catch (IOException e) {
                // ignore
                logger.error(e.getMessage(), e);
            }
        }
    }
}

我认为最好返回http雕像代码401而不是403403用于AccessDeniedException;

然后将您自己的AuthenticationEntryPoint注入ExceptionTranslationFilter;

http.exceptionHandling().authenticationEntryPoint(myEntryPoint());

@Bean
AuthenticationEntryPoint myEntryPoint() {
    return new ExampleAuthenticationEntryPoint();
}

/*
Or just return 401
AuthenticationEntryPoint myEntryPoint() {
    return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED);
} 
*/

顺便说一句:

如果您在continueFilterChainOnUnsuccessfulAuthentication中将true设置为RequestHeaderAuthenticationFilter,则异常不会引入超级过滤器,其默认值为true,您可能需要设置它为false,或者您需要在AuthenticationFailureHandler中注入DelegateRequestMatchingFilter以在进行身份验证时处理异常。

**更新**

else if (exception instanceof AccessDeniedException) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
                logger.debug(
                        "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
                        exception);

                sendStartAuthentication(
                        request,
                        response,
                        chain,
                        new InsufficientAuthenticationException(
                                "Full authentication is required to access this resource"));
            }
            else {
                logger.debug(
                        "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
                        exception);

                accessDeniedHandler.handle(request, response,
                        (AccessDeniedException) exception);
            }
        }

正如您可以从代码中读取的那样,现在它是AccessDeniedException,我认为您可以禁用匿名用户,或者您需要检查获取AccessDeniedException的原因。

http.anonymous().disable();

更新2

Authentication currentUser = SecurityContextHolder.getContext()
            .getAuthentication();

    if (currentUser == null) {
        return true;
    }

    if (!checkForPrincipalChanges) {
        return false;
    }

    if(!principalChanged(request, currentUser)) {
        return false;
    }

checkForPrincipalChanges的默认值为false,因此现在如果您启用anon过滤器,则会返回false并跳过RequestHeaderAuthenticationFilter中的身份验证,所以请尝试执行禁用anon过滤器。你需要学习弹簧安全代码并通过调试来研究它。