使用@ControllerAdvice 制作简单的 servlet 过滤器
Make simple servlet filter work with @ControllerAdvice
我有一个简单的过滤器,用于检查请求是否包含带有静态密钥的特殊 header - 无用户身份验证 - 只是为了保护端点。这个想法是如果密钥不匹配则抛出一个 AccessForbiddenException
然后将映射到带有 class 注释的响应 @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)
}
}
ExceptionController
@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
,因为它会在某些控制器出现异常时被调用,但您的 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() {
}
}
如 java servlet 规范所指定,Filter
s 总是在调用 Servlet
之前执行。现在 @ControllerAdvice
仅对在 DispatcherServlet
内执行的控制器有用。因此,使用 Filter
并期望调用 @ControllerAdvice
或在本例中 @ExceptionHandler
是不会发生的。
您需要在过滤器中放入相同的逻辑(用于编写 JSON 响应)或者使用 HandlerInterceptor
which does this check. The easiest way is to extend the 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;
}
}
Javaclasses 中的 Servlet 过滤器用于以下目的:
- 在客户端访问后端资源之前检查来自客户端的请求。
- 在发送回客户端之前检查来自服务器的响应。
过滤器抛出的异常可能无法被 @ControllerAdvice 捕获,因为可能无法到达 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 class
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
我有一个简单的过滤器,用于检查请求是否包含带有静态密钥的特殊 header - 无用户身份验证 - 只是为了保护端点。这个想法是如果密钥不匹配则抛出一个 AccessForbiddenException
然后将映射到带有 class 注释的响应 @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)
}
}
ExceptionController
@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
,因为它会在某些控制器出现异常时被调用,但您的 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() {
}
}
如 java servlet 规范所指定,Filter
s 总是在调用 Servlet
之前执行。现在 @ControllerAdvice
仅对在 DispatcherServlet
内执行的控制器有用。因此,使用 Filter
并期望调用 @ControllerAdvice
或在本例中 @ExceptionHandler
是不会发生的。
您需要在过滤器中放入相同的逻辑(用于编写 JSON 响应)或者使用 HandlerInterceptor
which does this check. The easiest way is to extend the 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;
}
}
Javaclasses 中的 Servlet 过滤器用于以下目的:
- 在客户端访问后端资源之前检查来自客户端的请求。
- 在发送回客户端之前检查来自服务器的响应。
过滤器抛出的异常可能无法被 @ControllerAdvice 捕获,因为可能无法到达 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 class
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