【面试题】深入理解Cookie、Session、Token的区别
cookie、sessionStorage、localStorage的区别
Cookie,它是客户端浏览器用来保存服务端数据的一种机制。
当通过浏览器进行网页访问的时候,服务器可以把某一些状态数据以 key-value的方式写入到 Cookie 里面存储到客户端浏览器。
然后客户端下一次再访问服务器的时候,就可以携带这些状态数据发送到服务器端,服务端可以根据 Cookie 里面携带的内容来识别使用者。
Session 表示一个会话,它是属于服务器端的容器对象。
默认情况下,针对每一个浏览器的请求,Servlet 容器都会分配一个 Session。
Session 本质上是一个 ConcurrentHashMap,可以存储当前会话产生的一些状态数据。
我们都知道,Http 协议本身是一个无状态协议,也就是服务器并不知道客户端发送过来的多次请求是属于同一个用户。
所以 Session 是用来弥补 Http 无状态的不足,简单来说,服务器端可以利用session 来存储客户端在同一个会话里面的多次请求记录。
基于服务端的 session 存储机制,再结合客户端的 Cookie 机制,就可以实现有状态的 Http 协议。
cookie存储是有效期,可以自行通过expires进行具体的日期设置;如果没设置,默认是关闭浏览器时失效。
当客户端存储的cookie失效后,服务端的session不会立即销毁,会有一个延时,服务端会定期清理无效session,不会造成无效数据占用存储空间的问题。

(1)客户端第一次访问服务端的时候,服务端会针对这次请求创建一个会话,并生成一个唯一的 sessionID 来标注这个会话。
(2)然后服务端把这个 sessionID 写入到客户端浏览器的 cookie 里面,用来实现客户端状态的保存。
(3)在后续的请求里面,每次都会携带sessionID,服务器端就可以根据这个sessionID 来识别当前的会话状态。
所以,总的来说,Cookie 是客户端的存储机制,Session 是服务端的存储机制。
当项目的并发量越来越大的时候,我们一台服务器不够,想要有多台Tomcat来部署一个Tomcat集群时,由于Session是不能共享的,所以该场景不适用!
如果是前后端分离的项目,即前端代码和后端代码部署在不同的服务器上时,也是不可以使用Session进行登入的!
token的意思是“令牌”,是服务端生成的一串字符串,作为客户端进行请求的一个标识。
当用户第一次登录后,服务器生成一个token,并将此token返回给客户端,以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。
Token 可以是无状态的,可以在多个服务间共享

请求登录时,token和sessionId原理相同,是对key和key对应的用户信息进行加密后的加密字符,登录成功后,会在响应主体中将{token:'字符串'}返回给客户端。
客户端通过cookie、sessionStorage、localStorage都可以进行存储。再次请求时不会默认携带,需要在请求拦截器位置给请求头中添加认证字段Authorization携带token信息,服务器端就可以通过token信息查找用户登录状态。
cookie、sessionStorage、localStorage 都是用于本地存储的技术。其中 cookie 出现最早,但是存储容量较小,仅有4KB;sessionStorage、localStorage存储容量要比cookie大很多,为5MB。
接下来对 sessionStorage、localstorage 进行比较。其中sessionStorage 的数据存储局限于浏览器窗口,只适合于单页面应用程序使用,因为sessionStorage打开浏览器新标签,会话状态不会共享;而localstorage 的数据存储位于本地文件夹中,用户关闭浏览器后再次打开时,数据仍会存在。
第一次登入的时候,会走下面的逻辑,生成一个随机、唯一的token。将
之后,如果在这个redis的key的TTL内,再次登入,就不需要走这个逻辑了!会走下面的拦截器的逻辑!
- @Override
- public Result login(LoginFormDTO loginForm, HttpSession session) {
-
- //1. 校验手机号
- String phone = loginForm.getPhone();
- if (RegexUtils.isPhoneInvalid(phone)) {
- return Result.fail("手机号格式错误");
- }
-
- //2. 从redis获取验证码并校验
- String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
- String code = loginForm.getCode();
- if (cacheCode == null || !cacheCode.toString().equals(code)){
- //3. 不一致,报错
- return Result.fail("验证码错误");
- }
-
- //4.一致,根据手机号查询用户
- User user = query().eq("phone", phone).one();
-
- //5. 判断用户是否存在
- if (user == null){
- //6. 不存在,创建新用户
- user = createUserWithPhone(phone);
- }
-
- //7.保存用户信息到redis
- //7.1 生成随机token作为登入令牌
- String token = UUID.randomUUID().toString(true);
-
- //7.2 将User对象作为HashMap存储
- UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
- // 注意!!!
- Map
userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(), - CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fileName,fileValue) -> fileValue.toString()));
-
- //7.3 存储
- String tokenKey = LOGIN_USER_KEY + token;
- stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
-
- //7.4设置token的有效期
- stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
-
- //8.返回token
- return Result.ok(token);
- }
- @Component
- public class LoginInterceptor implements HandlerInterceptor {
-
- // 基于Redis设置的拦截器
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
-
- //判断是否要拦截
- if (UserHolder.getUser() == null) {
- response.setStatus(401);
- return false;
- }
-
- //有用户,放行
- return true;
- }
-
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- //移除用户
- UserHolder.removeUser();
- }
- }
从HTTP请求头中获取 authorization (因为这里的前端将用户的token放在了这里面)!

在拦截器中,用刚刚解析出来的token,去查redis,判断是否有该用户(是否在登入有效期内)
用户每次操作都会自动刷新(推迟) Token 的过期时间
- @Component
- public class RefreshTokenInterceptor implements HandlerInterceptor {
-
- @Autowired
- private StringRedisTemplate stringRedisTemplate;
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- // 1.获取请求头中的token
- String token = request.getHeader("authorization");
- if (StrUtil.isBlank(token)) {
- return true;
- }
- // 2.基于TOKEN获取redis中的用户
- String key = LOGIN_USER_KEY + token;
- Map
- // 3.判断用户是否存在
- if (userMap.isEmpty()) {
- return true;
- }
- // 5.将查询到的hash数据转为UserDTO
- UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
- // 6.存在,保存用户信息到 ThreadLocal
- UserHolder.saveUser(userDTO);
- // 7.刷新token有效期
- stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
- // 8.放行
- return true;
- }
-
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- // 移除用户
- UserHolder.removeUser();
- }
- }
将上述两个拦截器,加到 MvcConfig 里面
- @Configuration
- public class MvcConfig implements WebMvcConfigurer {
-
- @Autowired
- private LoginInterceptor loginInterceptor;
-
- @Autowired
- private RefreshTokenInterceptor refreshTokenInterceptor;
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- // 登录拦截器
- registry.addInterceptor(loginInterceptor)
- .excludePathPatterns(
- "/shop/**",
- "/voucher/**",
- "/shop-type/**",
- "/upload/**",
- "/blog/hot",
- "/user/code",
- "/user/login"
- ).order(1);
-
- // token刷新的拦截器
- registry.addInterceptor(refreshTokenInterceptor)
- .addPathPatterns("/**").order(0);
- }
- }