目录 § ⇧
- 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数组
|
|
- 注意
JSON格式的双引号,其它引号是错误的写法,也不能省略
读取
users数据
- 先
fs.readFileSync('./db/user.json').toString() - 然后
JSON.parse('string'),反序列化,得到数组
创建
test.js
|
|
打开终端 运行命令
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,让用户填写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
|
|
- 不写
method,默认method: 'GET', 默认会将data直接接在请求头后面GET /register?{%22name%22:%22xxx%22,%22password%22:%22yyy%22} HTTP/1.1 - 用
POST,在请求体里传数据Query String Parameters
前端
register.js
|
|
- 数据传输格式
Content-Type,为application/x-www-form-urlencoded; charset=UTF-8,不为JSON - 设置
Request Headers的Content-Type为text/json; charset=UTF-8
前端
register.js
|
|
- 上传一个
JSON - 服务器获取一个
JSON - 即最简单的注册
- 前端的活完了
获取
POST数据,这是后端的活
- 添加动态服务器的路由:
/register - 判断请求方法是否是
POST - 设置请求头的
Content-Type - 设置返回的数据
如何拿到用户上传的
name和password
GET可以通过query.callback查询参数query替换全局属性(函数)名;用数据 替换 函数实参(var parsedUrl = url.parse(request.url, true); var query = parsedUrl.query)- 之前的代码
https://github.com/xmasuhai/cross-origin-1/blob/master/qq-com/server.js
后端
server.js
|
|
用
POST拿数据
- 获取当前数据库中的数据
- 声明一个数组存储数据 数据有可能是分段(数据块chunk)上传的, 而数据的大小未知,必须一点一点上传数据,获取时也是一点一点获取
- 发送请求时 监听请求 传来一个数据过来时 放入数组
- 监听响应结束,获取最后一个用户(id最大
userArray.length - 1)的数据,更新数据,将数据 转化为字符串覆盖写入数据库
后端
server.js
|
|
前端
register.js登录成功跳转
|
|
4. 目标2:登录功能 § ⇧
实现用户登录功能,跳转首页登录
- 首页
home.html,已登录用户可以看到自己的用户名 - 登录页
sign_in.html,用来提交用户名和密码 - 输入的用户名和密码如果匹配,就自动跳转首页
sign_in.html思路
- 前端
- 写一个
form,让用户填写name和password - 监听
submit事件 - 发送
post请求,数据位于请求体
- 写一个
- 后端
- 接受
post请求 - 获取请求体中的
name和password - 读取数据库,看是否有匹配的
name和password - 如果匹配,后端应标记用户已登录,如何标记
- 接受
前端
home.html
|
|
HTML不能直接读取数据库,用占位符{{user.name}}
后端
server.js添加home.html的路由
|
|
目标2太大,目标尽量小,实现标记用户是否已登录
server.js
|
|
- 是否匹配
name和password - MDN Array.prototype.find
sign.js
|
|
- 控制台测试成功
- 失败时改为返回一个
JSON,包含自定义的错误码{"errorCode": 4001}
server.js中添加路由/home.html
|
|
- 分支中必须以
response.end()未结尾结束,否则一直处于padding中 - 监听事件中的
response.end()是异步的,必须在每个分支条件后立即执行,而不可写在if分支外的最后,即直接执行response.end(),而不执行分支中的语句
修改
home.html
|
|
{{loginStatus}}替换为true/false- 有问题:登录成功跳转到
home.html发的请求和直接跳转发的一样
如何识别用户是否登录?
- 使用
Cookie
5. 目标3:Cookie 标记用户已登录 § ⇧
Cookie 定义
Cookie是服务器下发给浏览器的一段字符串- 浏览器必须保存这个
Cookie(除非用户删除) - 之后发起 非跨域 请求(任何请求方式)时,浏览器自动附上
Cookie - 有
Cookie就可以标记登录了,没Cookie就无法识别身份 - Document.cookie MDN
- HTTP cookies MDN
- 浏览器模型 Cookie 网道 / WangDoc.com
后端服务器给用户浏览器发
Cookie,判断用户名和密码匹配数据库中的数据成功时发Cookie
6. Set-Cookie响应头 § ⇧
后端
server.js匹配到已有的用户数据,即登录成功后,在响应头中设置
|
|
- 提示登录成功后查看
Network中Response Headers中的Set-Cookie设置成功 - 在
Application中的Cookies已存储 const cookie = request.headers['cookie']- 注意
cookie小写 - 无痕窗口登录的
cookie就是undefined - 抄MDN文档语法
-
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,从而辨别是否登录
|
|
发起相同二级域名请求(任何请求方式)时,浏览器自动附上
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改cookie1 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辨别登录的是谁
|
|
- 找到匹配的
user:userArray.find((user) => user.name === obj.name && user.password - 把
logined=1改成user_id=${user.id}
后端
server.js
|
|
- 前端
home.html通过Cookie里存的use.id知道登录的是谁
后端
server.js设置路由,错误处理,user.id判断,模板替换文字,显示用户名
|
|
实现目标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.json1 2 3 4 5 6 7 8 9 10 11 12 13 14 15const 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
|
|
- 此时在
Application里更改Cookies对应的session_id时,由于是一个随机数,只要设置用户最大的访问数(1000)
9. Cookie/Session总结 § ⇧
服务器可以给浏览器下发
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.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- 然后
JS发请求让服务器删Cookie
- 然后
- 安全起见,除了删除浏览器端的
Cookie,还需要把对应的Session数据删掉
不要用
MD5来加密
- 已被破解,不是一种加密算法,是
hash算法 - 你会做WEB上的用户登录功能吗?
完成一个最简单的动态网站
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的工作原理 ⇧
参考文章
相关文章
- 无