使用 @ControllerAdvice 制作简单的 servlet 过滤器

Posted

技术标签:

【中文标题】使用 @ControllerAdvice 制作简单的 servlet 过滤器【英文标题】:Make simple servlet filter work with @ControllerAdvice 【发布时间】: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。如果您想将其添加为答案,我很乐意接受。 【参考方案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

【讨论】:

以上是关于使用 @ControllerAdvice 制作简单的 servlet 过滤器的主要内容,如果未能解决你的问题,请参考以下文章

如何在同一个 SpringMvc 应用程序中有两个 ControllerAdvice

@ControllerAdvice三种使用场景

@ConTrollerAdvice的使用

@ControllerAdvice 用法

Kotlin 的 ControllerAdvice

spring boot中@ControllerAdvice的用法