5

上次在项目中做一个手机验证码功能时遇到后端set-cookie无法成功的问题,相信很多人都会遇到,今天分享出来,算是做个记录,也给有需要的小伙伴一个参考,下面先说一下场景

手机验证码实现步骤

  1. 用户输入手机号并点击发送验证码按钮

    此时是向后端服务器发起高请求,传递手机号码
  2. 后端接收到手机号码后通过短信服务商的接口发送短信到手机号

    由于安全性问题,一般服务商的接口都是由后端发起请求,那验证码是怎么来的呢?当然是我们自己后端生成的(一般是生产一个4位或6位随机数字号码),短信接口只负责发送
  3. 用户收到验证码后,在表单中输入并发送验证
  4. 后端收到验证码与第2步生成的进行校验,一致则通过,否则不通过

问题来了,我们都知道http请求是无状态的,即每个请求都是独立的,两个请求间没有任何联系,但上面的步骤中,用户发起了两次请求(第1步和第3步),后端怎么知道是同一个人呢?万一当时有10个人同时获取验证码,且都执行完所有步骤,怎么实现各自第1和第3步的对应关系进而进行第4步校验?于是我们引入session

什么是session

session是保存的服务器的一个数据存储机制,一般用于存储用户状态数据,你可以理解为客户端的cookie,因为它们很像,同样是有key/value和有效时间,只是一个在服务端,一个在客户端。这跟本文有什么关系呢?肯定有关系,关系大了去了,不然我写干嘛(自嗨中~~~)

咳~咳。。回归正题,刚刚说了http是无状态的,而我们现在又要把第1步和第3步关联起来,怎么关联呢?session+cookie出马,具体原理如下

  1. 第一次接到请求,后关会生成一个sessionid来标识当前会话(我使用express-session来实现),并通过set-cookie响应头在客户端生一个cookie,大概长这样,

        connect.sid=s%3Au9xG34DBU1vOVbIpCax0neMxL_Uc1fIC.4ndNJL5G%2B41DtUSLbQ%2BW75Z9wduOAON4lfu2JGTDe5
  2. 由于cookie会自动发送给服务器,所以当前用户后面的所有请求都会携带这个sessionid给服务器,服务器通过这个sessionid来标识是否为同一个用户,问题就迎刃而解了...

手机验证码功能具体实现步骤

  1. 前端:

    1. 用户输入手机号,点击获取验证码
    2. 发送请求到后端,
  2. 后端:

    1. 后端生成随机验证码,保存在session,并给前端响应connect.sid(express-session会Set-Cookie响应头)
    2. 并通过手机接口发送给用户手机号(需要后端配置手机短信接口,一般需要购买,这里不做额外说明)
  3. 前端:

    1. 浏览器接收到Set-Cookie响应头后,自动把sessionid写入浏览器cookie
    2. 用户接收到手机验证码并填写到对应输入框
    3. 用户点击按钮发送验证码到后端进行校验(sessionidcookie自动发送到后端)
  4. 后端:

    1. 后端拿到验证码,并与第3步保存在session的随机验证码进行比较,一致则响应成功,否则响应失败

通过以上步骤,正常情况下你已经能实现手机验证码功能了,可我偏偏遇到了非正常情况,这也是我今天这篇文章的意义,由于开发阶段为前后端分离,请求是产生了跨域,明明已经正确响应了Set-Cookie,允许了CORS跨域,但浏览器的cookie中就是看不到connect.sid的身影,关键浏览器还不报错。

于是各种千里寻她千百度,最后发现默认情况下,标准的跨域请求是不会发送cookie等用户认证凭据的,同样,后端通过Set-Cookie在跨域时默认是被浏览器忽略的,解决的方案是两步:

  1. 后端设置"Access-Control-Allow-Credentials":true响应头

    PS: 设置该响应头后,Access-Control-Allow-Origin的值不能设置为 *,必须设置为具体域名
  2. 前端发起请求时设置 withCredentials:true 请求头
    // XMLHttpRequest
    const xhr = new XMLHttpRequest();
    xhr.open('get',url,true);
    xhr.withCredentials = true;
    xhr.send()
    
    // axios
    axios.get(url,{
        ...
       withCredentials:true
    })
    
    // fetch
    fetch(url,{
        ...
        credentials: 'include'
    })

搞定,收工,希望对你有帮助


老谢
9 声望5 粉丝