【问题标题】:Make simple servlet filter work with @ControllerAdvice使用 @ControllerAdvice 制作简单的 servlet 过滤器
【发布时间】:2015-07-31 20:15:09
【问题描述】:

我有一个简单的过滤器,只是为了检查请求是否包含带有静态密钥的特殊标头 - 没有用户身份验证 - 只是为了保护端点。这个想法是如果键不匹配则抛出AccessForbiddenException,然后将其映射到带有@ControllerAdvice注释的类的响应。但是我不能让它工作。我的@ExceptionHandler 没有被调用。

ClientKeyFilter

import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Controller

import javax.servlet.*
import javax.servlet.http.HttpServletRequest

@Controller //I know that @Component might be here
public class ClientKeyFilter implements Filter {

  @Value('${CLIENT_KEY}')
  String clientKey

  public void init(FilterConfig filterConfig) {}

  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
    req = (HttpServletRequest) req
    def reqClientKey = req.getHeader('Client-Key')
    if (!clientKey.equals(reqClientKey)) {
      throw new AccessForbiddenException('Invalid API key')
    }
    chain.doFilter(req, res)
  }

  public void destroy() {}
}

AccessForbiddenException

public class AccessForbiddenException extends RuntimeException {
  AccessForbiddenException(String message) {
    super(message)
  }
}

异常控制器

@ControllerAdvice
class ExceptionController {
  static final Logger logger = LoggerFactory.getLogger(ExceptionController)

  @ExceptionHandler(AccessForbiddenException)
  public ResponseEntity handleException(HttpServletRequest request, AccessForbiddenException e) {
    logger.error('Caught exception.', e)
    return new ResponseEntity<>(e.getMessage(), I_AM_A_TEAPOT)
  }
}

我哪里错了?简单的 servlet 过滤器可以和 spring-boot 的异常映射一起工作吗?

【问题讨论】:

  • 过滤器永远不会发生这种情况。 @ControllerAdvice 仅对到达DispatcherServlet 的请求有用,Filters 总是在此之前执行。要么将该逻辑放在过滤器中,要么使用 HandlerInterceptor 代替过滤器。
  • @M.Deinum,我终于用上了HandlerInterceptor。如果您想将其添加为答案,我很乐意接受。

标签: spring filter exception-handling spring-security spring-boot


【解决方案1】:

正如java servlet 规范Filters 所指定的,总是在调用Servlet 之前执行。现在@ControllerAdvice 仅对在DispatcherServlet 内执行的控制器有用。因此,使用Filter 并期待@ControllerAdvice 或在本例中为@ExceptionHandler,将不会被调用。

您需要在过滤器中放入相同的逻辑(用于编写 JSON 响应),或者使用执行此检查的 HandlerInterceptor 代替过滤器。最简单的方法是扩展 HandlerInterceptorAdapter 并重写并实现 preHandle 方法并将过滤器中的逻辑放入该方法中。

public class ClientKeyInterceptor extends HandlerInterceptorAdapter {

    @Value('${CLIENT_KEY}')
    String clientKey

    @Override
    public boolean preHandle(ServletRequest req, ServletResponse res, Object handler) {
        String reqClientKey = req.getHeader('Client-Key')
        if (!clientKey.equals(reqClientKey)) {
          throw new AccessForbiddenException('Invalid API key')
        }
        return true;
    }

}

【讨论】:

  • 自从您发布答案后可能会发生变化,但现在的签名是:public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
【解决方案2】:

你不能使用@ControllerAdvice,因为它会在某些控制器出现异常的情况下被调用,但你的ClientKeyFilter不是@Controller

您应该将@Controller 注释替换为@Component,然后像这样设置响应正文和状态:

@Component
public class ClientKeyFilter implements Filter {

    @Value('${CLIENT_KEY}')
    String clientKey

    public void init(FilterConfig filterConfig) {
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String reqClientKey = request.getHeader("Client-Key");

        if (!clientKey.equals(reqClientKey)) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid API key");
            return;
        }

        chain.doFilter(req, res);
    }

    public void destroy() {
    }
}

【讨论】:

    【解决方案3】:

    Java 类中的 Servlet 过滤器用于以下目的:

    • 在客户端访问后端资源之前检查来自客户端的请求。
    • 在发送回客户端之前检查来自服务器的响应。

    @ControllerAdvice 可能无法捕获来自 Filter 的异常抛出,因为 in 可能无法到达 DispatcherServlet。我在我的项目中处理如下:

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws IOException, ServletException {
            String token = null;
            String bearerToken = request.getHeader("Authorization");
    
            if (bearerToken != null && (bearerToken.contains("Bearer "))) {
                if (bearerToken.startsWith("Bearer "))
                    token = bearerToken.substring(7, bearerToken.length());
                try {
                    AuthenticationInfo authInfo = TokenHandler.validateToken(token);
                    logger.debug("Found id:{}", authInfo.getId());
                    authInfo.uri = request.getRequestURI();
                    
                    AuthPersistenceBean persistentBean = new AuthPersistenceBean(authInfo);
                    SecurityContextHolder.getContext().setAuthentication(persistentBean);
                    logger.debug("Found id:'{}', added into SecurityContextHolder", authInfo.getId());
                    
                } catch (AuthenticationException authException) {
                    logger.error("User Unauthorized: Invalid token provided");
                    raiseException(request, response);
                    return;
                } catch (Exception e) {
                    raiseException(request, response);
                    return;
                }
    

    // 包装错误响应

    private void raiseException(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        ApiError apiError = new ApiError(HttpStatus.UNAUTHORIZED);
        apiError.setMessage("User Unauthorized: Invalid token provided");
        apiError.setPath(request.getRequestURI());
        byte[] body = new ObjectMapper().writeValueAsBytes(apiError);
        response.getOutputStream().write(body);
    }
    

    // ApiError 类

    public class ApiError {
        // 4xx and 5xx
        private HttpStatus status;
    
        // holds a user-friendly message about the error.
        private String message;
    
        // holds a system message describing the error in more detail.
        private String debugMessage;
    
        // returns the part of this request's URL
        private String path;
    
        public ApiError(HttpStatus status) {
          this();
          this.status = status;
        }
       //setter and getters
    

    【讨论】:

      猜你喜欢
      • 2011-02-23
      • 2012-04-25
      • 2014-04-07
      • 2015-03-25
      • 2014-10-26
      • 2012-07-25
      • 2011-12-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多