Skip to content

跨域请求中服务器给浏览器设置 Cookie 需要同时满足下面三个条件:

  • res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080') 没有设置为 *

  • res.setHeader('Access-Control-Allow-Credentials', 'true')

  • 客户端开启 withCredentials

javascript
const http = require('http')
const uuid = require('uuid')
const crypto = require('crypto')

// 一般通过 OpenSSL 生成 1024 字节的密钥
const key = '0.27276630965318804'
const session = Object.create(null)

// 签名
const sign = v => {
  return crypto.createHmac('sha256', key).update(v.toString()).digest('base64')
}

const server = http.createServer((req, res) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5501')
  res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, POST')
  res.setHeader('Access-Control-Allow-Credentials', 'true')

  req.getCookie = (key, options = {}) => {
    const cookies = req.headers.cookie.split('; ')
    const cookieMap = Object.create(null)

    for (let i = 0, l = cookies.length; i < l; i++) {
      const cookie = cookies[i]
      const index = cookie.indexOf('=')
      const key = cookie.slice(0, index)
      const value = cookie.slice(index + 1)

      cookieMap[key] = value
    }

    const val = cookieMap[key]

    if (!val) {
      return ''
    }

    if (options.signed) {
      const [rawVal, signed] = val.split('.')

      if (sign(rawVal) === signed) {
        return signed
      }

      return '' // cookie 被篡改
    }

    return val
  }
  
  res.setCookie = (() => {

    // 一次设置多个 cookie
    let cookie = ''

    return (key, val, options = {}) => {

      // 签名,同时还要保留原来的值,用于后续验证 cookie 是否被篡改,因此 cookie 不存
      // 敏感信息。
      // 不用 MD5,可以根据值计算出 MD5,然后修改为计算出的 MD5。
      if (options.sign) {
        val += `.${ sign(val) }`
        delete options.sign
      }
  
      cookie += `${ key }=${ val }; `
  
      for (const option in options) {
        cookie += `${ option }=${ options[option] }; `
      }
  
      res.setHeader('Set-Cookie', cookie)
    }
  })()

  if (req.url === '/foo') {
    res.setCookie('foo', 'foo', { 'max-age': 1000000, sign: true })
    // res.setCookie('bar', 'bar', { 'max-age': 1000000})
    res.end()
  }

  if (req.url === '/bar') {
    res.end(req.getCookie('foo', { signed: true }))
  }
})

server.listen(3000)

session

javascript
if (req.url === '/baz') {
  const cookie = req.getCookie('connect.sid')

  // 之后登录
  if (cookie && session[cookie]) {
    res.end()
  } 
  // 第一次登录或 cookie 过期或服务器重启等
  else {
    const _uuid = uuid.v4()

    // 如果不想在 cookie 中暴露任何信息,使用 session,用户信息可以保存在服务端,
    // 但比较少这样做,一般 session 中都是保存着用户标识,用这个标识去数据库中
    // 取数据。
    session[_uuid] = {}
    res.setCookie('connect.sid', _uuid)
    res.end()
  }
}

流程

  • 表单校验防止 SQL 注入和 XSS 攻击。

  • 对于隐私信息进行 MD5 加密。

  • 向服务器发送 POST 请求。

  • 服务器获取传递的数据。

  • 去数据库中查找符合账号和密码的用户

  • 没找到,返回登录失败并提示;找到了,把用户的基本信息存储在服务器的 session 中并产生 一个唯一标识 connect.sid,在返回客户端信息时基于在响应头中的 Set-Cookie 字段中存储 connect.sid。

  • 浏览器把 connect.sid 设置在本地的 cookie 中,之后进行登陆成功的提示和页面的跳转,还 可以用变量记录是否已经登录。

  • 刷新或重新打开页面时,登录标识消失,向服务器发 GET 请求并携带 cookie 验证之前是否登 录过。

  • 服务器获取到 connect.sid,去 session 中找,如果找到了,认为当前用户是登录的,否则 说明第一次登录或 cookie 过期 或服务器重启(如果 session 保存在服务器内存中)或 数据库中 session 丢失(session 持久化),重新生成 connect.sid,保存进 session 和 Set-Cookie 字段中。