使用@ExceptionHandler处理Spring Security身份验证异常


97

我正在使用Spring MVC @ControllerAdvice@ExceptionHandler处理REST Api的所有异常。对于Web mvc控制器抛出的异常,它工作正常,但对于Spring Security自定义过滤器抛出的异常,它不工作,因为它们在调用控制器方法之前运行。

我有一个自定义的spring安全过滤器,它执行基于令牌的身份验证:

public class AegisAuthenticationFilter extends GenericFilterBean {

...

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        try {

            ...         
        } catch(AuthenticationException authenticationException) {

            SecurityContextHolder.clearContext();
            authenticationEntryPoint.commence(request, response, authenticationException);

        }

    }

}

使用此自定义入口点:

@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{

    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
    }

}

并使用此类来全局处理异常:

@ControllerAdvice
public class RestEntityResponseExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ InvalidTokenException.class, AuthenticationException.class })
    @ResponseStatus(value = HttpStatus.UNAUTHORIZED)
    @ResponseBody
    public RestError handleAuthenticationException(Exception ex) {

        int errorCode = AegisErrorCode.GenericAuthenticationError;
        if(ex instanceof AegisException) {
            errorCode = ((AegisException)ex).getCode();
        }

        RestError re = new RestError(
            HttpStatus.UNAUTHORIZED,
            errorCode, 
            "...",
            ex.getMessage());

        return re;
    }
}

我需要做的就是返回一个详细的JSON正文,即使对于spring security AuthenticationException也是如此。有没有办法让Spring Security AuthenticationEntryPoint和Spring MVC @ExceptionHandler一起工作?

我正在使用Spring Security 3.1.4和Spring MVC 3.2.4。


9
您不能... (@)ExceptionHandler仅当请求由处理时,才能工作DispatcherServlet。但是,此异常在此之前发生,因为它由抛出Filter。因此,您将永远无法使用来处理此异常(@)ExceptionHandler
M. Deinum 2013年

好吧,你是对的。有没有一种方法可以返回JSON正文以及EntryPoint的response.sendError?
Nicola

听起来您需要在链的前面插入自定义过滤器以捕获Exception并相应地返回。该文件列出了过滤器,它们的别名和它们的应用顺序:docs.spring.io/spring-security/site/docs/3.1.4.RELEASE/...
Romski

1
如果只有您需要JSON的位置,则只需在中构造/写入JSON EntryPoint。您可能想要在此处构造对象,然后MappingJackson2HttpMessageConverter在其中注入a 。
Deinum M. 13年

@ M.Deinum我将尝试在入口点内部构建json。
Nicola

Answers:


58

好的,我尝试按照建议自己从AuthenticationEntryPoint编写json,它可以工作。

仅出于测试目的,我通过删除response.sendError更改了AutenticationEntryPoint

@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{

    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {

        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getOutputStream().println("{ \"error\": \"" + authenticationException.getMessage() + "\" }");

    }
}

这样,即使您使用的是Spring Security AuthenticationEntryPoint,也可以将自定义json数据与未经授权的401一起发送。

显然,您不会像我出于测试目的那样构建json,但会序列化一些类实例。


3
使用Jackson的示例:ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(),新的FailResponse(401,authException.getLocalizedMessage(),“访问被拒绝”,“”)));
Cyrusmith

1
我知道这个问题有点老了,但是您是否将AuthenticationEntryPoint注册到SecurityConfig?
leventunver '16

1
@leventunver在这里,您可以找到如何注册入口点:stackoverflow.com/questions/24684806/…
尼古拉

37

Spring SecuritySpring Web框架处理响应的方式不一致,这是一个非常有趣的问题。我相信它必须以MessageConverter一种便捷的方式原生支持错误消息处理。

我试图找到一种注入MessageConverterSpring Security 的优雅方法,以便它们可以捕获异常并根据内容协商以正确的格式返回它们。尽管如此,我下面的解决方案并不优雅,但至少要利用Spring代码。

我假设您知道如何包括Jackson和JAXB库,否则没有任何继续的意义。共有3个步骤。

第1步-创建独立类,存储MessageConverters

这堂课没有魔术。它仅存储消息转换器和处理器RequestResponseBodyMethodProcessor。魔术在处理器内部,它将完成所有工作,包括内容协商和相应地转换响应主体。

public class MessageProcessor { // Any name you like
    // List of HttpMessageConverter
    private List<HttpMessageConverter<?>> messageConverters;
    // under org.springframework.web.servlet.mvc.method.annotation
    private RequestResponseBodyMethodProcessor processor;

    /**
     * Below class name are copied from the framework.
     * (And yes, they are hard-coded, too)
     */
    private static final boolean jaxb2Present =
        ClassUtils.isPresent("javax.xml.bind.Binder", MessageProcessor.class.getClassLoader());

    private static final boolean jackson2Present =
        ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", MessageProcessor.class.getClassLoader()) &&
        ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", MessageProcessor.class.getClassLoader());

    private static final boolean gsonPresent =
        ClassUtils.isPresent("com.google.gson.Gson", MessageProcessor.class.getClassLoader());

    public MessageProcessor() {
        this.messageConverters = new ArrayList<HttpMessageConverter<?>>();

        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        this.messageConverters.add(new ResourceHttpMessageConverter());
        this.messageConverters.add(new SourceHttpMessageConverter<Source>());
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

        if (jaxb2Present) {
            this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
        }
        if (jackson2Present) {
            this.messageConverters.add(new MappingJackson2HttpMessageConverter());
        }
        else if (gsonPresent) {
            this.messageConverters.add(new GsonHttpMessageConverter());
        }

        processor = new RequestResponseBodyMethodProcessor(this.messageConverters);
    }

    /**
     * This method will convert the response body to the desire format.
     */
    public void handle(Object returnValue, HttpServletRequest request,
        HttpServletResponse response) throws Exception {
        ServletWebRequest nativeRequest = new ServletWebRequest(request, response);
        processor.handleReturnValue(returnValue, null, new ModelAndViewContainer(), nativeRequest);
    }

    /**
     * @return list of message converters
     */
    public List<HttpMessageConverter<?>> getMessageConverters() {
        return messageConverters;
    }
}

第2步-创建AuthenticationEntryPoint

在许多教程中,此类对于实现自定义错误处理至关重要。

public class CustomEntryPoint implements AuthenticationEntryPoint {
    // The class from Step 1
    private MessageProcessor processor;

    public CustomEntryPoint() {
        // It is up to you to decide when to instantiate
        processor = new MessageProcessor();
    }

    @Override
    public void commence(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException authException)
        throws IOException, ServletException {

        // This object is just like the model class, 
        // the processor will convert it to appropriate format in response body
        CustomExceptionObject returnValue = new CustomExceptionObject();
        try {
            processor.handle(returnValue, request, response);
        } catch (Exception e) {
            throw new ServletException();
        }
    }
}

步骤3-注册入口点

如前所述,我使用Java Config来实现。我只是在这里显示相关的配置,应该有其他配置,例如会话无状态等。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint());
    }
}

尝试一些身份验证失败的情况,请记住请求标头应包含Accept:XXX,并且应该以JSON,XML或其他格式获取此异常。


1
我正在尝试捕获一个,InvalidGrantException但是您的我的版本CustomEntryPoint没有被调用。知道我可能会缺少什么吗?
显示名

@显示名称。无法通过捕获所有异常的身份验证AuthenticationEntryPointAccessDeniedHandlerUsernameNotFoundExceptionInvalidGrantException可处理AuthenticationFailureHandler这里解释
威尔逊

23

我发现最好的方法是将异常委托给HandlerExceptionResolver

@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Autowired
    private HandlerExceptionResolver resolver;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        resolver.resolveException(request, response, null, exception);
    }
}

那么您可以使用@ExceptionHandler以所需的方式格式化响应。


9
奇迹般有效。如果Spring抛出错误,提示有2个bean定义用于自动装配,则必须添加限定符批注:@Autowired @Qualifier(“ handlerExceptionResolver”)private HandlerExceptionResolver resolver;
戴维德

1
请注意,@ControllerAdvice如果在批注中指定了basePackages ,则通过传递空处理程序将无法使用。我必须完全删除它以允许调用处理程序。
Jarmex '19

你为什么给@Component("restAuthenticationEntryPoint")?为什么需要像restAuthenticationEntryPoint这样的名称?是否可以避免某些Spring名称冲突?
程序员

@Jarmex因此,代替null的是什么?是某种处理程序吧?我应该只传递一个已用@ControllerAdvice注释的类吗?谢谢
程序员

@theprogrammer,我不得不稍微重新组织应用程序以删除basePackages批注参数来解决它-不理想!
Jarmex

5

在Spring Boot和的情况下,@EnableResourceServer扩展ResourceServerConfigurerAdapter而不是WebSecurityConfigurerAdapter在Java配置中进行扩展和AuthenticationEntryPoint通过覆盖configure(ResourceServerSecurityConfigurer resources)resources.authenticationEntryPoint(customAuthEntryPoint())在方法内部使用来注册自定义相对容易和方便。

像这样:

@Configuration
@EnableResourceServer
public class CommonSecurityConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.authenticationEntryPoint(customAuthEntryPoint());
    }

    @Bean
    public AuthenticationEntryPoint customAuthEntryPoint(){
        return new AuthFailureHandler();
    }
}

还有一个OAuth2AuthenticationEntryPoint可以扩展的优点(因为它不是最终的),并且在实现custom时可以部分重用AuthenticationEntryPoint。特别是,它添加带有错误相关详细信息的“ WWW-Authenticate”标头。

希望这会帮助某人。


我正在尝试这样做,但是commence()我的功能AuthenticationEntryPoint没有被调用-我缺少什么吗?
显示名

4

从@Nicola和@Victor Wing那里获取答案,并添加更标准化的方法:

import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class UnauthorizedErrorAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {

    private HttpMessageConverter messageConverter;

    @SuppressWarnings("unchecked")
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        MyGenericError error = new MyGenericError();
        error.setDescription(exception.getMessage());

        ServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
        outputMessage.setStatusCode(HttpStatus.UNAUTHORIZED);

        messageConverter.write(error, null, outputMessage);
    }

    public void setMessageConverter(HttpMessageConverter messageConverter) {
        this.messageConverter = messageConverter;
    }

    @Override
    public void afterPropertiesSet() throws Exception {

        if (messageConverter == null) {
            throw new IllegalArgumentException("Property 'messageConverter' is required");
        }
    }

}

现在,您可以注入已配置的Jackson,Jaxb或用于转换MVC批注或基于XML的配置(带有其序列化器,反序列化器等)的响应主体的任何对象。


我对Spring Boot非常陌生:请告诉我“如何将messageConverter对象传递给authenticationEntry点”
Kona Suresh

通过二传手。使用XML时,必须创建一个<property name="messageConverter" ref="myConverterBeanName"/>标签。使用@Configuration类时,只需使用setMessageConverter()方法。
加布里埃尔·维拉西斯

4

HandlerExceptionResolver在这种情况下,我们需要使用。

@Component
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Autowired
    //@Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        resolver.resolveException(request, response, null, authException);
    }
}

另外,您需要添加异常处理程序类以返回您的对象。

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(AuthenticationException.class)
    public GenericResponseBean handleAuthenticationException(AuthenticationException ex, HttpServletResponse response){
        GenericResponseBean genericResponseBean = GenericResponseBean.build(MessageKeys.UNAUTHORIZED);
        genericResponseBean.setError(true);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        return genericResponseBean;
    }
}

可能你在运行,因为多个实现一个项目的时候得到一个错误HandlerExceptionResolver,在这种情况下,你必须添加@Qualifier("handlerExceptionResolver")HandlerExceptionResolver


GenericResponseBean只是java pojo,您可以创建自己的
Vinit Solanki

2

我可以通过简单地覆盖过滤器中的方法“ unsuccessfulAuthentication”来解决这一问题。在那里,我使用所需的HTTP状态代码向客户端发送错误响应。

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException failed) throws IOException, ServletException {

    if (failed.getCause() instanceof RecordNotFoundException) {
        response.sendError((HttpServletResponse.SC_NOT_FOUND), failed.getMessage());
    }
}

1

更新:如果您喜欢并且更喜欢直接看代码,那么我为您提供了两个示例,一个示例使用的是您正在寻找的标准Spring Security,另一个示例使用的是Reactive Web和Reactive Security:
- 正常Web + Jwt安全性
- 响应式Jwt

我一直用于基于JSON的端点的外观如下所示:

@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {

    @Autowired
    ObjectMapper mapper;

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

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException e)
            throws IOException, ServletException {
        // Called when the user tries to access an endpoint which requires to be authenticated
        // we just return unauthorizaed
        logger.error("Unauthorized error. Message - {}", e.getMessage());

        ServletServerHttpResponse res = new ServletServerHttpResponse(response);
        res.setStatusCode(HttpStatus.UNAUTHORIZED);
        res.getServletResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        res.getBody().write(mapper.writeValueAsString(new ErrorResponse("You must authenticated")).getBytes());
    }
}

添加Spring Web Starter后,对象映射器将变成bean,但是我更喜欢对其进行自定义,因此这是我对ObjectMapper的实现:

  @Bean
    public Jackson2ObjectMapperBuilder objectMapperBuilder() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.modules(new JavaTimeModule());

        // for example: Use created_at instead of createdAt
        builder.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);

        // skip null fields
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return builder;
    }

您在WebSecurityConfigurerAdapter类中设置的默认AuthenticationEntryPoint:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ............
   @Autowired
    private JwtAuthEntryPoint unauthorizedHandler;
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                // .antMatchers("/api/auth**", "/api/login**", "**").permitAll()
                .anyRequest().permitAll()
                .and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);


        http.headers().frameOptions().disable(); // otherwise H2 console is not available
        // There are many ways to ways of placing our Filter in a position in the chain
        // You can troubleshoot any error enabling debug(see below), it will print the chain of Filters
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }
// ..........
}

1

自定义过滤器,并确定哪种异常,应该有比这更好的方法

public class ExceptionFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
    String msg = "";
    try {
        filterChain.doFilter(request, response);
    } catch (Exception e) {
        if (e instanceof JwtException) {
            msg = e.getMessage();
        }
        response.setCharacterEncoding("UTF-8");
        response.setContentType(MediaType.APPLICATION_JSON.getType());
        response.getWriter().write(JSON.toJSONString(Resp.error(msg)));
        return;
    }
}

}


0

我正在使用objectMapper。每个Rest Service通常都使用json,并且在您的一个配置中,您已经配置了对象映射器。

代码是用Kotlin编写的,希望可以。

@Bean
fun objectMapper(): ObjectMapper {
    val objectMapper = ObjectMapper()
    objectMapper.registerModule(JodaModule())
    objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)

    return objectMapper
}

class UnauthorizedAuthenticationEntryPoint : BasicAuthenticationEntryPoint() {

    @Autowired
    lateinit var objectMapper: ObjectMapper

    @Throws(IOException::class, ServletException::class)
    override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {
        response.addHeader("Content-Type", "application/json")
        response.status = HttpServletResponse.SC_UNAUTHORIZED

        val responseError = ResponseError(
            message = "${authException.message}",
        )

        objectMapper.writeValue(response.writer, responseError)
     }}

0

ResourceServerConfigurerAdapter课堂上,下面的代码片段对我有用。http.exceptionHandling().authenticationEntryPoint(new AuthFailureHandler()).and.csrf()..不工作。这就是为什么我将其编写为单独的电话。

public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

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

        http.exceptionHandling().authenticationEntryPoint(new AuthFailureHandler());

        http.csrf().disable()
                .anonymous().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .antMatchers("/subscribers/**").authenticated()
                .antMatchers("/requests/**").authenticated();
    }

AuthenticationEntryPoint的实现,用于捕获令牌到期和缺少授权头。


public class AuthFailureHandler implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)
      throws IOException, ServletException {
    httpServletResponse.setContentType("application/json");
    httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

    if( e instanceof InsufficientAuthenticationException) {

      if( e.getCause() instanceof InvalidTokenException ){
        httpServletResponse.getOutputStream().println(
            "{ "
                + "\"message\": \"Token has expired\","
                + "\"type\": \"Unauthorized\","
                + "\"status\": 401"
                + "}");
      }
    }
    if( e instanceof AuthenticationCredentialsNotFoundException) {

      httpServletResponse.getOutputStream().println(
          "{ "
              + "\"message\": \"Missing Authorization Header\","
              + "\"type\": \"Unauthorized\","
              + "\"status\": 401"
              + "}");
    }

  }
}

无法正常工作..仍显示默认消息
aswzen
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.