使用@ExceptionHandler 处理 spring 安全认证异常

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19767267/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 06:20:48  来源:igfitidea点击:

Handle spring security authentication exceptions with @ExceptionHandler

springspring-mvcspring-security

提问by Nicola

I'm using Spring MVC's @ControllerAdviceand @ExceptionHandlerto handle all the exception of a REST Api. It works fine for exceptions thrown by web mvc controllers but it does not work for exceptions thrown by spring security custom filters because they run before the controller methods are invoked.

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

I have a custom spring security filter that does a token based auth:

我有一个自定义 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);

        }

    }

}

With this custom entry point:

使用此自定义入口点:

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

}

And with this class to handle exceptions globally:

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

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

What I need to do is to return a detailed JSON body even for spring security AuthenticationException. Is there a way make spring security AuthenticationEntryPoint and spring mvc @ExceptionHandler work together?

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

I'm using spring security 3.1.4 and spring mvc 3.2.4.

我正在使用 spring 安全 3.1.4 和 spring mvc 3.2.4。

采纳答案by Nicola

Ok, I tried as suggested writing the json myself from the AuthenticationEntryPoint and it works.

好的,我按照建议尝试从 AuthenticationEntryPoint 自己编写 json 并且它有效。

Just for testing I changed the AutenticationEntryPoint by removing response.sendError

只是为了测试,我通过删除 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() + "\" }");

    }
}

In this way you can send custom json data along with the 401 unauthorized even if you are using Spring Security AuthenticationEntryPoint.

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

Obviously you would not build the json as I did for testing purposes but you would serialize some class instance.

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

回答by Victor Wong

This is a very interesting problem that Spring Securityand Spring Webframework is not quite consistent in the way they handle the response. I believe it has to natively support error message handling with MessageConverterin a handy way.

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

I tried to find an elegant way to inject MessageConverterinto Spring Security so that they could catch the exception and return them in a right format according to content negotiation. Still, my solution below is not elegant but at least make use of Spring code.

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

I assume you know how to include Hymanson and JAXB library, otherwise there is no point to proceed. There are 3 Steps in total.

我假设您知道如何包含 Hymanson 和 JAXB 库,否则没有必要继续。总共有3个步骤。

Step 1 - Create a standalone class, storing MessageConverters

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

This class plays no magic. It simply stores the message converters and a processor RequestResponseBodyMethodProcessor. The magic is inside that processor which will do all the job including content negotiation and converting the response body accordingly.

这门课没有魔法。它只是存储消息转换器和处理器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 Hymanson2Present =
        ClassUtils.isPresent("com.fasterxml.Hymanson.databind.ObjectMapper", MessageProcessor.class.getClassLoader()) &&
        ClassUtils.isPresent("com.fasterxml.Hymanson.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 (Hymanson2Present) {
            this.messageConverters.add(new MappingHymanson2HttpMessageConverter());
        }
        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;
    }
}

Step 2 - Create AuthenticationEntryPoint

第 2 步 - 创建 AuthenticationEntryPoint

As in many tutorials, this class is essential to implement custom error handling.

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

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

Step 3 - Register the entry point

第 3 步 - 注册入口点

As mentioned, I do it with Java Config. I just show the relevant configuration here, there should be other configuration such as session stateless, etc.

如前所述,我使用 Java Config 来完成。我这里只展示相关的配置,应该还有其他的配置,比如 session stateless等。

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

Try with some authentication fail cases, remember the request header should include Accept : XXXand you should get the exception in JSON, XML or some other formats.

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

回答by Christophe Bornet

The best way I've found is to delegate the exception to the HandlerExceptionResolver

我发现的最好方法是将异常委托给 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);
    }
}

then you can use @ExceptionHandler to format the response the way you want.

然后您可以使用@ExceptionHandler 按照您想要的方式格式化响应。

回答by Vladimir Salin

In case of Spring Boot and @EnableResourceServer, it is relatively easy and convenient to extend ResourceServerConfigurerAdapterinstead of WebSecurityConfigurerAdapterin the Java configuration and register a custom AuthenticationEntryPointby overriding configure(ResourceServerSecurityConfigurer resources)and using resources.authenticationEntryPoint(customAuthEntryPoint())inside the method.

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

Something like this:

像这样的东西:

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

There's also a nice OAuth2AuthenticationEntryPointthat can be extended (since it's not final) and partially re-used while implementing a custom AuthenticationEntryPoint. In particular, it adds "WWW-Authenticate" headers with error-related details.

还有一个很好的OAuth2AuthenticationEntryPoint可以扩展(因为它不是最终的)并在实现自定义AuthenticationEntryPoint. 特别是,它添加了带有错误相关详细信息的“WWW-Authenticate”标头。

Hope this will help someone.

希望这会帮助某人。

回答by Gabriel Villacis

Taking answers from @Nicola and @Victor Wing and adding a more standardized way:

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

}

Now, you can inject configured Hymanson, Jaxb or whatever you use to convert response bodies on your MVC annotation or XML based configuration with its serializers, deserializers and so on.

现在,您可以注入已配置的 Hymanson、Jaxb 或任何用于转换 MVC 注释或基于 XML 的配置及其序列化器、反序列化器等的响应主体的东西。

回答by Vinit Solanki

We need to use HandlerExceptionResolverin that case.

我们需要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);
    }
}

Also, you need to add in the exception handler class to return your object.

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

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

may you get an error at the time of running a project because of multiple implementations of HandlerExceptionResolver, In that case you have to add @Qualifier("handlerExceptionResolver")on HandlerExceptionResolver

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

回答by user3619911

I was able to handle that by simply overriding the method 'unsuccessfulAuthentication' in my filter. There, I send an error response to the client with the desired HTTP status code.

我能够通过简单地覆盖过滤器中的方法“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());
    }
}

回答by Melardev

Update:If you like and prefer to see the code directly, then I have two examples for you, one using standard Spring Security which is what you are looking for, the other one is using the equivalent of Reactive Web and Reactive Security:
- Normal Web + Jwt Security
- Reactive Jwt

The one that I always use for my JSON based endpoints looks like the following:

更新:如果您喜欢并且更喜欢直接查看代码,那么我为您提供两个示例,一个使用您正在寻找的标准 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());
    }
}

The object mapper becomes a bean once you add the spring web starter, but I prefer to customize it, so here is my implementation for ObjectMapper:

一旦你添加了 spring web starter,对象映射器就变成了一个 bean,但我更喜欢自定义它,所以这里是我对 ObjectMapper 的实现:

  @Bean
    public Hymanson2ObjectMapperBuilder objectMapperBuilder() {
        Hymanson2ObjectMapperBuilder builder = new Hymanson2ObjectMapperBuilder();
        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;
    }

The default AuthenticationEntryPoint you set in your WebSecurityConfigurerAdapter class:

您在 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);
    }
// ..........
}

回答by Buff

Customize the filter, and determine what kind of abnormality, there should be a better method than this

自定义过滤器,判断是什么异常,应该有比这个更好的方法

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

}

}

回答by Kemal Atik

In ResourceServerConfigurerAdapterbelow flow worked for me. http.exceptionHandling().authenticationEntryPoint(new AuthFailureHandler()).and.csrf()..did not work. That's why I wrote it as separate call.

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

Implementation of AuthenticationEntryPoint for catching token expiry and missing authorization header.

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

  }
}