2021-10-20

6-1 开始

登录

有业界通用标准,只需学习不用设计

  • 核心:登录校验 & 登录信息存储
    • 登录校验:如果想要进入到博客管理中心,必须要登录,不然不让你做
    • 登录信息存储:存储 username realname 等,管理时比较方便,并且便于操作
  • 为何只讲登录,不讲注册?
    • 注册和创建博客一样,没有难度
    • 二维码注册等一些新的方式层出不穷,暂不讲

目录

  • cookie 和 session
  • session 写入 redis
  • 开发登录功能,和前端联调(用到 nginx 反向代理)

6-2 cookie-介绍

cookie

  • 什么是 cookie
  • JavaScript 操作 cookie,浏览器中查看 cookie
  • server 端操作 cookie,实现登录验证

最大4kb
image.png
image.png

image.png
三种方式

  • 查看浏览器-network-headers
  • 查看浏览器-application-cookies
  • 查看浏览器-console 然后输入 document.cookie 就可以查看

第三种方式还可以自行累加
image.png

6-3 cookie用于登录验证

server 端 nodejs 操作 cookie

  • 查看 cookie
  • 修改 cookie
  • 实现登录验证

查看 cookie

在 app.js 中写入解析 cookie 的代码
该段代码就是将浏览器中的 cookie 的这种形式:k1=v1;k2=v2;k3=v3 解析为
{
k1:v1,
k2:v2,
k3:v3
}

  1. const serverHandle = (req, res) => {
  2. ...
  3. // 解析 cookie
  4. req.cookie = {}
  5. const cookieStr = req.headers.cookie || '' // k1=v1;k2=v2;k3=v3
  6. cookieStr.split(';').forEach(item => {
  7. if (!item) {
  8. return
  9. }
  10. const arr = item.split('=')
  11. const key = arr[0]
  12. const value = arr[1]
  13. req.cookie[key] = value
  14. })
  15. console.log('req.cookie', req.cookie);
  16. ...
  17. }

可以做一个统一:如果 cookie 中有 username 则认为已经登录,如果没有的话就认为没有登录
修改所有的 loginCheck 为 login

实现登录验证

在 router/user.js 中增加一条判断逻辑来判断是否能成功进行登录验证
因为是用 promise 来封装的,所以返回 Promise.resolve() 来做

  1. // 登录验证的测试
  2. if (method === 'GET' & req.path === '/api/user/login-test') {
  3. if (req.cookie.username) {
  4. return Promise.resolve(
  5. new SuccessModel()
  6. )
  7. }
  8. return Promise.resolve(
  9. new ErrorModel('尚未登录')
  10. )
  11. }

在 console 中手动添加 document.cookie = ‘username=zhangsan’image.png
image.png
成功!

然后临时将登录的逻辑改为 GET 并且实现一下修改 cookie
修改这三处
image.png
因此当输入 api/user/login?username=zhangsan&password=123 时
就将账号和密码通过nodejs后端的方式设置到了浏览器中,并且 path 是 / ,所以所有页面都生效
这样的话再输入 /api/user/login-test 时,后端就可以拿到 req.cookie.username,因此就可以验证登录成功了,并且全都是后端所做的

2021-10-21

6-4 cookie做限制(1)

设置浏览器限制

现在遇到一个问题,就是当 username=lisi 登录后,如果在浏览器修改 username 为数据库存在的其他数据,则会成为别人,现在处理下这个问题

解决:
在 set cookie 时,设置 httpOnly
image.png
image.png
然后再尝试修改,发现 document.cookie 看不见了,如果改的话也能改成功,但是server端的会覆盖掉前端改的

6-5 cookie做限制(2)

设置过期时间

目前的登录是永久有效的,可以给它设置过期时间
在 router/user.js 中
首先设置获取 cookie 过期时间的函数

  1. // 获取 cookie 的过期时间
  2. const getCookieExpires = () => {
  3. // 获取当前时间
  4. const d = new Date()
  5. // 设置时间为当前时间加24小时
  6. d.setTime(d.getTime() + (24 * 60 * 60 * 1000))
  7. // 最后返回这个时间
  8. console.log('d.toGMTString() is', d.toGMTString());
  9. return d.toGMTString()
  10. }

然后在 setHeader 的地方设置 expires

  1. // 操作 cookie , 设置 cookie 生效的 path 为根路由,这样所有的网页都会生效
  2. res.setHeader('Set-Cookie', `username=${data.username}; path=/; httpOnly; expires=${getCookieExpires()}`)

试一下:
http://localhost:8000/api/user/login?username=lisi&password=123
成功!
image.png
image.png

总结

  • 知道 cookie 的定义和特点
  • 前后端如何查看修改 cookie
  • 如何使用 cookie 实现登录验证

6-6、6-7 session介绍 、session演示

session

  • 上一节的问题:会暴露 username ,很危险
    • 各种私人信息,并且很多信息放到cookie中不够放 只有4kb
  • 如何解决:cookie 中存 userid,server 端对应 username
  • 解决方案:session,即 server 端存储用户信息

不同的浏览器都去 server 中的 session 去取 userid 对应的内容

image.png代码演示

首先在 app.js 中解析 session

  • 当没有 userId 时,即用户初次登录时,这时候需要创建cookie,并且创建 userId ,最后将SESSION_DATA[userId] 初始化
  • 当有 userId 时,即用户已经登录成功,但是有可能是用户更换账号,这种情况就将SESSION_DATA[userId] 初始化
  • 最后的最后将 SESSION_DATA[userId] 设置为 req.session

    1. // 解析 session
    2. let needSetCookie = false
    3. let userId = req.cookie.userid
    4. // 如果有 userId
    5. if (userId) {
    6. // 就看下 session 中有没有对应的值
    7. // 如果没有的话,就给它初始化一下
    8. if (!SESSION_DATA[userId]) {
    9. SESSION_DATA[userId] = {}
    10. }
    11. // 如果没有 userId, 就需要设置 cookie
    12. // 就把 userId 初始化一下
    13. } else {
    14. needSetCookie = true
    15. userId = `${Date.now()}_${Math.random()}`
    16. SESSION_DATA[userId] = {}
    17. }
    18. // 最后赋值 session 为 SESSION_DATA[userId]
    19. req.session = SESSION_DATA[userId]

    然后修改访问 blog 和 user 的逻辑
    如果初次登录,即需要设置 cookie 时由客户端设置 cookie
    image.png
    image.png
    最后修改 路由逻辑
    将所有数据库中的数据全部放到 session 中去
    image.png
    总结

  • 使用 session 后机制相比 cookie 的改变了

  • 在尚未登录且第一次访问的情况下,就会设置一个新的cookie
  • 若以未登录的情况下再次访问,就不会设置新的cookie,就以第一次访问设置的cookie来进行访问
  • 在登录的时候,若登录成功且已经访问过页面的话他就还是拿旧的 cookie,然后去走login的路由,将query中的 username password 设置到 session 中去

也就是说 前端只存着 sessionid,一个浏览器一个 sessionid,拿着 sessionid 去请求,通过session去做任何请求

总结

  • 知道 session 解决的问题
  • 如何实现 session

6-8 从 session 到 redis

当前设置 session 的问题

  • 目前 session 直接是 js 变量,放在 nodejs 进程内存中
  • 第一个问题:内存进程有限,访问量过大,内存暴增怎么办?
    • nodejs 在16G内存电脑中只会有1.6g的内存分配,因为操作系统会限制一个进程的最大可用内存
    • 同样的,session也只会占据nodejs一块限制的内存
  • 第二个问题:正式线上运行的是多进程,进程之间的内存无法共享
    • 多进程就会有多 session
    • 无法共享就会导致当登录相同的账户时访问不同的进程就会登录失败

image.png
image.png

解决方案:redis

  • web server 最常用的缓存数据库,数据存放在内存中
  • 相比 mysql ,访问速度快(内存和硬盘不是一个数量级别的)
  • 成本更高,可存储的数据量更小(内存的硬伤)

image.png
◆将 web server 和 redis 拆分为两个单独的服务
◆双方都是独立的,都是可扩展的(例如都扩展成集群)
◆(包括mysql,也是一一个单独的服务,也可扩展)

因此将 session 从 nodejs(web server)中抽离出来,这样就不会有上述的问题了

为何 session 适合用 redis?

  • session 访问频繁,对性能要求极高(因此不适合将登录数据放到 mysql 中)
  • session 可以不考虑断电丢失数据的问题(内存的硬伤)——当然可以做到数据恢复等不丢失的手段
  • session 数据量不会很大(相比 mysql 中的数据)

为何网站数据不适合用 redis?

  • 操作频率不是太高(相比 session 操作)
  • 断电不能丢失,必须保留
  • 数据量太大,内存成本太高

6-9 redis介绍

安装 redis

image.png

redis 基本使用

进入 redis 访问下面两个命令:
redis-server 和 redis-cli
image.png
redis 使用比较简单,使用key value的形式
最基本的使用方式
set key value 然后 get key 即得 value 值
image.png
keys * 就可以获得当前所有得到的key

总结

  • 为何要用 redis?不用 redis 会有什么问题?
  • session 和 redis 分别适合什么场景?

2021-10-22

6-10 nodejs 连接 redis 的 demo

用 redis 存储 session

  • nodejs 连接 redis 的 demo
  • 封装成工具函数,可供 API 使用

redis-server 启动起来,可以看到端口号是6379 记住
然后创建一个 redis-test 的目录,然后 npm init -y 初始化
然后新建 index.js 然后安装 redis
npm i redis —save

然后在 index.js 中写入

  1. const redis = require('redis')
  2. // 创建客户端
  3. const redisClient = redis.createClient(6379, '127.0.0.1')
  4. // 监控 error
  5. redisClient.on('error', err => {
  6. console.error(err)
  7. })
  8. // 测试
  9. // 这里的 redis.print 用来打印这个 set 是否正确
  10. redisClient.set('myname', 'demonlb', redis.print)
  11. redisClient.get('myname', (err, val) => {
  12. if (err) {
  13. console.error(err)
  14. return
  15. }
  16. console.log('val', val)
  17. // 退出 redis
  18. redisClient.quit()
  19. })

然后尝试运行一下 node index.js
发现这个结果
image.png
这里的 “Reply: OK” 就是在 redis.print 出打印出来的
在 redis 中存储的也是成功的
image.png

6-11 nodejs 连接 redis - 封装工具函数

回到 blog-1 中
首先安装 redis
然后来到 /src/conf/db.js 新建 redis 的配置

  1. ...
  2. // 配置 redis 的值
  3. let REDIS_CONF
  4. ...
  5. if (env === 'dev') {
  6. ...
  7. // redis
  8. REDIS_CONF = {
  9. port: 6379,
  10. host: '127.0.0.1',
  11. }
  12. }
  13. if (env === 'production') {
  14. ...
  15. // redis
  16. REDIS_CONF = {
  17. port: 6379,
  18. host: '127.0.0.1',
  19. }
  20. }
  21. // 最后导出
  22. module.exports = {
  23. ...
  24. REDIS_CONF
  25. }

然后配置连接
来到 /src/db/redis.js 中
封装工具函数

  1. const redis = require('redis')
  2. const { REDIS_CONF } = require('../conf/db')
  3. // 创建客户端
  4. const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)
  5. // 监控 error
  6. redisClient.on('error', err => {
  7. console.error(err)
  8. })
  9. function set(key, val) {
  10. // 因为 redis 只能存储字符串,所以要将对象改为字符串的形式
  11. if (typeof val === 'object') {
  12. val = JSON.stringify(val)
  13. }
  14. redisClient.set(key, val, redis.print)
  15. }
  16. function get(key) {
  17. const promise = new Promise((resolve, reject) => {
  18. edisClient.get(key, (err, val) => {
  19. if (err) {
  20. reject(err)
  21. return
  22. }
  23. // 如果瞎传了一个key,则 val 就是 null
  24. if (val == null) {
  25. resolve(null)
  26. }
  27. try {
  28. // 将字符串解析为对象来返回
  29. resolve(
  30. JSON.parse(val)
  31. )
  32. // 如果不是的就正常返回
  33. } catch (ex) {
  34. resolve(val)
  35. }
  36. })
  37. })
  38. return promise
  39. }
  40. module.exports = {
  41. set,
  42. get
  43. }

OK。下节课使用这个 set 和 get 方法

2021-10-23

6-12 session 存入 redis

首先把 app.js 中解析 session 的代码中注释一下,因为不再需要在 node 中存 session 的值了
然后将 session 的值存到 redis 中

  1. // 解析 session 使用 redis
  2. let needSetCookie = false
  3. let userId = req.cookie.userid
  4. if (!userId) {
  5. needSetCookie = true
  6. userId = `${Date.now()}_${Math.random()}`
  7. // 如果没有 userId 的话,初始化 redis 中的 session
  8. set(userId, {})
  9. }
  10. // 然后获取 userId 对应的值
  11. req.sessionId = userId
  12. get(req.sessionId).then(sessionData => {
  13. if (sessionData == null) {
  14. // 初始化 redis 中的 session
  15. set(req.sessionId, {})
  16. // 设置 session
  17. req.session = {}
  18. }
  19. // 设置 session
  20. req.session = sessionData
  21. console.log('req.session', req.session)
  22. return getPostData(req)
  23. })

然后修改路由中的获取逻辑,因为目前在路由中只将数据存到了本地
如下图,引入 set 和同步到 redis 中
image.png
梳理下代码逻辑,首先访问
http://localhost:8000/api/user/login-test 时,因为未登录所以,返回 ErrorModel 并且显示 login failed
在 app.js 中走过代码会发现:
会设置 needSetCookie 为 false
然后因为没有 userId ,所以 设置一个新的 userId 为时间戳+随机数的形式,然后通过 set 方法,在 redis 中将这个 userId 设置为 空对象【联动 redis】
然后将这个 userId 存储在 req 中,取名为 req.sessionId,然后通过 get 方法去取 req 中的 sessionId 得到 sessionData,如果 sessionData 为空的话(即现在)就再次赋值一遍上面的操作(在 redis 中将这个 userId 即req.sessionId 设置为 空对象),然后将 req.session 也置为空对象,最后 设置 req.session 为 sessionData(即也置空)【联动 cookie】
然后因为最开始 设置了 needSetCookie 为 false,所以会触发下面这个逻辑,将cookie中的userid设定,这样就可以绑定浏览器和cookie的关系了

  1. if (needSetCookie) {
  2. res.setHeader('Set-Cookie', `userid=${userId}; path=/; httpOnly; expires=${getCookieExpires()}`)
  3. }

然后访问
http://localhost:8000/api/user/login?username=zhangsan&password=123 时,因为此时有了 userId 和 req.sessionId
但是是初次登录,所以 sessionData 依旧是 null,但不需要修改cookie,来到 handleUserRouter 的逻辑中,通过 set 方法,将绑定的 userId 设置其正确的 password 和 username,这样就将 session 中的内容转到了 redis 中,使用其他路由时就没有限制了,而在 cookie 中 只存 user.id 了

6-13 完成 server 端登录的代码

回顾下 login-test 的作用,也就是说当 req.session.username 时就认为此时已经登录了
也就是说做到了:1.登录验证 2.获取登录信息
后面将不再使用这个接口

现在考虑一下在 blog.js 中:新建、更新和删除博客时需要登录验证,不再使用假数据
定义一个统一的登录验证函数

// 统一的登录验证函数
const loginCheck = (req) => {
    // 如果没登录就返回 ErrorModel
    if (!req.session.username) {
        return Promise.resolve(
            new ErrorModel('尚未登录')
        )
    }
}

然后在新建、更新和删除博客时进行登录验证
类似的,在 新建、更新和删除博客 的代码逻辑中添加如下代码

const loginCheckResult = loginCheck(req)
if (loginCheckResult) {
  // 未登录
  return loginCheck
}

最后将 登录 设置为 post 请求,将登录信息又从 req.query 中获取改为从 req.body 中获取,并且将 /api/user/login-test 置空

后续进行前后端联调

6-14 联调-介绍html页面

和前端联调

  • 登录功能依赖 cookie,必须使用浏览器来联调
  • cookie 跨域不共享,前端和 server 端必须同域
  • 需要用到 nginx 做代理,让前后端同域

开发前端页面

创建一个 html-test 文件夹,放入所有所需要调试的基本页面
里面就是 jQuery 和 ajax 的请求,还有 HTML 代码
image.png
为了让这些 HTML 跑起来就安装 http-server
npm install http-server -g
然后启动
输入 http-server -p 8001
然后访问 localhost:8001
发现 list 请求 404 了
image.png
因为这个请求是请求的 8001 端口,所以 404 了
后面会使用 nginx 来处理这个问题使用8080端口来让 api 不 404

2021-10-26

6-15 nginx配置

nginx 介绍

  • 高性能的 web 服务器,开源免费
    • 使用率很高
  • 一般用于做静态服务,负载均衡(本课用不到)
    • 静态服务:CDN(js文件,css文件,json文件,图片等)
    • 负载均衡:集群中,各个服务器均摊流量
  • 反向代理(本课用到)

image.png
通过 nginx ,对于浏览器来说具体访问什么端口是未知的,只需要访问 index.html,所有内容经过 8000 和8080 端口然后 nginx 代理返回给客户端

nginx配置地址
/usr/local/etc/nginx/nginx.conf

nginx 命令

测试配置文件格式是否正确 nginx -t
启动 nginx: 直接输入 nginx
重启 nginx:nginx -s reload
停止 nginx -s stop

修改 nginx

进入该 nginx.conf 中
这里的 worker_processes 表示启动服务时使用几核,然后在开发环境中一般不考虑,
image.png
然后修改代理:location
把原先这里的注释了
image.png
然后如下设置

# nodejs project configuration
location / {
proxy_pass http://localhost:8001;
}

location /api/ {
proxy_pass http://localhost:8000;
# set header to send host
proxy_set_header Host $host;
}

然后使用 nginx -t 来测试一下
成功

然后试一下 http://localhost:8080/ 正常访问
image.png

总结

  • nginx 反向代理的配置
  • 前后端如何同域联调

2021-10-27

6-16 联调演示与总结

https://coding.imooc.com/learn/questiondetail/kxNyDXbde7jPGgb7.html
isadmin 用处:

isadmin 是识别后台管理的。

第一,如果是后台管理的请求(即 req.query.isadmin 是 true),则用户名必须是自己的,即只能搜索自己的博客

第二,如果不是后台管理的请求(即 req.query.isadmin 是 false),则用户名可以是别人的,即可以搜索别人的博客。

修改 isadmin
在获取时限制只让获取自己的根据query的isadmin,这个是由后端加入的
image.png
image.png