分布式会话拦截器

633 阅读3分钟

思路

用户执行登录,注册等操作时,后端会生成一个token放入cookie中返回给前端,同时保存到redis中, 前端后续涉及验证tokenhttp请求都需要在header中携带token和用户userId, 后端设置interceptor拦截指定请求,拿到header中的token, 后端通过 userIdredis中查找,如果查到并且二者匹配的上,次请求通过拦截,否则http请求无法到达controller

image-20200629113855412
image-20200629113855412

拦截器 vs 过滤器

过滤器和拦截器有相似之处,都能对 Servlet 请求二次加工。但是过滤器并不是 SpringBoot 规范中的概念,事实上,过滤器是 Servlet 规范中的事物。

因此过滤器和拦截器的最大区别就是他们存在的空间是不一样的。

Filter 拦截器是 Servlet 中的规范,它可不依赖于 Spring,它是由 Servlet 容器 Filter 每个请求和响应。它可以在请求到达 Servlet 之前就处,因此 Filter 也总是优先于 Interceptor 执行。

img
img

nterceptor 过滤器是工作在 Spring 容器中的,由 Spring 所控制,因此能和 Spring 紧密的结合,在 Spring 中使用拦截器,处理拦截行为更方便,事实上 Filter 能做的事情,Interceptor 也都能实现。

img
img

拦截器和过滤器执行时机

IjOIC
IjOIC

springboot 过滤器使用

https://www.baeldung.com/spring-boot-add-filter

@Component
@Order(1)
public class TransactionFilter implements Filter {
  private static final Logger LOGGER = LoggerFactory.getLogger(TransactionFilter.class);

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

    LOGGER.info( "Starting a transaction for req : {}",
            req.getRequestURI());

    chain.doFilter(request, response);
    LOGGER.info(
            "Committing a transaction for req : {}",
            req.getRequestURI());
  }
}

springboot 拦截器使用

请求->过滤器->拦截器的preHandle()->控制器Controller->拦截器的postHandle()->视图页面渲染->拦截器的afterCompletion()

img
img

1 声明自定义拦截器

/**
 * 自定义:用户会话请求拦截器
 * 
 * @author shengding
 */
@Component
public class UserTokenInterceptor implements HandlerInterceptor {
  private static final Logger LOGGER = LoggerFactory.getLogger(UserTokenInterceptor.class);

  @Autowired
  RedisOperator redisOperator;

  /**
   * 拦截请求,请求访问到controller之前
   * @param request
   * @param response
   * @param handler
   * @return false : 请求被拦截,被驳回
   *         true  : 请求通过拦截器,是OK的,是可以放行的
   * @throws Exception
   */
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    // 对于复杂的Ajax 跨域请求,浏览器会首先发送一个 OPTIONS 请求,如果返回200,则继续发送真正的请求
    // 配置了拦截器后,由于 OPTIONS请求时不携带 自定义header的,所以 preHandle返回false, OPTIONS请求被拦截,
    // 后续跨域设置失效
    if(HttpMethod.OPTIONS.name().equals(request.getMethod().toUpperCase())) {
      return true;
    }

    // http请求header中携带的token
    String requestHeaderUserToken = request.getHeader("headerUserToken");

    // http请求header中携带的userId
    String requestHeaderUserId = request.getHeader("headerUserId");
    String redisUserToken = redisOperator.get(RedisKey.USERTOKEN.getKey() + ":" + requestHeaderUserId);

    LOGGER.info("请求进入拦截器--->, 请求header中userId: {}, token: {}; redis中用户token: {}", requestHeaderUserId, requestHeaderUserToken, redisUserToken);
    // http携带token不为空且与redis所存的token一致,则通过拦截,否则视为非法请求
    if(StringUtils.isNotBlank(requestHeaderUserId) && StringUtils.isNotBlank(requestHeaderUserToken)) {
      if(StringUtils.isBlank(redisUserToken)) {
        returnErrorResponse(response, JSONResult.errMsg("请登录..."));
        return false;
      } else {
        if(!requestHeaderUserToken.equals(redisUserToken)) {
          returnErrorResponse(response, JSONResult.errMsg("账号异地登录..."));
          return false;
        }
      }
    } else {
      returnErrorResponse(response, JSONResult.errMsg("请登录..."));
      return false;
    }
    return true;
  }

  /**
   * 请求访问到controller, 渲染视图之前
   * @param request
   * @param response
   * @param handler
   * @param modelAndView
   * @throws Exception
   */
  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

  }

  /**
   * 请求访问到controller, 渲染视图之后
   * @param request
   * @param response
   * @param handler
   * @param ex
   * @throws Exception
   */
  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

  }
}

2 注册拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

  @Autowired
  UserTokenInterceptor userTokenInterceptor;

  /**
   * 注册拦截器(自定义拦截器只有注册了才生效)
   * @param registry
   */
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(userTokenInterceptor)
            .addPathPatterns("/shopcart/add")
            .addPathPatterns("/shopcart/del")

            .addPathPatterns("/address/list")
            .addPathPatterns("/address/add")
            .addPathPatterns("/address/update")
            .addPathPatterns("/address/setDefault")
            .addPathPatterns("/address/del")

            .addPathPatterns("/orders/getPaidOrderInfo")
            .addPathPatterns("/orders/items")
            .addPathPatterns("/orders/receive")
            .addPathPatterns("/orders/create")

            .addPathPatterns("/comments/list")
            .addPathPatterns("/comments")

            .addPathPatterns("/center/trend")
            .addPathPatterns("/center/statusCounts")
            .addPathPatterns("/center/orders")
            .addPathPatterns("/center/userface")
            .addPathPatterns("/center/userInfo")

            .addPathPatterns("/pay/getPaidOrderInfo");
  }
}

3 封装错误返回消息

  /**
   * 由于拦截器 preHandle 返回 boolean值, 当请求被拦截时若想返回json格式,需要手动设置response
   * @param response
   * @param jsonResult
   */
  public void returnErrorResponse(HttpServletResponse response, JSONResult jsonResult) {
    response.setContentType("application/json");
    response.setCharacterEncoding("utf-8");
    try(OutputStream outputStream = response.getOutputStream()) {
      outputStream.write(JsonUtils.objectToJson(jsonResult).getBytes());
      outputStream.flush();
    } catch(IOException e) {
      e.printStackTrace();
    }
  }

4 前端发起请求

axios.post(
  serverUrl + '/orders/getPaidOrderInfo?orderId=' + orderId,
  {},
  {
    headers: {
      'headerUserId': userInfo.id,
      'headerUserToken': userInfo.userUniqueToken
    }
  })

5 测试

image-20200629130436228
image-20200629130436228