目录 § ⇧
- 1. 动态服务器 V.S. 静态服务器
- 2. 数据库
db
user.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
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
,让用户填写name
和password
- 监听
submit
事件
- 发送
post
请求,数据位于 请求体
- 后端
- 接受
post
请求
- 获取请求体中的
name
和password
- 存储数据
前端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 Headers
的Content-Type
为text/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
- 设置返回的数据
如何拿到用户上传的name
和password
后端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
,让用户填写name
和password
- 监听
submit
事件
- 发送
post
请求,数据位于请求体
- 后端
- 接受
post
请求
- 获取请求体中的
name
和password
- 读取数据库,看是否有匹配的
name
和password
- 如果匹配,后端应标记用户已登录,如何标记
前端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
发的请求和直接跳转发的一样
如何识别用户是否登录?
5. 目标3:Cookie
标记用户已登录 § ⇧
Cookie
定义
后端服务器给用户浏览器发Cookie
,判断用户名和密码匹配数据库中的数据成功时发Cookie
6. Set-Cookie
响应头 § ⇧
后端server.js
匹配到已有的用户数据,即登录成功后,在响应头中设置
1
|
response.setHeader('Set-Cookie', 'logined=1')
|
- 提示登录成功后查看
Network
中 Response 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.html
的Network -> 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 = xxx
;document.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
|
<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
知道登录的是谁
后端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 = xxx
;document.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.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
浏览器上的Cookie
可以被篡改
- 用开发者工具就能改
- 弱智后端下发的
Cookie
用JS
也能篡改
服务器必须下发不可篡改的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 id
从session
里删掉
- 将
cookie
从浏览器删掉(可选)
实现
- 前端
- 前端制作注销按钮
- 前端监听注销按钮点击事件
- 前端发送
delete session
请求
- 后端
- 后端接受
delete session
请求
- 后端获取当前
session id
,并将其删除
- 后端下发一个过期的同名
Cookie
(因为没有删除)
还有一个bug
11. 目标7:防止密码泄露 § ⇧
不要存明文
- 拿到明文之后,使用第三方库
bcrypt.js
加密,得到密文
- 将密文保存到
user.json
- 用户登录时,加密后对比密文是否一致
- 一致则说明用户知道密码,可以登录
- 安全起见,不能用本地
JS
直接删除 Cookie
,应该使用带 HttpOnly
的 Cookie
- 安全起见,除了删除浏览器端的
Cookie
,还需要把对应的 Session
数据删掉
不要用MD5
来加密
完成一个最简单的动态网站
12. 大总结 § ⇧
如何获取post
请求体(后端)
如何使用Cookie
(后端)
- 永远不要用
JS
操作Cookie
,要http-only
什么是Session
(后端)
注册、登录、注销的实现(后端)
13. 技巧 § ⇧
Cookie
和Session
的区别
Cookie
信息存在于浏览器,session.json
存在于服务器
Cookie
长度有限制4kb
,session.json
一般没有
Session
是借助Cookie
实现的,id
存放在cookie
中
- 服务器一般会将
Session id
放到 Cookie
中,发放给浏览器
Cookie
和LocalStorage
的区别
Cookie
会被自动放在每一次的请求里,LocalStorage
不会
Cookie
有长度限制 4 KB
左右,LocalStorage
的长度一般为5M~10M
,不同浏览器有不同的实现
14. Cookie
其他细节 § ⇧
相同二级域名 不是同源
- 举例:
a.qq.com
和qq.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
在浏览器Network
面板中的 请求头 中查看再次发送请求时自动携带的Cookie
- 再点击 Cookie 选项卡(另一次刷新后的截图)
在浏览器Network
面板中的 响应头 中查看服务器发来要求浏览器携带新的 Set-Cookie
字段
Cookie
的几大特性 ⇧
- 自动发送:发起请求时,浏览器自动携带上一次存储的
Cookie
- 域名独立:确保身份认证的安全性
- 过期时限
- 4KB 限制
Cookie
在身份认证中的作用 ⇧
- 客户端 第一次 请求服务器的时候,服务器通过 响应头 的形式,向客户端发送一个 身份认证的
Cookie
- 客户端会 自动 将
Cookie
保存在浏览器中,不同域名对应存储不同的Cookie
- 之后,当客户端浏览器每次请求服务器的时候,浏览器会自动将 未过期 的 身份认证相关的
Cookie
,通过 请求头 的形式发送给服务器,服务器通过比对服务器端存储的Session
即可验明客户端的身份
Cookie
不具有安全性 ⇧
- 由于
Cookie
是存储在浏览器中的,而且浏览器也提供了 读写 Cookie
的 API
- 因此 Cookie 很容易被伪造,不具有安全性
- 不建议服务器将重要的隐私数据(比如用户名和密码等),通过
Cookie
的形式发送给浏览器
提高身份认证的安全性 ⇧
- 使用
Cookie
(客户端) + Session
(服务器) 认证机制
什么是Session
⇧
Session
是浏览器-服务器之间的一种身份验证机制
介绍Session
的工作原理 ⇧
参考文章
相关文章
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名
- 河
掘
思
知
简