目录 §


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

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

1.1 判断依据

是否请求了数据库

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

直接用JSON文件当做数据库

2. 数据库dbuser.json §

结构:一个数组

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

读取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

  • 打出的是字符串,需要数组

    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
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
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}

添加路由 /home.html server.js

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

改home.html

1
2
3
4
5
6
7
<body>
  <!-- <p> 你好, {{user.name}}</p> -->

  <p> 你好, {{loginStatus}}</p>

  <p> <a href="sign_in.html">登录</a></p>
</body>
  • {{loginStatus}}替换为true/false
  • 有问题:登陆成功跳转到home.html发的请求和直接跳转发的一样

如何识别用户是否登录?

  • Cookie

后端服务器给用户浏览器发Cookie,判断用户名和密码匹配数据库中的数据成功时发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 文档

     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()
    }
    })
    
  • 读文件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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 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");
  • 防止JS改cookie

     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
6
7
<body>
  <!-- <p> 你好, {{user.name}}</p> -->

  <p> 你好, {{user.name}} {{loginStatus}}</p>

  <p> <a href="sign_in.html">登录</a></p>
</body>
  • 找到匹配的 user :userArray.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 知道登录的是谁

设置路由,错误处理,user.id判断,模板替换文字,显示用户名 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
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取到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 截获加密信息后,模拟登陆,加密后的内容可无限期使用
  • 解决办法:JWT(JSON 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(会话)

     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
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)

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

  • 通过Response Header,MDN语法

浏览器上的Cookie可以被篡改

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

服务器下发不可篡改的Cookie

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

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

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

10. 目标6 注销 §

思路

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

实现

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

还有一个bug

  • 用户密码被泄露时

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

不要存明文

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

不要用MD5

一个最简单的动态网站

12. 大总结 §

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

  • 全栈(会)

如何使用Cookie(后端)

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

什么是Session(后端)

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

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


13. 技巧 §

CookieSession的区别

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

CookieLocalStorage的区别

  • Cookie会被放在每一次的请求里,LocalStorage不会
  • Cookie有长度限制,LocalStorage的长度一般为5M~10M

相同二级域名

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

功能

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