目录 §

  • 1. 动态服务器 V.S. 静态服务器
  • 2. 数据库dbuser.json
  • 3. 目标1 用户注册
  • 4. 目标2 登录功能
  • 5. 目标3 Cookie标记用户已登录
  • 6. Set-Cookie响应头
  • 7. 目标4 用户注册
  • 8. 目标5 防篡改user_id
  • 9. Cookie/Session总结
  • 10. 目标6 注销
  • 11. 目标7 防止密码泄露
  • 12. 大总结
  • 13. 技巧
  • 14. Cookie其他细节

1. 动态服务器 V.S. 静态服务器 §

也叫静态网页 V.S. 动态网页

1.1 判断依据

是否请求了数据库

  • 没有请求数据库,就是 静态服务器
  • 请求了数据库,就是 动态服务器

直接用JSON文件当做数据库

2. 使用文件user.json模拟数据库 §

数据结构:一个JSON数组

1
2
3
4
[
{"id":1, "name": "frank", "password": "***", "age": 18},
{"id":2, "name": "jack", "password": "***", "age": 20}
]
  • 注意JSON格式的双引号,其它引号是错误的写法,也不能省略

读取users数据

  • fs.readFileSync('./db/user.json').toString()
  • 然后JSON.parse('string'),反序列化,得到数组

创建test.js

1
2
3
4
// 读取数据库
const fs = require('fs')
const usersString = fs.readFileSync('./db/users.json').toString()
// console.log(usersString)

打开终端 运行命令 node test.js

  • 打出的是字符串,但需要的是JSON数组
1
2
3
4
5
6
// 读取数据库
const fs = require('fs')
const usersString = fs.readFileSync('./db/users.json').toString()
const usersArray = JSON.parse(usersString)
// console.log(usersString)
// console.log(usersArray)
  • 后台无法“看出”类型
  • 控制台打出类型
1
2
3
4
5
6
7
8
9
// 读取数据库
const fs = require('fs')
const usersString = fs.readFileSync('./db/users.json').toString()
const usersArray = JSON.parse(usersString)
// console.log(typeof usersString)
// console.log(usersString)
// console.log(typeof usersArray)
// console.log('is Array:' + (usersArray instanceof Array))
// console.log(usersArray)

存储users数据

  • 文件 只能存储字符串
  • JSON.stringify(json)(序列化),得到字符串
  • 序列化即字符串化;反序列化,即反字符串化
  • 然后fs.writeFileSync('./db/users/json', data)存入文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 写数据库
const user3 = {
  id: 3,
  name: "tom",
  password: "yyy"
}
usersArray.push(user3)

// 文件只能存储字符串
const string = JSON.stringify(usersArray)
fs.writeFileSync('./db/users.json', string)
  • 每运行一次就添加一次
  • 简化数据库

3. 目标1:用户注册 §

实现用户注册功能

  • 用户提交用户名和密码
  • 跳转到登录页面
  • user.json里新增一行数据

思路

  • 前端
    • 写一个form,让用户填写namepassword
    • 监听submit事件
    • 发送post请求,数据位于 请求体
  • 后端
    • 接受post请求
    • 获取请求体中的namepassword
    • 存储数据

前端register.html

  • 清空users.json,留一个空数组[]
  • try catch去判断const usersArray = JSON.parse(usersString)是否为空
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<body>
  <form action="" id="registerForm">
    <div>
      <label for="">用户名
        <input type="text" name="name">
      </label>
    </div>
    <div>
      <label for="">密码
        <input type="password" name="password">
      </label>
    </div>
    <div>
      <button type="submit">注册</button>
    </div>
  </form>
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script src="./register.js"></script>
</body>

前端register.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const $form = $('#registerForm')
$form.on('submit', (e) => {
  //阻止默认事件
  //preventDefault()[dom标准写法(ie678不兼容)]
  //ie678用returnValue
  //或者利用return false也能阻止默认行为,没有兼容问题(只限传统注册方式)
  e.preventDefault()
  // 获取用户名和密码
  const name = $form.find('input[name=name]').val()
  const password = $form.find('input[name=password]').val()
  // console.log(typeof name, typeof password)
  // console.log(name, password)

  // 提交获取的数据 发一个AJAX请求
  $.ajax({
    url: '/register',
    /*     data: JSON.stringify({
          name: name,
          password: password
        }), */
    data: JSON.stringify({
      name,
      password
    })
  })
})
  • 不写method,默认method: 'GET', 默认会将data直接接在请求头后面GET /register?{%22name%22:%22xxx%22,%22password%22:%22yyy%22} HTTP/1.1
  • POST,在请求体里传数据 Query String Parameters

前端register.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const $form = $('#registerForm')
$form.on('submit', (e) => {
  e.preventDefault()
  const name = $form.find('input[name=name]').val()
  const password = $form.find('input[name=password]').val()
  $.ajax({
    method: 'POST',
    url: '/register',
    data: JSON.stringify({
      name,
      password
    })
  })
})
  • 数据传输格式Content-Type,为application/x-www-form-urlencoded; charset=UTF-8,不为JSON
  • 设置Request HeadersContent-Typetext/json; charset=UTF-8

前端register.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const $form = $('#registerForm')
$form.on('submit', (e) => {
  e.preventDefault()
  const name = $form.find('input[name=name]').val()
  const password = $form.find('input[name=password]').val()
  $.ajax({
    method: 'POST',
    url: '/register',
    contentType: "text/json; charset=utf-8",
    data: JSON.stringify({
      name,
      password
    })
  })
})
  • 上传一个JSON
  • 服务器获取一个JSON
  • 即最简单的注册
  • 前端的活完了

获取POST数据,这是后端的活

  • 添加动态服务器的路由:/register
  • 判断请求方法是否是POST
  • 设置请求头的Content-Type
  • 设置返回的数据

如何拿到用户上传的namepassword

后端server.js

1
2
3
4
5
6
7
8
9
//...
  // 动态服务器
  if (path === '/register' && method === 'POST') {
    response.setHeader('Content-Type', "text/html;charset=utf-8")
    response.end("很好")
  } else {
  // 静态服务器 - [static-server-1](https://github.com/xmasuhai/static-server-1)
}
//...

POST拿数据

  • 获取当前数据库中的数据
  • 声明一个数组存储数据 数据有可能是分段(数据块chunk)上传的, 而数据的大小未知,必须一点一点上传数据,获取时也是一点一点获取
  • 发送请求时 监听请求 传来一个数据过来时 放入数组
  • 监听响应结束,获取最后一个用户(id最大 userArray.length - 1)的数据,更新数据,将数据 转化为字符串覆盖写入数据库

后端server.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 动态服务器
if (path === '/register' && method === 'POST') {
    response.setHeader('Content-Type', "text/html;charset=utf-8")

    // 获取当前数据库中的数据
    const userArray = JSON.parse(fs.readFileSync('./db/users.json'))

    // 声明一个数组塞数据 数据有可能是分段(数据块chunk)上传的 数据的大小未知
    const array = []
    // 监听发送请求
    // 请求传来一个数据过来 放入数组
    request.on('data', (chunk) => {
      array.push(chunk)
    })
    // 监听请求结束
    request.on('end', () => {
      const string = Buffer.concat(array).toString()

      // console.log(string)
      // console.log(array)

      const obj = JSON.parse(string)
      console.log('obj.name: ' + obj.name, 'obj.password:' + obj.password)

      // id 为最后一个用户的id + 1
      let lastUser = userArray[userArray.length - 1]
      const newUser = {
        id: lastUser ? lastUser.id + 1 : 1,
        name: obj.name,
        password: obj.password
      }

      // 更新数据
      userArray.push(newUser)
      // 将数据 转化为字符串覆盖写入数据库
      fs.writeFileSync('./db/users.json', JSON.stringify(userArray))

      response.end("很好")
    })
}

前端register.js 登录成功跳转

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const $form = $('#registerForm')
$form.on('submit', (e) => {
  // 阻止默认事件 刷新页面
  // preventDefault()[dom标准写法(ie678不兼容)]
  // ie678用returnValue
  // 或者利用return false也能阻止默认行为,没有兼容问题(只限传统注册方式)
  e.preventDefault()
  // 获取用户名和密码 的值
  const name = $form.find('input[name=name]').val()
  const password = $form.find('input[name=password]').val()

  // console.log(typeof name, typeof password)
  // console.log(name, password)

  // 提交获取的数据 发一个AJAX请求
  $.ajax({
    method: 'POST',
    url: '/register',
    /*
    data: JSON.stringify({
          name: name,
          password: password
    }),
    */
    contentType: "text/json; charset=utf-8",
    data: JSON.stringify({
      name,
      password
    })
  }).then(
    // 成功
    () => {
      alert('注册成功')
      // window.open()
      location.href = '/sign_in.html'
    },
    // 失败
    () => {
      alert('注册失败')
    })
})

4. 目标2:登录功能 §

实现用户登录功能,跳转首页登录

  • 首页home.html,已登录用户可以看到自己的用户名
  • 登录页sign_in.html,用来提交用户名和密码
  • 输入的用户名和密码如果匹配,就自动跳转首页

sign_in.html思路

  • 前端
    • 写一个form,让用户填写namepassword
    • 监听submit事件
    • 发送post请求,数据位于请求体
  • 后端
    • 接受post请求
    • 获取请求体中的namepassword
    • 读取数据库,看是否有匹配的namepassword
    • 如果匹配,后端应标记用户已登录,如何标记

前端home.html

1
2
3
4
<body>
  <p> 你好, {{user.name}}</p>
  <p> <a href="sign_in.html">登录</a></p>
</body>
  • HTML 不能直接读取数据库,用占位符{{user.name}}

后端server.js 添加home.html的路由

1
2
3
4
if(/**/true){}
else if (path === '/home.html') {
    // 当前用户未知
  }

目标2太大,目标尽量小,实现标记用户是否已登录 server.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 登录
if (path === '/sign_in' && method === 'POST') {
    response.setHeader('Content-Type', "text/html;charset=utf-8")

    // 获取当前数据库中的用户名密码
    const userArray = JSON.parse(fs.readFileSync('./db/users.json'))

    // 声明一个数组塞数据 数据有可能是分段(数据块chunk)上传的 数据的大小未知
    const array = []
    // 监听发送请求
    // 请求传来一个数据过来 放入数组
    request.on('data', (chunk) => {
      array.push(chunk)
    })

    // 监听请求结束
    request.on('end', () => {
      // console.log(array)
      const string = Buffer.concat(array).toString()
      console.log(string)
      const obj = JSON.parse(string) // name password
      // console.log('obj.name: ' + obj.name, 'obj.password:' + obj.password)

      // 是否有匹配的 name 和 password
      const user = userArray.find((user) => user.name === obj.name && user.password === obj.password)

      if (user === undefined) {
        response.statusCode = 400
        response.end('name password不匹配')
      } else {
        response.statusCode = 200
        response.end()
      }
      response.end()
    })
}

sign.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function sign(signId, signUrl, signHref, signMsg) {
  const $form = $(signId)
  $form.on('submit', (e) => {
    e.preventDefault()
    const name = $form.find('input[name=name]').val()
    const password = $form.find('input[name=password]').val()

    $.ajax({
      method: 'POST',
      url: signUrl,
      contentType: "text/json; charset=utf-8",
      data: JSON.stringify({
        name,
        password
      })
    }).then(
      () => {
        alert(signMsg + '成功')
        location.href = signHref
      },
      () => {
        alert(signMsg + '失败')
      })
  })
}

if ($('#registerForm')[0]) {
  sign('#registerForm', '/register', '/sign_in.html', '注册')
  // console.log($('#registerForm')[0])
} else {
  sign('#signInForm', '/sign_in', '/home.html', '登录')
  // console.log($('#signInForm')[0])
}
  • 控制台测试成功
  • 失败时改为返回一个JSON,包含自定义的错误码{"errorCode": 4001}

server.js中添加路由 /home.html

1
2
3
4
5
if(/*...*/true) {/*...*/}
else if (path === '/home.html') {
    // 当前用户未知
    response.end("home")
  }
  • 分支中必须以response.end()未结尾结束,否则一直处于padding
  • 监听事件中的response.end()是异步的,必须在每个分支条件后立即执行,而不可写在if分支外的最后,即直接执行response.end(),而不执行分支中的语句

修改home.html

1
2
3
4
5
<body>
  <!-- <p> 你好, {{user.name}}</p> -->
  <p> 你好, {{loginStatus}}</p>
  <p> <a href="sign_in.html">登录</a></p>
</body>
  • {{loginStatus}}替换为true/false
  • 有问题:登录成功跳转到home.html发的请求和直接跳转发的一样

如何识别用户是否登录?

  • 使用Cookie

5. 目标3:Cookie 标记用户已登录 §

后端服务器给用户浏览器发Cookie,判断用户名和密码匹配数据库中的数据成功时发Cookie

6. Set-Cookie响应头 §

后端server.js 匹配到已有的用户数据,即登录成功后,在响应头中设置

1
response.setHeader('Set-Cookie', 'logined=1')
  • 提示登录成功后查看 NetworkResponse Headers 中的 Set-Cookie 设置成功
  • Application中的Cookies已存储
  • const cookie = request.headers['cookie']
  • 注意cookie小写
  • 无痕窗口登录的cookie就是undefined
  • MDN文档语法
  • Node.js 文档 Buffer
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 监听请求结束
request.on('end', () => {
  const string = Buffer.concat(array).toString()
  const obj = JSON.parse(string) // name password

  // 判断是否有匹配的 name 和 password ,并且拿到存进变量user
  const user = userArray.find((user) => (user.name === obj.name && user.password === obj.password))

  if (user === undefined) {
    response.statusCode = 400
    // response.end('name password不匹配')
    response.end(`{"errorCode":4001}`)
  } else {
    response.statusCode = 200
    response.setHeader('Set-Cookie', 'logined=1')
    response.end()
  }
})
  • HTML文件const homeHtml = fs.readFileSync('./public/home.html').toString()注意.toString()
  • 注意路径'./public/home.html'

home.html辨别是否登录

  • 测试打开home.html,未登录的cookie就是undefined
  • 成功登录后,查看home.htmlNetwork -> Request Headers -> Cookie
  • 浏览器已经保存了Cookie信息
  • 当在请求home.html时,浏览器自动将Cookie设置的信息放到请求头中
  • 无痕窗口不带Cookie
  • 这些行为都受到浏览器控制

后端sever.js辨别cookie,从而辨别是否登录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if(/*...*/true){/*...*/}
 else if (path === '/home.html') {
    const cookie = request.headers['cookie'] // 小写的cookie
    // console.log(cookie)

    // 匹配则替换 home.html 里的文字
    if (cookie === 'logined=1') {
      const homeHtml = fs.readFileSync('./public/home.html').toString()
      const string = homeHtml.replace('{{loginStatus}}', '已登录')
      response.write(string)
    } else {
      const homeHtml = fs.readFileSync('./public/home.html').toString()
      const string = homeHtml.replace('{{loginStatus}}', '未登录')
      response.write(string)
    }
    response.end("home")
  }

发起相同二级域名请求(任何请求方式)时,浏览器自动附上Cookie

7. 目标4:用户注册 §

显示用户名

  • home.html渲染前获取 user 信息
  • 如果有user,则将{{user.name}}替换成user.name
  • 如果无user,则显示登录按钮

Bug

  • 用户可以篡改user_id
  • 开发者工具或者JS都能改
  • 未设置response.setHeader("Set-Cookie", "...; HttpOnly");
  • 可通过document.cookie取到cookie的值
  • 当然也能改掉document.cookie = xxxdocument.cookie = 'logined=0'
  • 可设置过期时限
  • 设置response.setHeader("Set-Cookie", "...; HttpOnly");,在浏览器端就读不到 Set-Cookie的信息,防止前端扰乱后端的信息,必须加上HttpOnly

永远只在后端设置Cookie

  • 前端可以伪造Cookie
  • 需设置response.setHeader("Set-Cookie", "...; HttpOnly");
  • 防止JScookie
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 登录
if (path === '/sign_in' && method === 'POST') {
    response.setHeader('Content-Type', "text/html;charset=utf-8")

    // 获取当前数据库中的用户名密码
    const userArray = JSON.parse(fs.readFileSync('./db/users.json'))

    // 声明一个数组塞数据 数据有可能是分段(数据块chunk)上传的 数据的大小未知
    const array = []
    // 监听发送请求
    // 请求传来一个数据过来 放入数组
    request.on('data', (chunk) => {
      array.push(chunk)
    })

    // 监听请求结束
    request.on('end', () => {
      const string = Buffer.concat(array).toString()
      const obj = JSON.parse(string) // name password

      // 判断是否有匹配的 name 和 password ,并且拿到存进变量user
      const user = userArray.find((user) => user.name === obj.name && user.password === obj.password)

      if (user === undefined) {
        response.statusCode = 400
        // response.end('name password不匹配')
        response.end(`{"errorCode":4001}`)
      } else {
        response.statusCode = 200
        response.setHeader('Set-Cookie', `logined=1;HttpOnly`) // 替换
        response.end()
      }
    })
}

开发者工具Application中仍可以改,在 目标5 防篡改user_id 解决


前端home.html辨别登录的是谁

1
2
3
4
5
<body>
  <!-- <p> 你好, {{user.name}}</p> -->
  <p> 你好, {{user.name}} {{loginStatus}}</p>
  <p> <a href="sign_in.html">登录</a></p>
</body>
  • 找到匹配的 useruserArray.find((user) => user.name === obj.name && user.password
  • logined=1 改成 user_id=${user.id}

后端server.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const user = userArray.find((user) => user.name === obj.name && user.password === obj.password)
  if (user === undefined) {
    response.statusCode = 400
    // response.end('name password不匹配')
    response.end(`{"errorCode":4001}`)
  } else {
    response.statusCode = 200
    // response.setHeader('Set-Cookie', `logined=1`)
    response.setHeader('Set-Cookie', `user_id=${user.id}`)
    response.end()
  }
  • 前端home.html通过Cookie里存的use.id 知道登录的是谁

后端server.js设置路由,错误处理,user.id判断,模板替换文字,显示用户名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
if(/**/true){}
else if (path === '/home.html') { // 跳转主页

    const cookie = request.headers['cookie'] // 小写的cookie
    console.log('cookie: ' + cookie)
    // 模板
    const homeHtml = fs.readFileSync('./public/home.html').toString()

    let userId // "string"
    try {
      // 将字符串 转为数组 处理
      // .split(';') // 分割成子字符串数组
      // .filter(s => s.indexOf('user_id') >= 0)[0] // 查找到"user_id"
      // 由response.setHeader('Set-Cookie', `user_id=${user.id}`)
      // .split('=')[1]
      userId = cookie.split(';')
        .filter(s => s.indexOf('user_id=') >= 0)[0]
        .split('=')[1]
    } catch (error) {}
    // console.log(cookie.split(';').filter(s => s.indexOf('user_id') >= 0)[0].split('=')[1])

    // 匹配则替换home.html里的文字
    if (userId) {
      // 获取当前数据库中的数据
      const userArray = JSON.parse(fs.readFileSync('./db/users.json'))

      //user.id "string"
      const user = userArray.find(user => user.id.toString() === userId)
      let string

      // 判断user是否存在
      if (user) {
        // 模板替换文字
        string = homeHtml
          .replace(/登录/g, '退出,重新登录')
          .replace(/{{loginStatus}}/g, '已登录')
          .replace(/{{user.name}}/g, user.name)
      } else {
        string = homeHtml
          .replace(/{{loginStatus}}/g, '未登录')
          .replace(/{{user.name}}/g, '请登录,游客')
      }
      response.write(string)
    } else {
      console.log('userId: ' + userId)
      string = homeHtml
        .replace(/{{loginStatus}}/g, '未登录')
        .replace(/{{user.name}}/g, '请登录,游客')
      response.write(string)
    }
    response.end("home")
  }

实现目标4+:显示用户名

  • home.html渲染前获取user信息
  • 如果有user,则将{{user.name}}替换成user.name
  • 如果无user,则像是登录按钮
  • response.end()不写的话,浏览器就一直等待,在Application里拿不到cookie
  • 分支中必须以response.end()未结尾结束,否则一直处于padding
  • 监听事件中的response.end()是异步的,必须在每个分支条件后立即执行,而不可写在分支外的最后,即直接执行response.end(),而不执行分支中的语句
  • 保证异步的语句在是时间上的最后执行,而不是代码的最后一行

回顾Bug

  • 用户可以篡改user_id
  • 开发者工具或者JS都能改
  • 未设置response.setHeader("Set-Cookie", "...; HttpOnly");
  • document.cookie可读可写
    • 可通过document.cookie取到cookie的值
    • 当然也能改掉document.cookie = xxxdocument.cookie = 'user_id=6'
  • 可设置过期时限
  • 设置response.setHeader("Set-Cookie", "...; HttpOnly");
    • 在浏览器端就读不到 Set-Cookie的信息,防止前端扰乱后端的信息,必须加上HttpOnly

永远只在后端设置Cookie

  • 前端可以伪造Cookie,骗取服务器的身份认证
  • 需设置response.setHeader("Set-Cookie", "...; HttpOnly");
  • 防止JS篡改cookie

开发者工具Application Cookies中仍可以改,相应的用户名显示也改变,十分危险

8. 目标5:防篡改user_id §

思路一 加密

  • user_id加密发送给前端,后端读取user_id时解密
  • 有安全漏洞:wifi 截获加密信息后,模拟登录,加密后的内容可无限期使用
  • 解决办法:JWTJSON Web Token):JSON Web Token 入门教程 阮一峰
  • MD5不可用来加密,有安全风险

思路二 把信息隐藏在服务器

  • 把用户信息放在服务器的 x
  • 再给x设置一个随机属性名:const random = (10 * Math.random()).toString(36).slice(2)
  • 值为包含user_id的对象{user_id: 8}
  • 把随机id发给浏览器
  • 后端下次读取到id时,通过 x[id] 获取用户信息
  • 为什么用户无法改 id (因为 id 很长,而且随机)
  • x是文件,不能用内存,因为断电内存就清空,而写入文件最终存储在硬盘上
  • 这个x又被叫做session.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const session = JSON.parse(fs.readFileSync('./session.json').toString())

/* ... */
response.statusCode = 200
const random = (10 * Math.random()).toString(36).slice(2)
session[random] = {
  user_id: user.id
}

// 写入session
fs.writeFileSync('./session.json', JSON.stringify(session))
// response.setHeader('Set-Cookie', `logined=1`) // 替换
// response.setHeader('Set-Cookie', `user_id=${user.id};HttpOnly`)
response.setHeader('Set-Cookie', `session_id=${random};HttpOnly`)
response.end()

在路由访问登录时,用随机数生成 user_id存储到session中,代替原来的response.setHeader('Set-Cookie', 'user_id=${user.id};HttpOnly')

server.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
const session = JSON.parse(fs.readFileSync('./session.json').toString())

/* 动态服务器 */

// 登录
if (path === '/sign_in' && method === 'POST') {
    response.setHeader('Content-Type', "text/html;charset=utf-8")
    // 获取当前数据库中的用户名密码
    const userArray = JSON.parse(fs.readFileSync('./db/users.json'))

    // 声明一个数组塞数据 数据有可能是分段(数据块chunk)上传的 数据的大小未知
    const array = []
    // 监听发送请求
    // 请求传来一个数据过来 放入数组
    request.on('data', (chunk) => {
      array.push(chunk)
    })

    // 监听请求结束
    request.on('end', () => {
      const string = Buffer.concat(array).toString()
      const obj = JSON.parse(string) // name password

      // 判断是否有匹配的 name 和 password ,并且拿到存进变量user
      const user = userArray.find((user) => user.name === obj.name && user.password === obj.password)

      if (user === undefined) {
        response.statusCode = 400
        // response.end('name password不匹配')
        response.end(`{"errorCode":4001}`)
      } else {
        response.statusCode = 200

        const random = (10 * Math.random()).toString(36).slice(2)
        // 每发一次请求 读取一次 session
        // const session = JSON.parse(fs.readFileSync('./session.json').toString())
        session[random] = {
          user_id: user.id
        }

        // 写入session
        fs.writeFileSync('./session.json', JSON.stringify(session))
        // response.setHeader('Set-Cookie', `logined=1`) // 替换
        // response.setHeader('Set-Cookie', `user_id=${user.id};HttpOnly`)
        response.setHeader('Set-Cookie', `session_id=${random};HttpOnly`)

        response.end()
      }
    })

    } else if (path === '/home.html') { // 跳转主页
        const cookie = request.headers['cookie'] // 小写的cookie
        console.log('cookie: ' + cookie)
        // 模板
        const homeHtml = fs.readFileSync('./public/home.html').toString()

        // let userId // "string"
        let sessionId // "string"
        try {
          /*
            userId = cookie.split(';')
            .filter(s => s.indexOf('user_id=') >= 0)[0]
            .split('=')[1]
          */

          sessionId = cookie
            .split(';')
            .filter(s => s.indexOf('session_id=') >= 0)[0]
            .split('=')[1]

        } catch (error) {}
        // console.log(cookie.split(';').filter(s => s.indexOf('user_id') >= 0)[0].split('=')[1])
        // console.log("free from browser by HttpOnly: sessionId: ")
        // console.log(sessionId)
        // console.log("hide from browser by HttpOnly: session[sessionId]: ")
        // console.log(session[sessionId])

        // 匹配则替换home.html里的文字
        if (sessionId && session[sessionId]) {
          const userId = session[sessionId].user_id

          // 获取当前数据库中的数据
          const userArray = JSON.parse(fs.readFileSync('./db/users.json'))
          // const user = userArray.find(user => user.id.toString() === userId)
          const user = userArray.find(user => user.id === userId)
          let string = ''

          // console.log('user: ')
          // console.log(user)

          if (user) {
            console.log('userId: ' + userId)
            // 模板替换文字
            string = homeHtml.replace(/登录/g, '退出,重新登录')
              .replace(/{{loginStatus}}/g, '已登录')
              .replace(/{{user.name}}/g, user.name)
              .replace(/logIn/g, "logOut")
          } else {
            // string ='' // 必须有string 至少是空字符串,写到页面里
            string = homeHtml.replace(/{{loginStatus}}/g, '未登录')
              .replace(/{{user.name}}/g, '请登录,游客')
          }
          response.write(string)
        } else {
          string = homeHtml.replace(/{{loginStatus}}/g, '未登录')
            .replace(/{{user.name}}/g, '请登录,游客')
          response.write(string)
        }
        response.end()
}
  • 此时在Application里更改Cookies对应的session_id时,由于是一个随机数,只要设置用户最大的访问数(1000)

9. Cookie/Session总结 §

服务器可以给浏览器下发Cookie

  • 通过Response HeaderMDN 语法

浏览器上的Cookie可以被篡改

  • 用开发者工具就能改
  • 弱智后端下发的CookieJS也能篡改

服务器必须下发不可篡改的Cookie

  • Cookie可包含加密后的信息(还得解密)
  • Cookie也可只包含一个id(随机数)

session[id]可以在后端拿到对应的信息

  • 把用户信息放在服务器的 session
  • 再给session设置一个随机id
  • 把随机id发给浏览器
  • 后端下次读取到id时,通过 x[id] 获取用户信息
  • 用户无法通过改 id 来试错 (因为 id 很长,而且随机)
  • session.json是文件,不能用内存,因为断电内存就清空
  • 这个x又被叫做session.json(会话)
  • 这个 id 无法被篡改,但可以被复制
  • session可以被注销,随时间隔一段时间删除
  • 重新登录生成新的随机id,具有时效性

10. 目标6 注销 §

实现注销的思路

  • session idsession里删掉
  • cookie从浏览器删掉(可选)

实现

  • 前端
    • 前端制作注销按钮
    • 前端监听注销按钮点击事件
    • 前端发送delete session请求
  • 后端
    • 后端接受delete session请求
    • 后端获取当前session id,并将其删除
    • 后端下发一个过期的同名Cookie(因为没有删除)

还有一个bug

  • 用户密码被泄露时

11. 目标7:防止密码泄露 §

不要存明文

  • 拿到明文之后,使用第三方库 bcrypt.js 加密,得到密文
  • 将密文保存到user.json
  • 用户登录时,加密后对比密文是否一致
  • 一致则说明用户知道密码,可以登录
  • 安全起见,不能用本地 JS 直接删除 Cookie,应该使用带 HttpOnlyCookie
    • 然后 JS 发请求让服务器删 Cookie
  • 安全起见,除了删除浏览器端的 Cookie,还需要把对应的 Session 数据删掉

不要用MD5来加密

完成一个最简单的动态网站

12. 大总结 §

如何获取post请求体(后端)

  • 全栈(会)

如何使用Cookie(后端)

  • 永远不要用JS操作Cookie,要http-only

什么是Session(后端)

  • 就是后端的一个文件,保存会话数据,一般存用户信息

注册、登录、注销的实现(后端)


13. 技巧 §

CookieSession的区别

  • Cookie信息存在于浏览器,session.json存在于服务器
  • Cookie长度有限制4kbsession.json一般没有
  • Session是借助Cookie实现的,id存放在cookie
  • 服务器一般会将 Session id 放到 Cookie 中,发放给浏览器

CookieLocalStorage的区别

  • Cookie会被自动放在每一次的请求里,LocalStorage不会
  • Cookie有长度限制 4 KB左右,LocalStorage的长度一般为5M~10M,不同浏览器有不同的实现

14. Cookie其他细节 §

相同二级域名 不是同源

  • 举例:a.qq.comqq.com的二级域名都是qq.com
  • a.qq.com发起请求时,必须附上qq.com里的cookie
  • a.qq.com无效

功能

  • Set-Cookie MDN
  • 后端可以设置Cookie的过期时间
  • 后端可以设置Cookie的域名(不能乱设置)
  • 后端可以设置Cookie是否能被JS读取(禁止JS)
  • 后端可以设置Cookie的路径(较少用)

其他Cookie相关知识回顾

了解HTTP协议的无状态性

  • HTTP 协议的 无状态性,指的是客户端的 每次 HTTP 请求都是独立的
  • 即(多个请求)各个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态

如何突破HTTP无状态的限制

  • 使用在 Web 开发的一种身份认证方式,叫做 Cookie

什么是Cookie

  • Cookie存储在用户浏览器中的一段不超过 4 KB 的字符串
  • Cookie 由以下几个字段组成:
    • 一个名称(Name
    • 一个值(Value),注意Value也是一个字段(属性),也有对应的值
    • 以及其它几个用于控制 Cookie 有效期 Expires安全性Secure使用范围HttpOnly 等字段
    • 每个字段都可以有对应的值,用等号=连接字段和值
    • 不同字段间用英文分号;连接
  • 不同域名下的 Cookie 各自独立
    • 每当客户端发起请求时,会自动把当前域名下所有 未过期的 Cookie 一同发送到服务器

在浏览器Application面板中查看Cookie

  • 点击Application -> Storage -> Cookies就可看到当前域名下的Cookie

cookie-like

在浏览器Network面板中的 请求头 中查看再次发送请求时自动携带的Cookie

  • 可以对比查看两者是否一一对应

cookiesInRequestHeadersNetwork

  • 再点击 Cookie 选项卡(另一次刷新后的截图)

Cookie_Network

在浏览器Network面板中的 响应头 中查看服务器发来要求浏览器携带新的 Set-Cookie字段

Set-CookieInResponseHeadersNetwork


Cookie的几大特性

  • 自动发送:发起请求时,浏览器自动携带上一次存储的Cookie
  • 域名独立:确保身份认证的安全性
  • 过期时限
  • 4KB 限制

Cookie在身份认证中的作用

  • 客户端 第一次 请求服务器的时候,服务器通过 响应头 的形式,向客户端发送一个 身份认证的 Cookie
  • 客户端会 自动Cookie 保存在浏览器中,不同域名对应存储不同的Cookie
  • 之后,当客户端浏览器每次请求服务器的时候,浏览器会自动将 未过期身份认证相关的 Cookie,通过 请求头 的形式发送给服务器,服务器通过比对服务器端存储的Session即可验明客户端的身份

Cookie

Cookie不具有安全性

  • 由于 Cookie 是存储在浏览器中的,而且浏览器也提供了 读写 Cookie 的 API
  • 因此 Cookie 很容易被伪造,不具有安全性
  • 不建议服务器将重要的隐私数据(比如用户名和密码等),通过 Cookie 的形式发送给浏览器

提高身份认证的安全性

  • 使用 Cookie(客户端) + Session(服务器) 认证机制

什么是Session

  • Session是浏览器-服务器之间的一种身份验证机制

介绍Session的工作原理

session



参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名