前端与移动开发----Node.js----前后端身份验证,JWT认证,Promise,async函数和await修饰符

Node.js

回顾

数据库 (database) 是用来组织、存储和管理数据的仓库

需要安装PHPStudy里面内置了mysql环境软件一起安装到电脑中

MYSQL: 数据库管理软件

Navicat: 是图形化界面, 可以操作MySQL数据库的

node+mysql模块: 可以用代码来操作MYSQL数据库

SQL语句, 本质上都用这个来操作数据库

新增:

INSERT INTO 表名 SET ?

查询:

SELECT 字段 FROM 表名 WHERE 条件 ORDER BY 排序字段 LIMIT 开始下标, 个数
  • 带 and or 等查询 > < 等条件
  • 模糊查询 like % 占0到多位, _ 占1位
  • 统计
    • count() - 数量
    • sum() 求和
    • max() - 求最大
    • avg() - 求平均值

更新

UPDATE 表名 SET ? 

项目思路:

前端JS+axios - nodejs+mySQL+express, 数据库保存数据

22. 前后端身份验证

22.0 项目开发模式

  • 服务器端渲染

    服务器端渲染完数据形成的html页面, 直接把html页面响应给客户端浏览器直接显示

    前端无需Ajax再请求数据了

    • 优点

      • 前端耗时少

      • 更容易被爬虫(SEO)

    • 缺点

      • 占用服务器端资源
      • 前端复杂度高的开发效率过低
  • 前后端分离项目

    服务器端只负责提供API数据接口, 前端用Ajax调用使用即可

    • 优点
      • 开发体验好, 前端专注UI效果开发
      • 后端只专注数据业务逻辑开发.减轻服务器端压力
    • 缺点
      • 不利于seo, 因为是动态渲染的页面, 不利于爬虫

具体使用哪种, 要根据实际业务场景来定的, 而且同一个项目中, 不同网页可以使用以上的2种方式

  • 首页是静态宣传网页, 无任何交互, 可以使用后端渲染的方式
  • 其他页面用户交互比较多, 可以选择前后端分离方式的开发模式

22.1 身份认证

身份认证, 又称"身份验证", “鉴权”, 是指通过一定的手段, 完成对用户身份的确认

目的: 确认当前所声称为某种身份的用户, 确实是所声称的用户, 例如你去取快递, 你需要先声明你手机号后4位

不同开发模式下的身份认证

服务器端渲染的项目: 推荐使用session认证机制

前后端分离的项目: 推荐使用JWT认证机制

HTTP 无状态性

服务器接收到每次请求时, 和响应后, 服务器是不会保留过上一次HTTP请求的任何信息和状态的(在协议中)

在这里插入图片描述

突破HTTP无状态性

每次请求过来以后, 服务器都对浏览器设置一个cookie保存一个值, 这样下次这个浏览器在发送请求过来, 会携带cookie过来, 服务器就知道你是谁了

在这里插入图片描述

22.2 Cookie

Cookie跟localStorage和sessionStorage都是在浏览器上存储的一种方式由名称值格式存储
特点:

  1. 自动发送未过期的cookie给后台
  2. 不同域名下cookie是各自独立
  3. 可以设置过期时限
  4. 4KB大小限制
  5. 跨域后端无法设置前端的cookie, 所以一般我们都用jwt(token)

查看Cookie

检查 -> Application -> Cookie

22.3 Cookie认证机制

客户端第一次请求-> 服务器, 服务器在响应头给客户端设置cookie保存在浏览器中

以后客户端每次请求服务器都会把未过期的cookie携带给后台, 表明自己的身份 (在请求头里自动携带)

在这里插入图片描述

去运行下提前准备好的代码, 无需编写, 明白流程, 因为一会儿我们有插件使用

前台登录.html

$("#btn").on("click", () => {
    
    
    var username = $("#userInp").val();
    var password = $("#passInp").val();
    axios({
    
    
        url: "http://localhost:3000/api/login",
        params: {
    
    
            username,
            password
        }
    }).then(res => {
    
    
        if (res.data.status == 1) {
    
    
            window.location.href = "/22.3_首页.html";
        }
    })
})

22.3_cookie验证.js

const express = require("express");
const app = express();
app.listen(3000, () => {
    
    
    console.log("服务器启动了");
})

app.use(express.static(__dirname + "/../1_前端/"));

let arr = [
    {
    
    
        username: "nana",
        password: 111111,
        money: 10000
    },
    {
    
    
        username: "xiaomi",
        password: 222222,
        money: 500
    }
]
// 重要: 跨域后端无法设置前端cookie - 直接在浏览器打开后端的静态资源

// 服务器端, 利用响应头给前端本次请求的位置设置cookie的方式
app.get("/api/login", (req, res) => {
    
    
    // 假装登录成功了, 设置一个cookie给前端, 比如用户名字
    // set-cookie是固定的, 后面的格式固定名称=值 - 去浏览器请求这个接口-观察前端浏览器上被设置了cookie
    let {
    
     username, password } = req.query;
    // 查找用户和密码是否有同时匹配的
    var have = arr.some(obj => obj.username == username && obj.password == password);
    if (have) {
    
    
        res.setHeader("set-cookie", `user=${
      
      username}; path=/`); // path代表此cookie在前端生效的区域(发送请求时url只要是在/下, 就都能使用cookie)
        res.send({
    
    
            status: 1,
            msg: "登录成功"
        })
    } else {
    
    
        res.send({
    
    
            status: 0,
            msg: "账号或密码错误"
        })
    }
})

app.get("/api/getMoney", (req, res) => {
    
    
    // 提取cookie, 找到user保存的用户名
    var argStr = [];
    if (req.headers.cookie) {
    
    
        argStr = req.headers.cookie.split("; ");
    }
    var username = ""; // 保存找到的用户名
    argStr.forEach(str => {
    
    
        let smallArr = str.split("=");
        if (smallArr[0] == "user") {
    
    
            username = smallArr[1];
        }
    })
    // 再用用户名, 查询这个用户的金额
    var money = 0;
    arr.forEach(obj => {
    
    
        if (obj.username == username) {
    
    
            money = obj.money;
        }
    })
    res.send({
    
    
        status: 1,
        msg: "获取成功",
        data: money
    })
})


前台_首页.html

用户查询此用户余额

axios({
    
    
    url: "http://localhost:3000/api/getMoney"
}).then(res => {
    
    
    $("#myDiv").html(res.data.data);
})

cookie不具有安全性

后端只判断有cookie, 而且有那个名字user(类似于会员卡), 就直接返回了数据

那前端浏览器伪造(直接在浏览器cookie写个名字user, 值随便写), 就可以随意拿到数据了, 很危险

后端需要验证值是否也合法

在这里插入图片描述

22.4 Session认证机制

底层需要cookie配合, cookie端保存一个代号, 代号对应此用户在后台保存的身份信息名字

在这里插入图片描述

express-session

前台不变

这是一个第三方模块, 用于快速让express服务器集成session功能, 而封装的模块, npm下载后可以直接使用

npm install express-session
const express = require("express");
const app = express();
app.listen(3000, () => {
    
    
    console.log("服务器启动了");
})
app.use(express.static(__dirname + "/../1_前端/"));

let arr = [
    {
    
    
        username: "nana",
        password: 111111,
        money: 10000
    },
    {
    
    
        username: "xiaomi",
        password: 222222,
        money: 500
    }
]

const session = require("express-session");
app.use(session({
    
    
    secret: 'keyboard cat', // 用于加密目标数据用的字符串
    resave: false, // resave是指每次请求都重新设置session cookie
    saveUninitialized: true // saveUninitialized是指无论有没有session cookie,每次请求都设置个session cookie ,默认给个标示为 connect.sid
}))

app.get("/api/login", (req, res) => {
    
    
    let {
    
    username, password} = req.query;
    // 查找用户和密码是否有同时匹配的
    var have = arr.some(obj => obj.username == username && obj.password == password);
    if (have) {
    
    
        req.session.usern = username;
        res.send({
    
    
            status: 1,
            msg: "登录成功"
        })
    } else {
    
    
        res.send({
    
    
            status: 0,
            msg: "账号或密码错误"
        })
    }
})

// 重要 - 多个前端登录验证, 需要用不同的浏览器, 因为一个浏览器的同一个域名下的cookie会被覆盖
app.get("/api/getMoney", (req, res) => {
    
    
    // 提取cookie的值, 从express-session内存中提取对应的值
    var username = req.session.usern;
    // 查找余额
    var money = 0;
    arr.forEach(obj => {
    
    
        if (obj.username == username) {
    
    
            money = obj.money;
        }
    })

    res.send({
    
    
        status: 1,
        msg: "获取成功",
        data: money
    })
})

注意每个浏览器使用连接的cookie相同, 如果想要测试不同登录人的cookie需要不同浏览器

22.5 删除Session

注册, 只会删除这个用户的

app.put("/logout", (req, res) => {
    
    
    req.session.destroy();
    res.send({
    
    
        status: 1,
        msg: "退出成功"
    })
})

23. JWT认证

jwt全称叫 JSON Web Token - 是一种代替session的验权技术

Session如果在跨域的项目中, 后端需要开启Https + SameSite 才能给跨域的前端设置cookie使用 (否则浏览器无法被跨域的服务器设置cookie)

而且Session只保存在当前的服务器上, 如果是分布式服务器集群, Session还需要同步给各个服务器, 太麻烦

JWT就是颁发给前端一串字符串保存在客户端(加密后), 后端解密JWT, 提取用户身份信息来认真

在这里插入图片描述

jwt组成

在这里插入图片描述

23.0 jwt使用

前端登录以后, 后端生成签名token, 前端得到后保存到浏览器本地

以后请求接口都携带token, 后端解析token提取你的用户信息, 然后返回你的对应的信息

后端下载2个包

jsonwebtoken 用于生成jwt字符串

express-jwt 用于将jwt字符串解析还原成JS对象

npm i jsonwebtoken express-jwt
// 1. 引入模块
const jsonwebtoken = require('jsonwebtoken');
const expressJwt = require("express-jwt");

// 2. 设置秘钥
const secretKey = "itheima No.1 ^.^";

// 4. 解析 jwt
// 用哪个秘钥解密, jwt的算法, unless哪些接口不需要权限验证
app.use(expressJwt({
    
    secret: secretKey, algorithms: ["HS256"]}).unless({
    
    path: [/^\/api\//]}))

app.get("/api/login", (req, res) => {
    
    
    let {
    
    username, password} = req.query;
    var have = arr.some(obj => obj.username == username && obj.password == password);

    if (have) {
    
    
        // 3. 登录成功 - 生成token
        res.send({
    
    
            status: 1,
            msg: "登录成功",
            // 注意这个前缀是expressJwt规定的, 必须带这个固定的头加空格, 里面要拆分使用(跟真正jwt数据毫无关系)
            token: "Bearer " + jsonwebtoken.sign({
    
    username: username}, secretKey, {
    
    expiresIn: "30s"})
        })
        // h是小时, s是秒
    } else {
    
    
        res.send({
    
    
            status: 0,
            msg: "账号或密码错误"
        })
    }
})

app.get("/my/getMoney", (req, res) => {
    
    
    // 5. 使用req.user.username
    var username = req.user.username;
    var money = 0;
    arr.forEach(obj => {
    
    
        if (obj.username == username) {
    
    
            money = obj.money;
        }
    })
    res.send({
    
    
        status: 1,
        msg: "获取成功",
        data: money
    })
})

23.1 jwt过期 - 兜底错误验证

// 6. 兜底错误处理 - 自定义错误处理中间件
app.use((err, req, res, next) => {
    
    
    // token 解析失败导致的错误
    if (err.name == "UnauthorizedError") {
    
    
        return res.send({
    
    status: 401, msg: "无效的token"});
    }

    res.send({
    
    status: 500, msg: err.message});
})

24. Promise

  • 如何获取异步的结果?必须通过回调函数获取,不能通过返回值获取
  • 如果要保证多个异步结果获取的顺序那么需要进行回调的嵌套,如果嵌套层次比较多,那么就会出现一个问题:回调地狱
  • 为了解决回调地狱的问题就诞生了一种新的技术:Promise

Promise:Promise对象用于表示一个异步操作的最终完成 (或失败)及其结果值。它是异步任务的管家

JS异步有哪些

  • 定时任务
  • Ajax
  • 事件处理函数

24.0 Promise基本用法

  • 异步任务需要在Promise回调函数中进行处理 当Promise对象创建时, 马上执行回调函数里异步任务
  • 正常的异步结果交给resolve函数
  • 异常的错误信息交给reject函数
  • 获取正常或异常的数据分别使用then或者catch
  • resolve调用 -> then里回调函数执行
  • reject调用 -> catch里回调函数执行
// 1. 新建Promise对象
let p = new Promise((resolve, reject) => {
    
    
    // 2. 异步任务在这里
    fs.readFile(__dirname + "/24/0.json", "UTF-8", (err, data) => {
    
    
        if (err) reject(err)
        else resolve(data);
    })
})

// 3. 监测p任务管家的结果
p.then(res => {
    
    
    console.log(res);
}).catch(err => {
    
    
    console.log(err);
})

24.1 then里return值

then函数本身会返回一个Promise

这个Promise的值就是then回调函数里return出来的结果

如果return的又是一个Promise则会覆盖then函数默认返回的Promise

  • 如果then回调函数的返回值返回Promise实例对象,那么下一个then会得到该异步任务的结果
  • 如果then回调函数的返回值返回普通数据,那么下一个then会直接获取该数据
// 1. then()方法默认返回一个Promise, 而then里的return值作为这个Promise成功的结果触发这个Promise的then函数执行并传值

// let p = new Promise((res, rej) => {
    
    
//     setTimeout(() => {
    
    
//         res("异步返回结果");
//     }, 2000);
// })

// var newP = p.then(res => {
    
    
//     console.log(res);
//     return 123;
// })

// newP.then(res => {
    
    
//     console.log("第二个then执行");
//     console.log(res);
// })

// 2. then()方法里return一个新的Promise使用
let p = new Promise((res, rej) => {
    
    
    setTimeout(() => {
    
    
        res("异步返回结果");
    }, 2000);
})

let p2 = new Promise((resolve, reject) => {
    
    
    setTimeout(() => {
    
    
        resolve("5秒后返回结果");
    }, 5000);
})

var newP = p.then(res => {
    
    
    console.log("第一个then执行");
    console.log(res);

    return p2;
})
newP.then(res => {
    
    
    console.log("第二个then执行");
    console.log(res);
})
// 代码执行顺序, 创建p变量的Promise管家, 然后马上执行里面的异步任务(开始计时)
// 主线程继续往下, 创建p2变量的Promise管家, 然后也马上执行Promise里面的异步任务(开始计时)
// 然后给p绑定then函数, 返回一个newP, 也马上绑定then函数 - 主线程执行完毕
// 待时间到了, 触发p的Promise里的resolve调用p的then函数体执行, 
// 然后return p2这个Promise对象-等待时间到达后, 触发p2的then函数执行 - 如果要是值直接就传给下一个处理函数

24.2 解决回调地狱问题

// 需求读取3个文件异步任务 - 要从0和1和2的顺序执行

const fs = require("fs");
// 之前回调地狱写法
// fs.readFile(__dirname + "/24/0.json", "UTF-8", (err, data) => {
    
    
//     if (err) console.log(err)
//     else {
    
    
//         console.log(data);
//         fs.readFile(__dirname + "/24/1.json", "UTF-8", (err, data) => {
    
    
//             if (err) console.log(err)
//             else {
    
    
//                 console.log(data);
//                 fs.readFile(__dirname + "/24/2.json", "UTF-8", (err, data) => {
    
    
//                     if (err) console.log(err)
//                     else {
    
    
//                         console.log(data);
//                     }
//                 })
//             }
//         })
//     }
// })

// Promise处理回调地狱
let p0 = new Promise((resolve, reject) => {
    
    
    fs.readFile(__dirname + "/24/0.json", "UTF-8", (err, data) => {
    
    
        if (err) reject(err)
        else resolve(data);
    })
});
let p1 = new Promise((resolve, reject) => {
    
    
    fs.readFile(__dirname + "/24/1.json", "UTF-8", (err, data) => {
    
    
        if (err) reject(err)
        else resolve(data);
    })
});
let p2 = new Promise((resolve, reject) => {
    
    
    fs.readFile(__dirname + "/24/2.json", "UTF-8", (err, data) => {
    
    
        if (err) reject(err)
        else resolve(data);
    })
});
// 重点在这里
p0.then(res => {
    
    
    console.log(res);
    return p1;
}).then(res => {
    
    
    console.log(res);
    return p2;
}).then(res => {
    
    
    console.log(res);
})

24.3 重构数据库操作

function theMySQL(sql, params) {
    
    
    return new Promise((resolve, reject) => {
    
    
        const mysql = require('mysql');
        const conn = mysql.createConnection({
    
    
            host: 'localhost',
            user: 'root',
            password: 'root',
            database: '121_db'
        });
        conn.connect();

        // 完成增删改查
        conn.query(sql, params, (err, data) => {
    
    
            if (err) reject(err);
            else resolve(data);
        });

        conn.end();
    })
}

// 查询
// theMySQL("SELECT * FROM student", []).then(res => {
    
    
//     console.log(res);
// })


// 新增
// let userObj = {
    
    
//     name: "小米",
//     age: 20,
//     sex: "男",
//     tel: 13900203123
// }
// let sql = `INSERT INTO student SET ?`;
// theMySQL(sql, userObj).then(res => {
    
    
//     console.log(res);
// })

24.4 Promise的对象方法

// 案例: 想要执行多个异步任务, 都成功了, 同时按照顺序返回, 活着呢有一个失败了直接返回失败的提示, 建议使用Promise.all()
// 需求读取3个文件异步任务 - 要从0和1和2的顺序执行

const fs = require("fs");
let p0 = new Promise((resolve, reject) => {
    
    
    fs.readFile(__dirname + "/24/0.json", "UTF-8", (err, data) => {
    
    
        if (err) reject(err)
        else resolve(JSON.parse(data));
    })
});
let p1 = new Promise((resolve, reject) => {
    
    
    fs.readFile(__dirname + "/24/1.json", "UTF-8", (err, data) => {
    
    
        if (err) reject(err)
        else resolve(JSON.parse(data));
    })
});
let p2 = new Promise((resolve, reject) => {
    
    
    fs.readFile(__dirname + "/24/2.json", "UTF-8", (err, data) => {
    
    
        if (err) reject(err)
        else resolve(JSON.parse(data));
    })
});

// 重点在这里
// Promise.all() 可以把多个Promise对象合并成一个大的
// Promise.all([p0, p1, p2]).then(res => {
    
    
//     console.log(res);
// }).catch(err => {
    
    
//     console.log(err.message);
// })

// 案例: 哪个Promise异步执行完了(无论成功或者失败) - 直接触发 - 不关心其他人
Promise.race([p0, p1, p2]).then(res => {
    
    
    console.log(res);
}).catch(err => {
    
    
    console.log(err.message);
})

25. async函数和await修饰符

ES2017(ES8) 新出的2个关键字, async+await以更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise

async函数: 使用async关键字声明的函数。 其中允许使用await关键字。
await关键字: await 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用。无需刻意地链式调用promise

  • 如果等待的不是 Promise 对象,则返回该值本身
  • 如果等待的是Promise对象, 则等待Promise对象的resolve调用把值返回到原地

25.0 async+await干掉then函数

const fs = require("fs");
// Promise处理回调地狱
let p0 = new Promise((resolve, reject) => {
    
    
    fs.readFile(__dirname + "/24/0.json", "UTF-8", (err, data) => {
    
    
        if (err) reject(err)
        else resolve(data);
    })
});
let p1 = new Promise((resolve, reject) => {
    
    
    fs.readFile(__dirname + "/24/1.json", "UTF-8", (err, data) => {
    
    
        if (err) reject(err)
        else resolve(data);
    })
});
let p2 = new Promise((resolve, reject) => {
    
    
    fs.readFile(__dirname + "/24/2.json", "UTF-8", (err, data) => {
    
    
        if (err) reject(err)
        else resolve(data);
    })
});
// 重点在这里
console.log("开始执行");
async function getData(){
    
     // 这是一个异步块的代码, 不会阻塞主线程执行
    const res0 = await p0; // await会暂停代码, 在这里等待p0这个Promise的结果回来才会继续往下执行
    const res1 = await p1;
    const res2 = await p2;
    console.log(res0);
    console.log(res1);
    console.log(res2);
}
getData();
console.log("主线程结束");

25.1 优化上面

const fs = require("fs");
// 代码有点重复, 封装一个异步函数, 专门用于读取文件的Promise管家
const readFileFn = (pathUrl) => {
    
    
    return new Promise((resolve, reject) => {
    
    
        // 这里的异步任务马上执行
        fs.readFile(__dirname + pathUrl, "UTF-8", (err, data) => {
    
    
            if (err) reject(err)
            else resolve(data);
        })
    });
}

async function getData(){
    
    
    const res0 = await readFileFn("/24/0.json"); 
    const res1 = await readFileFn("/24/1.json");
    const res2 = await readFileFn("/24/2.json");
    console.log(res0);
    console.log(res1);
    console.log(res2);
}
getData();

Promise和await和async都是ES6/ES8标准制定的, Node和前端都是JS语法

25.2 try和catch的使用

// try, catch是 JS当中2个关键字
// 专门来捕获同步代码的错误

const fs = require("fs");
// 异步代码捕获错误: -> 回调函数里的形参 (JS源码会给你返回错误对象)
// fs.readFile(__dirname + "/hah.json", "UTF-8", (err, data) => {
    
    
//     if (err) console.log("自定义错误提示消息: 文件可能不在");
//     else console.log(data);
// })

// 同步代码(等待结果) - 捕获错误
// (1): 用try{} 把可能报错的代码 包起来 (如果有错误, 会直接进入到catch不会继续执行下面的代码)
try {
    
    
    const data = fs.readFileSync(__dirname + "/haha.json", "UTF-8");
    console.log(data);
} catch (err) {
    
     // (2): 等待上面{}里代码如果报错了, 就会进入到这个catch块, 传递给err值
    if (err) console.log("自定义错误提示消息: 文件可能不在");
}

25.3 try和catch解决await错误

function readFileFn(filePath) {
    
    
    const fs = require("fs");
    return new Promise((resolve, reject) => {
    
    
        fs.readFile(filePath, "UTF-8", (err, data) => {
    
    
            if (err) reject(err)
            else resolve(data);
        })
    })
}
// 同步代码的错误, 如果程序员不捕获处理, js就会直接抛出到控制台里去
// await 等待Promise的resolve的结果返回在原地
// await 等待的Promise报错了, 执行了reject - 同步流程报错- 阻止代码继续向下执行

async function myFn() {
    
    
    try {
    
     // 如果同步代码有try+catch, js就会把错误对象抛到catch里去, 让程序处理这个异常
        const res0 = await readFileFn(__dirname + "/24/1.json");
        const res1 = await readFileFn(__dirname + "/24/1.json");
        const res2 = await readFileFn(__dirname + "/24/2.json");
        console.log(res0);
        console.log(res1);
        console.log(res2);
    } catch (err) {
    
    
        if (err) console.log("可能文件的路径不对");
    }
}
myFn();
// await 错误 - 后面还会学await-to.js(功能)  (优化掉try+catch以及大括号)

如有不足,请多指教,
未完待续,持续更新!
大家一起进步!

猜你喜欢

转载自blog.csdn.net/qq_40440961/article/details/112296257