问题引入
在开发过程中需要对某些接口进行鉴权,一开始的想法是写个Filter,然后拦截指定的路径,通过获取header中的信息进行鉴权,没有权限则抛出自定义异常,然后自定义全局异常处理器再去拦截。
程序测试时发现,没权限时确实被拦截了,但是并没有进入到全局异常处理器,而是直接抛出500。通过查找发现,Filter的在进入Servlet之前进行拦截的,而全局异常是SpringBoot中的(本质是Servlet),所以全局异常处理并不能拦截到Filter抛出的异常。
基本概念
与这个相关的一些概念如下:
web.xml 的加载顺序是:context- param -> listener -> filter -> servlet
1、servlet:servlet是一种运行服务器端的java应用程序,具有独立于平台和协议的特性,并且可以动态的生成web页面,它工作在客户端请求与服务器响应的中间层。
2、filter:filter是一个可以复用的代码片段,可以用来转换HTTP请求、响应和头信息。Filter不像Servlet,它不能产生一个请求或者响应,它只是修改对某一资源的请求,或者修改从某一的响应。
3、listener:监听器,从字面上可以看出listener主要用来监听只用。通过listener可以监听web服务器中某一个执行动作,并根据其要求作出相应的响应。通俗的语言说就是在application,session,request三个对象创建消亡或者往其中添加修改删除属性时自动执行代码的功能组件。
4、interceptor:是在面向切面编程的,就是在你的service或者一个方法,前调用一个方法,或者在方法后调用一个方法,比如动态代理就是拦截器的简单实现,在你调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在你调用方法后打印出字符串,甚至在你抛出异常的时候做业务逻辑的操作。
Filter和Interceptor对比
过滤器Filter
是在请求进入容器后,但在进入servlet
之前进行预处理,请求结束是在servlet
处理完以后。
拦截器 Interceptor
是在请求进入servlet
后,在进入Controller
之前进行预处理的,Controller
中渲染了对应的视图之后请求结束。
解决Filter抛出异常无法全局捕获
由上面知道,在filter中抛出的自定义异常是无法在spring的全局异常处理器中捕获到的。那么介绍三种方案去解决这个问题。
1.自定义response返回
@Component
@Slf4j
public class authFilter implements Filter {
private String elasticJobCode;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
elasticJobCode = ConfigService.getAppConfig().getProperty("wormhole.code", "");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 认证相关
String signNew = "";
if(signNew.equals(sign)){
//认证成功
filterChain.doFilter(request, response);
} else{
//认证失败
updateResponse(response, ReturnCode.NO_PERMISSION);
}
}
@Override
public void destroy() {
}
private void updateResponse(HttpServletResponse response, ReturnCode errorCode) {
PrintWriter writer = null;
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
try {
writer = response.getWriter();
if (writer != null) {
writer.print(errorCode.toString());
}
} catch (IOException e) {
log.error("authenticate error : {}", errorCode, e);
} finally {
if (writer != null) {
writer.flush();
writer.close();
}
writer = null;
}
}
}
2.重写SpringBoot内置的对异常进行统一处理的Controller–BasicErrorController
(不建议)
- 自定义异常
public class MyException extends RuntimeException {
private static final long serialVersionUID = -1909845654770784460L;
private final int code;
public int getCode() {
return code;
}
public MyException(ResponseDto<?> responseDto) {
super(responseDto.getMsg());
this.code = responseDto.getCode();
}
public MyException(int code, String message) {
super(message);
this.code = code;
}
public MyException(ErrorEnum errorEnum) {
super(errorEnum.getMessage());
this.code = errorEnum.getCode();
}
}
- 自定义全局异常处理器
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
/**
* 参数check异常
* @param e
* @param <T>
* @return
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public <T> ResponseDto<T> methodArgumentNotValidExceptionHandle(MethodArgumentNotValidException e) {
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
return ResponseDto.fail(ErrorEnum.PARAM_ERROR.getCode()
, CollectionUtils.isEmpty(allErrors) ? ErrorEnum.PARAM_ERROR.getMessage() : allErrors.get(0).getDefaultMessage());
}
/**
* 业务异常
* @param e
* @param <T>
* @return
*/
@ExceptionHandler(value = MyException.class)
public <T> ResponseDto<T> creditExceptionHandler(MyException e){
return ResponseDto.fail(e.getCode(), e.getMessage());
}
/**
* 未知异常
* @param request
* @param e
* @param <T>
* @return
*/
@ExceptionHandler({Exception.class})
public <T> ResponseDto<T> unknownExceptionHandle(HttpServletRequest request, Exception e) {
return ResponseDto.fail(ErrorEnum.SERVER_ERROR.getCode(), ErrorEnum.SERVER_ERROR.getMessage());
}
- 自定义filter
@Component
public class IpFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getHeader("afaefew") == null){
throw new MyException(ErrorEnum.APPID_NOT_EXISTS);
}
filterChain.doFilter(request, response);
}
}
- 配置Filter
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public FilterRegistrationBean myFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean(new IpFilter());
registration.addUrlPatterns("/test");
registration.setOrder(4);
return registration;
}
}
- 自定义 BasicErrorController
@RestController
public class ErrorController extends BasicErrorController {
public ErrorController() {
super(new DefaultErrorAttributes(), new ErrorProperties());
}
@Override
@RequestMapping(produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
// 获取错误信息
String code = "333";
String message = "返回异常";
ApiErrorResult apiErrorResult = new ApiErrorResult(false,code,message);
return new ResponseEntity<>(apiErrorResult,status);
}
}
public class ApiErrorResult extends LinkedHashMap<String,Object> {
private static final String SUCCESS_KEY = "success";
private static final String CODE_KEY = "code";
private static final String MESSAGE_KEY = "message";
public ApiErrorResult(boolean success, String code, String message) {
this.put(SUCCESS_KEY,success);
this.put(CODE_KEY,code);
this.put(MESSAGE_KEY,message);
}
}
- 测试
通过postMan调用
但是浏览器直接请求还是有问题,这里需要记录下,但是这个方法比较复杂,并不推荐使用。
3.通过HandlerExceptionResolver
注意点 因为Filter的加载优先于spring容器初始化实例,所以使用@Autowired肯定为null, 如下的调用很定会出现空指针。
@Component
public class IpFilter implements Filter {
/** 在Filter中注入HandlerExceptionResolver 会出现空指针**/
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getHeader("afaefew") == null){
// 这里会出现空指针
resolver.resolveException(request, response, null, new MyException(ErrorEnum.APPID_NOT_EXISTS));
return;
}
filterChain.doFilter(request, response);
}
}
正确的做法是,
方法一:用ApplicationContext根据bean名称(注意名称为实现类而不是接口)去获取bean,随便写个工具类即可
// spring context 工具类
package com.njit.filterinterceptordemo.common;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringBeanUtil implements ApplicationContextAware {
//ApplicationContext对象是Spring开源框架的上下文对象实例,在项目运行时自动装载Handler内的所有信息到内存。
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringBeanUtil.applicationContext == null) {
SpringBeanUtil.applicationContext = applicationContext;
}
}
//获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通过name获取 Bean.
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
//通过class获取Bean.
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
//通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
修改Filter
@Component
public class IpFilter implements Filter {
// /** 在Filter中注入HandlerExceptionResolver 会出现空指针**/
// @Autowired
// @Qualifier("handlerExceptionResolver")
// private HandlerExceptionResolver resolver;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
HandlerExceptionResolver handlerExceptionResolver = (HandlerExceptionResolver)SpringBeanUtil.getBean("handlerExceptionResolver");
if (request.getHeader("afaefew") == null){
handlerExceptionResolver.resolveException(request, response, null, new MyException(ErrorEnum.APPID_NOT_EXISTS));
return;
}
filterChain.doFilter(request, response);
}
}
方法二:上面通过SpringBeanUtil 可以拿到这个handlerExceptionResolver,还有一种方法,就是重写Filter的init方法,通过入参filterConfig
获取bean,具体代码如下:
@Component
public class IpFilter implements Filter {
// /** 在Filter中注入HandlerExceptionResolver 会出现空指针**/
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
//解决filter无法依赖注入的问题
ServletContext sc = filterConfig.getServletContext();
WebApplicationContext cxt = WebApplicationContextUtils.getWebApplicationContext(sc);
if (cxt.getBean("handlerExceptionResolver") != null && resolver == null){
resolver = (HandlerExceptionResolver)cxt.getBean("handlerExceptionResolver");
}
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// HandlerExceptionResolver handlerExceptionResolver = (HandlerExceptionResolver)SpringBeanUtil.getBean("handlerExceptionResolver");
if (request.getHeader("afaefew") == null){
resolver.resolveException(request, response, null, new MyException(ErrorEnum.APPID_NOT_EXISTS));
return;
}
filterChain.doFilter(request, response);
}
}
postman测试,发现可以正常的进入到全局异常处理器了。
postman测试,发现可以正常的进入到全局异常处理器了。
参考文档
SpringBoot统一异常拦截处理(filter中的异常无法被拦截处理)
总结
在SpringBoot的项目中,如果需要拦截并希望用全局异常处理器去处理的话,就用Interceptor,其他的情况两种的使用差别不是很大,都能完成你的需求。
在这种需要拦截请求后抛出自定义异常的,然后需要在全局异常处理器中处理当的,更建议通过Interceptor来实现,而不是Filter
rticle/details/87777285)
总结
在SpringBoot的项目中,如果需要拦截并希望用全局异常处理器去处理的话,就用Interceptor,其他的情况两种的使用差别不是很大,都能完成你的需求。
在这种需要拦截请求后抛出自定义异常的,然后需要在全局异常处理器中处理当的,更建议通过Interceptor来实现,而不是Filter