谷粒商城-高级-62 -商城业务-购物车-拦截器及 ThreadLocal 用户身份鉴别

一、使用redis作为session存储

由于购物车的数据都是保存在Redis中,所以,需要先导入redis依赖,由于购物车数据比较重要特殊,正式上线后,需要给购物车专门配置一个Redis服务器,不应该和我们的Redis混合一起使用,测试阶段暂不区分。

1、导入相关依赖

gulimall-cart/pom.xml

 <!-- redis -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- 整合SpringSession完成session共享问题,使用redis作为session存储 -->
    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
    </dependency>

2、配置

配置文件添加redis、session配置:
gulimall-cart/src/main/resources/application.yml

spring:
  #  配置nacos注册中心
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: gulimall-cart
  thymeleaf:
    cache: false   # 测试期间关掉缓存
  redis:
    host: 192.168.10.10
    prot: 6379
  session:
    store-type: redis  # Session store type,SpringSession整合,使用redis作为session存储
server:
  port: 21000
  servlet:
    session:
      timeout: 30m  # Session timeout,SpringSession整合

启动类添加开启Redis作为Session存储注解:
gulimall-cart/xxx/cart/GulimallCartApplication.java

package com.atguigu.gulimall.cart;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@EnableRedisHttpSession // 整合redis作为session存储
@EnableFeignClients // 添加注册发现功能
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallCartApplication {

  public static void main(String[] args) {
    SpringApplication.run(GulimallCartApplication.class, args);
  }

}

3、子域共享Session及JSon序列化存储

gulimall-cart/xxx/cart/config/GulimallSessionConfig.java

package com.atguigu.gulimall.cart.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

/**
 * @author: kaiyi
 * @create: 2020-09-09 17:05
 */
// @EnableRedisHttpSession //除了在启动类开启,这里也可以开启
@Configuration
public class GulimallSessionConfig {

  /**
   * 子域名共享设置及session名自定义
   * @return
   */
  @Bean
  public CookieSerializer cookieSerializer(){
    DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();

    defaultCookieSerializer.setDomainName("gulimall.com");   // 设置作用域
    defaultCookieSerializer.setCookieName("GULISESSION");     // 设置session名

    return defaultCookieSerializer;
  }

  /**
   * 默认序列化转为JSON存储
   *
   * @return
   */
  @Bean
  public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
    return new GenericJackson2JsonRedisSerializer();
  }
}

二、拦截器

拦截器在方法执行前(Controller),会去做一些判断处理,如果校验通过继续执行下边的业务,如果校验不通过,则会做相关的处理。拦截器想要共享变量,则需使用ThreadLocal。

创建拦截器:gulimall-cart/xxx/cart/interceptor/CartInterceptor.java

package com.atguigu.gulimall.cart.interceptor;

import com.atguigu.common.constant.AuthServerConstant;
import com.atguigu.common.constant.CartConstant;
import com.atguigu.common.vo.MemberResponseVo;
import com.atguigu.gulimall.cart.to.UserInfoTo;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;

/**
 * 拦截器(用于对Controller方法的拦截)
 *
 * @Description:
 * 在执行目标方法之前,判断用户的登录状态.并封装传递给controller目标请求
 *
 * @author: kaiyi
 * @create: 2020-09-12 11:14
 */
public class CartInterceptor implements HandlerInterceptor {

  public static ThreadLocal<UserInfoTo> toThreadLocal = new ThreadLocal<>();

  /***
   * 目标方法执行之前
   *
   * @思路:
   * 如果用户登录了,则有userId,如果用户没登录则只有userKey
   *
   * @param request
   * @param response
   * @param handler
   * @return
   * @throws Exception
   */
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    UserInfoTo userInfoTo = new UserInfoTo();

    HttpSession session = request.getSession();

    //获得当前登录用户的信息
    MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);

    // 将数据提前封装
    if (memberResponseVo != null) {
      //用户登录了
      userInfoTo.setUserId(memberResponseVo.getId());
    }

    Cookie[] cookies = request.getCookies();
    if (cookies != null && cookies.length > 0) {
      for (Cookie cookie : cookies) {
        //user-key
        String name = cookie.getName();
        if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)) {
          userInfoTo.setUserKey(cookie.getValue());
          //标记为已是临时用户
          userInfoTo.setTempUser(true);
        }
      }
    }

    //如果没有临时用户一定分配一个临时用户
    if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
      String uuid = UUID.randomUUID().toString();
      userInfoTo.setUserKey(uuid);
    }

    //目标方法执行之前
    toThreadLocal.set(userInfoTo);
    return true;
  }

  /**
   * 业务执行之后,分配临时用户来浏览器保存
   * 
   * 将临时数据写入 cookie
   * 
   * @param request
   * @param response
   * @param handler
   * @param modelAndView
   * @throws Exception
   */
  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    //获取当前用户的值
    UserInfoTo userInfoTo = toThreadLocal.get();

    //如果没有临时用户一定保存一个临时用户
    if (!userInfoTo.getTempUser()) {
      //创建一个cookie
      Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
      //扩大作用域
      cookie.setDomain("gulimall.com");
      //设置过期时间
      cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
      response.addCookie(cookie);
    }

  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

  }
}

拦截器创建好之后,还需要将拦截器放在Web配置中,否则拦截器不起作用。
创建拦截器web配置:gulimall-cart/xxx/cart/interceptor/GulimallWebConfig.java

package com.atguigu.gulimall.cart.interceptor;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器Web配置
 *
 * @Description:
 * 创建的拦截器必须添加到Web配置中
 *
 * @author: kaiyi
 * @create: 2020-09-12 11:14
 */

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CartInterceptor())//注册拦截器
                .addPathPatterns("/**");   // 拦截所有请求路径
    }
}

控制器:
com/atguigu/gulimall/cart/controller/CartController.java

/**
 * @author: kaiyi
 * @create: 2020-09-12 10:58
 */
public class CartController {

  @Resource
  private CartService cartService;

    /**
   * 去购物车页面的请求
   * 浏览器有一个cookie:user-key 标识用户的身份,一个月过期
   * 如果第一次使用jd的购物车功能,都会给一个临时的用户身份:
   * 浏览器以后保存,每次访问都会带上这个cookie;
   *
     * [拦截器]拦截器做判断处理
     * 
   * 登录:session有
   * 没登录:按照cookie里面带来user-key来做
   * 第一次,如果没有临时用户,自动创建一个临时用户
  *
   * @return
   */
  @GetMapping(value = "/cart.html")
  public String cartListPage(Model model) throws ExecutionException, InterruptedException {
    //快速得到用户信息:id,user-key
        // Thread t = Thread.currentThread();
    // UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();

    CartVo cartVo = cartService.getCart();
    model.addAttribute("cart",cartVo);
    return "cartList";
  }
}

说明:当浏览器请求cart.html连接,会找到 CartController 控制器的 cartListPage() 方法,在执行该方法前拦截器会先做处理,并做数据的整理,如登录信息的封装,并通过ThreadLocal传递数据,Controller,Service层都可以拿到拦截器封装的数据(CartInterceptor.toThreadLocal.get())。

三、ThreadLocal用户身份鉴别

ThreadLocal的作用是,同一个线程可以共享数据,每一个线程不会互相干扰。

file

ThreadLocal线程的核心原理,其实很简单,在一个Map里边放一个线程ID和对应线程的数据。

Map<Thread,Object> threadLocal

相关文件:
谷粒商城-高级-60 -商城业务-认证服务-分布式 Session

为者常成,行者常至