128

我正在使用 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);

        }

    }

}

使用此自定义入口点:

@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。

4

13 回答 13

78

好的,我尝试按照建议自己从 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,但您会序列化一些类实例。

在 Spring Boot 中,您应该将其添加到 SecurityConfiguration 文件的 http.authenticationEntryPoint() 部分。

于 2013-11-05T12:11:24.123 回答
40

这是一个非常有趣的问题,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 来完成。我这里只是展示一下相关配置,应该还有其他配置如 session stateless等。

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

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

于 2014-10-22T07:29:38.587 回答
39

我发现的最好方法是将异常委托给 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 以您想要的方式格式化响应。

于 2017-10-02T16:51:14.927 回答
8

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

于 2018-10-05T09:03:06.457 回答
5

在 Spring Boot 和 的情况下,扩展而不是在 Java 配置中并通过在方法内部覆盖和使用来注册自定义@EnableResourceServer是相对容易和方便的。ResourceServerConfigurerAdapterWebSecurityConfigurerAdapterAuthenticationEntryPointconfigure(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的可以扩展(因为它不是最终的)并在实现自定义时部分重用AuthenticationEntryPoint。特别是,它添加了带有错误相关详细信息的“WWW-Authenticate”标头。

希望这会对某人有所帮助。

于 2017-06-05T15:30:10.690 回答
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 的配置上的响应主体的东西。

于 2016-08-16T15:48:52.897 回答
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());
    }
}
于 2019-02-06T17:14:19.023 回答
1

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

  }
}

于 2020-05-18T15:41:18.483 回答
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;
    }
}

}

于 2020-04-08T05:36:43.247 回答
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);
    }
// ..........
}
于 2019-06-23T19:05:10.143 回答
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)
     }}
于 2017-10-06T12:26:35.403 回答
0

我只创建一个类来处理有关身份验证的所有异常

@Component 公共类 JwtAuthenticationEntryPoint 实现 AuthenticationEntryPoint {

private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest httpServletRequest,
                     HttpServletResponse httpServletResponse,
                     AuthenticationException e) throws IOException, ServletException {
    logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
    httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}

}

于 2021-10-23T10:01:41.767 回答
0

您可以改用 objectMapper 来写入值

ApiError response = new ApiError(HttpStatus.UNAUTHORIZED);
String message = messageSource.getMessage("errors.app.unauthorized", null, httpServletRequest.getLocale());
response.setMessage(message);
httpServletResponse.setContentType("application/json");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
OutputStream out = httpServletResponse.getOutputStream();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(out, response);
out.flush();
于 2021-05-04T11:47:06.753 回答