【面试题】深入理解Cookie、Session、Token的区别
【面试题】深入理解Cookie、Session、Token的区别
【面试题】深入理解Cookie、Session、Token的区别
Cookie与Session
Cookie
Cookie,它是客户端浏览器用来保存服务端数据的一种机制。
当通过浏览器进行网页访问的时候,服务器可以把某一些状态数据以 key-value的方式写入到 Cookie 里面存储到客户端浏览器。
然后客户端下一次再访问服务器的时候,就可以携带这些状态数据发送到服务器端,服务端可以根据 Cookie 里面携带的内容来识别使用者。
Session
Session 表示一个会话,它是属于服务器端的容器对象。
默认情况下,针对每一个浏览器的请求,Servlet 容器都会分配一个 Session。
Session 本质上是一个 ConcurrentHashMap,可以存储当前会话产生的一些状态数据。
Cookie与Session之前的联系
我们都知道,Http 协议本身是一个无状态协议,也就是服务器并不知道客户端发送过来的多次请求是属于同一个用户。
所以 Session 是用来弥补 Http 无状态的不足,简单来说,服务器端可以利用session 来存储客户端在同一个会话里面的多次请求记录。
基于服务端的 session 存储机制,再结合客户端的 Cookie 机制,就可以实现有状态的 Http 协议。
cookie存储是有效期,可以自行通过expires进行具体的日期设置;如果没设置,默认是关闭浏览器时失效。
当客户端存储的cookie失效后,服务端的session不会立即销毁,会有一个延时,服务端会定期清理无效session,不会造成无效数据占用存储空间的问题。
Cookie与Session的在请求中的工作流程
(1)客户端第一次访问服务端的时候,服务端会针对这次请求创建一个会话,并生成一个唯一的 sessionID 来标注这个会话。
(2)然后服务端把这个 sessionID 写入到客户端浏览器的 cookie 里面,用来实现客户端状态的保存。
(3)在后续的请求里面,每次都会携带sessionID,服务器端就可以根据这个sessionID 来识别当前的会话状态。
所以,总的来说,Cookie 是客户端的存储机制,Session 是服务端的存储机制。
Cookie与Session存在问题
当项目的并发量越来越大的时候,我们一台服务器不够,想要有多台Tomcat来部署一个Tomcat集群时,由于Session是不能共享的,所以该场景不适用!
如果是前后端分离的项目,即前端代码和后端代码部署在不同的服务器上时,也是不可以使用Session进行登入的!
Token
什么是Token?
token的意思是“令牌”,是服务端生成的一串字符串,作为客户端进行请求的一个标识。
当用户第一次登录后,服务器生成一个token,并将此token返回给客户端,以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。
为什么要有token?
-
Token 可以是无状态的,可以在多个服务间共享
- Token 可以避免 CSRF攻击(跨站点请求伪造)
- 减轻服务器压力。通常session是存储在内存中的,每个用户通过认证之后都会将session数据保存在服务器的内存中,而当用户量增大时,服务器的压力增大。
token认证机制
Token流程
- 客户端使用用户名和密码请求登录。
- 服务端收到请求,验证用户名和密码。
- 验证成功后,服务端会生成一个token(存在redis中),然后把这个token发送给客户端。
- 客户端收到token后把它存储起来,可以放在cookie或者Local Storage(本地存储)里。
- 客户端每次向服务端发送请求的时候都需要带上服务端发给的token。
- 服务端收到请求,然后去验证客户端请求里面带着token,如果验证成功,就向客户端返回请求的数据。(如果这个 Token 在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌。)
请求登录时,token和sessionId原理相同,是对key和key对应的用户信息进行加密后的加密字符,登录成功后,会在响应主体中将{token:'字符串'}返回给客户端。
客户端通过cookie、sessionStorage、localStorage都可以进行存储。再次请求时不会默认携带,需要在请求拦截器位置给请求头中添加认证字段Authorization携带token信息,服务器端就可以通过token信息查找用户登录状态。
cookie、sessionStorage、localStorage的区别
-
cookie、sessionStorage、localStorage 都是用于本地存储的技术。其中 cookie 出现最早,但是存储容量较小,仅有4KB;sessionStorage、localStorage存储容量要比cookie大很多,为5MB。
-
接下来对 sessionStorage、localstorage 进行比较。其中sessionStorage 的数据存储局限于浏览器窗口,只适合于单页面应用程序使用,因为sessionStorage打开浏览器新标签,会话状态不会共享;而localstorage 的数据存储位于本地文件夹中,用户关闭浏览器后再次打开时,数据仍会存在。
代码实现
登入功能
第一次登入的时候,会走下面的逻辑,生成一个随机、唯一的token。将<token,user> 存入redis
之后,如果在这个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<String, Object> 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();
}
}
Redis的token自动刷新拦截器
从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<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 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);
}
}