使用@ExceptionHandler处理spring安全性身份验证异常

Nic*_*ola 79 spring spring-mvc spring-security

我正在使用Spring MVC @ControllerAdvice@ExceptionHandler处理REST Api的所有异常.它适用于Web mvc控制器抛出的异常,但它不适用于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);

        }

    }

}
Run Code Online (Sandbox Code Playgroud)

使用此自定义入口点:

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

}
Run Code Online (Sandbox Code Playgroud)

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

@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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我需要做的是返回一个详细的JSON主体,即使是Spring安全性AuthenticationException也是如此.有没有办法让Spring安全AuthenticationEntryPoint和spring mvc @ExceptionHandler一起工作?

我正在使用spring security 3.1.4和spring mvc 3.2.4.

Nic*_*ola 47

好吧,我按照建议自己从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() + "\" }");

    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 使用Jackson的示例:ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(),新的FailResponse(401,authException.getLocalizedMessage(),“访问被拒绝”,“”))); (2认同)
  • @leventunver 在这里您可以找到如何注册入口点:http://stackoverflow.com/questions/24684806/how-to-define-a-custom-authenticationentrypoint-without-xml-configuration。 (2认同)

Vic*_*ong 35

这是一个非常有趣的问题,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;
    }
}
Run Code Online (Sandbox Code Playgroud)

第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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

第3步 - 注册入口点

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

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

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


小智 19

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

然后你可以使用@ExceptionHandler以你想要的方式格式化响应.

  • 奇迹般有效.如果Spring抛出一个错误,说有2个bean定义为autowirering,你必须添加限定符注释:@Autowired @Qualifier("handlerExceptionResolver")private HandlerExceptionResolver resolver; (7认同)
  • 请注意,如果您在注释上指定了 basePackages,则通过传递 null 处理程序,您的 `@ControllerAdvice` 将无法工作。我不得不完全删除它以允许调用处理程序。 (2认同)

Vin*_*nki 6

我们需要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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

@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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Vla*_*lin 5

在弹簧引导的情况下和@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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

希望这会帮助某人。

  • 我正在尝试这个,但是我的“AuthenticationEntryPoint”的“commence()”函数没有被调用——我错过了什么吗? (2认同)