✅P239_商城业务-购物车-ThreadLocal用户身份鉴别

gong_yz大约 4 分钟谷粒商城

开篇

用户身份鉴别方式

  • 当用户登录之后点击购物车,则进行用户登录
  • 用户未登录的时候点击购物车,会为临时用户生成一个name为user-key的cookie临时标识,过期时间为一个月,以后每次浏览器进行访问购物车的时候都会携带user-key。user-key 是用来标识和存储临时购物车数据的

ThreadLocal 用户身份鉴别

ThreadLocal-同一个线程共享数据

  • 核心原理是:Map<Thread,Object> threadLocal
  • 在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。线程之间互不干扰

user-key在cookie里,标识用户身份,第一次使用购物车,都会给一个临时用户信息,浏览器保存cookie后,每次访问都会从cookie中取到user-key。

  • 在调用购物车的接口前,先通过session信息判断是否登录,并分别进行用户身份信息的封装
  • session有用户信息则进行用户登录
  • session中没有用户信息
    • cookie中携带 user-key,则表示有临时用户,把user-key进行用户身份信息的封装;
    • cookie中未携带 user-key,则表示没有临时用户,进行分配

将信息封装好放进ThreadLocal。

在调用购物车的接口后,若cookie中未携带 user-key,则分配临时用户,让浏览器保存。


业务实现

pom依赖

cfmall-cart/pom.xml

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

配置Session

cfmall-cart/src/main/java/com/gyz/cfmall/config/CfMalllSessionConfig.java

package com.gyz.cfmall.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.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

@Configuration
public class CfMalllSessionConfig {
    /**
     * 方法作用域,解决子域共享问题
     *
     * @return
     */
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("CFMALLSESSION");
        serializer.setDomainName("cfmall.com");
        return serializer;
    }

    /**
     * 使用json序列化将对象序列化存储到redis中
     *
     * @return
     */
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

编写传输对象与常量

cfmall-cart/src/main/java/com/gyz/cfmall/to/UserInfoTo.java

package com.gyz.cfmall.to;

import lombok.Data;

@Data
public class UserInfoTo {
    private Long userId;
    private String userKey;
    /**
     * 是否为临时用户
     */
    private Boolean tempUser = false;
}

cfmall-common/src/main/java/com/gyz/common/constant/CartConstant.java

public class CartConstant {

    public final static String TEMP_USER_COOKIE_NAME = "user-key";
    public final static int TEMP_USER_COOKIE_TIMEOUT = 60*60*24*30;
}

编写拦截器

拦截器逻辑

  • 业务执行之前,判断是否登录,若登录则封装用户信息,将是否为临时用户标识位设置为true,postHandler就不再设置作用域和有效时间,否则为其创建一个user-key

注意细节

  • 整合SpringSession之后,Session获取数据都是从Redis中获取的

使用ThreadLocal,解决线程共享数据问题,方便同一线程共享传输对象UserInfoTo

实现逻辑

package com.gyz.cfmall.interceptor;

import com.gyz.cfmall.to.UserInfoTo;
import com.gyz.common.constant.AuthServerConstant;
import com.gyz.common.constant.CartConstant;
import com.gyz.common.vo.MemberResponseVo;
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;

public class CartInterceptor implements HandlerInterceptor {

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

    /**
     * 目标方法执行之前
     *
     * @param request
     * @param response
     * @param handler
     * @return
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        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);
        }

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


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

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

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

    }

}

配置拦截器

配置拦截器,否则拦截器不生效。

cfmall-cart/src/main/java/com/gyz/cfmall/config/CfmallWebConfig.java

package com.gyz.cfmall.config;

import com.gyz.cfmall.interceptor.CartInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CfmallWebConfig implements WebMvcConfigurer {

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

编写Controller

cfmall-cart/src/main/java/com/gyz/cfmall/controller/CartController.java

@Controller
public class CartController {
    @GetMapping("/cartList.html")
    public String listPage() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        System.out.println(userInfoTo);
        return "cartList";
    }
}

测试

请求GET:http://localhost:9100/cartList.htmlopen in new window 接口,

通过断点的方式查看UserInfoTo结果,输出如下:

UserInfoTo(userId=null, userKey=bc917f86-f899-42cb-8f02-e4c43bdaf75b, tempUser=false)