Node 3.0


# 前后端的身份认证

# Web 开发模式

  • 基于服务端渲染的传统 Web 开发模式
  • 基于前后端分离的新型 Web 开发模式

一些页面可能会采用首屏服务端渲染 + 其他页面前后端分离的开发模式

# 服务端渲染的 Web 开发模式

服务端渲染的概念:服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用 Ajax 这样的技术额外请求页面的数据。代码示例如下:

app.get("/index.html", (req, res) => {
  // 要渲染的数据
  const user = { name: "zs", age: 20 };
  // 服务器通过字符串的拼接,动态生成 HTML 内容
  const html = `<h1>姓名${user.name},年龄${user.age}</h1>`;
  // 把生成好的页面内容响应给客户端,因此,客户端拿到的是带有真实数据的 HTML 页面
  res.send(html);
});
  • 优点:

    • 前端耗时少。因为服务器端负责动态生成 HTML 内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
    • 有利于 SEO。因为服务器端响应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO。
  • 缺点:

    • 占用服务器端资源。即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
    • 不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发。

# 前后端分离的 Web 开发模式

前后端分离的概念:前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。简而言之,前后端分离的 Web 开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式。

  • 优点:

    • 开发体验好。前端专注于 UI 页面的开发,后端专注于 api 的开发,且前端有更多的选择性。
    • 用户体验好。Ajax 技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
    • 减轻了服务器端的渲染压力。因为页面最终是在每个用户的浏览器中生成的。
  • 缺点:

    • 不利于 SEO。因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方案:利用 Vue、React 等前端框架的 SSR (server side render)技术能够很好的解决 SEO 问题!)

# 身份认证

# 什么是身份认证

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

  • 日常生活中的身份认证随处可见,例如:高铁的验票乘车,手机的密码或指纹解锁,支付宝或微信的支付密码等。
  • 在 Web 开发中,也涉及到用户身份的认证,例如:各大网站的 手机验证码登录、邮箱密码登录、二维码登录 等。

# 为什么需要身份认证

身份认证的目的,是为了 确认当前所声称为某种身份的用户,确实是所声称的用户 。例如,你去找快递员取快递,你要怎么证明这份快递是你的。
在互联网项目开发中,如何对用户的身份进行认证,是一个值得深入探讨的问题。例如,如何才能保证网站不会错误的将 “马云的存款数额” 显示到 “马化腾的账户” 上。

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

对于 服务端渲染 和 前后端分 离这两种开发模式来说,分别有着不同的身份认证方案:

  • 服务端渲染 推荐使用 Session 认证机制
  • 前后端分离 推荐使用 JWT 认证机制

# Session 认证机制

# HTTP 协议的无状态性

了解 HTTP 协议的无状态性是进一步学习 Session 认证机制的必要前提。
HTTP 协议的无状态性,指的是客户端的 每次 HTTP 请求都是独立的 ,连续多个请求之间没有直接的关系, 服务器不会主动保留每次 HTTP 请求的状态

# 如何突破 HTTP 无状态的限制

对于超市来说,为了方便收银员在进行结算时给 VIP 用户打折,超市可以为每个 VIP 用户发放会员卡。
注意:现实生活中的 会员卡身份认证方式 ,在 Web 开发中的专业术语叫做 Cookie 。

Cookie 是 存储在用户浏览器中的一段不超过 4 KB 的字符串 。它由一个 名称 (Name)、一个 值 (Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性 组成。
不同域名下的 Cookie 各自独立 ,每当客户端发起请求时,会 自动 把 当前域名下 所有 未过期的 Cookie 一同发送到服务器。

Cookie 的几大特性:

  • 自动发送
  • 域名独立 (百度的不能去访问腾讯的)
  • 过期时限
  • 4KB 限制

客户端第一次请求服务器的时候,服务器 通过响应头的形式 ,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会 自动 将身份认证相关的 Cookie, 通过请求头的形式 发送给服务器,服务器即可验明客户端的身份。

由于 Cookie 是存储在浏览器中的,而且 浏览器也提供了读写 Cookie 的 API ,因此 Cookie 很容易被伪造 ,不具有安全性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器。
注意 :千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等。

# 提高身份认证的安全性

为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以 在收银机上进行刷卡认证 。只有收银机确认存在的会员卡,才能被正常使用。
这种 “会员卡 + 刷卡认证” 的设计理念,就是 Session 认证机制的精髓。

# Session 的工作原理

# 在 Express 中使用 Session 认证

安装 express-session 中间件 npm install express-session

  • 案例

    const express = require("express");
    // 创建 express 的服务器实例
    const app = express();
    
    // TODO_01:请配置 Session 中间件
    const session = require("express-session");
    app.use(
      session({
        secret: "giao", // secret 属性的值可以为任意字符串
        resave: false, // 固定写法
        saveUninitialized: true, // 固定写法
      })
    );
    
    // 托管静态页面
    app.use(express.static("./pages"));
    // 解析 POST 提交过来的表单数据
    app.use(express.urlencoded({ extended: false }));
    
    // 登录的 API 接口
    app.post("/api/login", (req, res) => {
      // 判断用户提交的登录信息是否正确
      if (req.body.username !== "admin" || req.body.password !== "000000") {
        return res.send({ status: 1, msg: "登录失败" });
      }
    
      // TODO_02:请将登录成功后的用户信息,保存到 Session 中
      // 注意:只有成功配置了 express-session 这个中间件之后,才能够通过 req 点出来 session 这个属性
      req.session.user = req.body; // 将用户的信息,存储到 Session 中
      req.session.islogin = true; // 将用户的登录状态,存储到 Session 中
    
      res.send({ status: 0, msg: "登录成功" });
    });
    
    // 获取用户姓名的接口
    app.get("/api/username", (req, res) => {
      // TODO_03:请从 Session 中获取用户的名称,响应给客户端
      if (!req.session.islogin) {
        return res.send({ status: 1, msg: "fail" });
      }
      res.send({
        status: 0,
        msg: "success",
        username: req.session.user.username,
      });
    });
    
    // 退出登录的接口
    app.post("/api/logout", (req, res) => {
      // TODO_04:清空 Session 信息
      // 调用 req.session.destroy() 函数,即可清空服务器保存的该用户所对应的 session 信息,而不会清空其他用户的 Session 信息。
      req.session.destroy();
      res.send({
        status: 0,
        msg: "退出登录成功",
      });
    });
    
    // 调用 app.listen 方法,指定端口号并启动web服务器
    app.listen(80, function () {
      console.log("Express server running at http://127.0.0.1:80");
    });

# JWT 认证机制

# 了解 Session 认证的局限性

Session 认证机制 需要配合 Cookie 才能实现 。由于 Cookie 默认不支持跨域访问,所以,当涉及到 前端跨域请求后端接口 的时候, 需要做很多额外的配置 ,才能实现跨域 Session 认证。

注意:

  • 当前端请求后端接口 不存在跨域问题 的时候, 推荐使用 Session 身份认证机制。

  • 当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制。

  • JWT(英文全称:JSON Web Token)是目前 最流行的跨域认证解决方案。

# JWT 的工作原理

总结:用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。

# JWT 的组成部分

JWT 通常由三部分组成,分别是 Header (头部)、 Payload (有效荷载)、 Signature (签名)。
三者之间使用英文的 “.” 分隔,格式如下:

Header.Payload.Signature;

下面是 JWT 字符串的示例:

eyJhbGc101JIUzI1N1IsInR5cCI6IkpXVCJ9.eyJpZCI6MSw1dXN1cm5hbWU101JhZG1 pb1I sInBhc3N3b3JkIjo1Iiwibm1ja25 hbWUi01Lms6X1t7T1t7QiLCJ1bWFpbCI6Im5pYmF1YUBpdGNhc3QuY241LCJ1c2VyX3BpYyI6IiIs Im1hdCI6MTU30DAzNjY4Miw1ZXhwI JoxNTc4MDcyNj8yfQ.Mwq7GqCxJPK-EA8LN tMG0411KdZ33S9KBL3XeuBxuI

# JWT 的三个部分各自代表的含义

JWT 的三个组成部分,从前到后分别是 Header、Payload、Signature。
其中:

  • Payload 部分 才是真正的用户信息 ,它是用户信息经过加密之后生成的字符串。
  • Header 和 Signature 是 安全性相关 的部分,只是为了保证 Token 的安全性。

# JWT 的使用方式

客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中。
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是 把 JWT 放在 HTTP 请求头的 Authorization 字段中 ,格式如下:

Authorization: Bearer <token>
// 举例:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjU5ODg2ODUwLCJleHAiOjE2NTk4ODY4ODB9.M_0N3CoyVPDe6DmbZmj5JLzD4MRjxtL4_bGozh6YLIA

# 在 Express 中使用 JWT 认证

安装两个 JWT 相关的包 npm install jsonwebtoken express-jwt
jsonwebtoken 用于生成 JWT 字符串
express-jwt 用于将 JWT 字符串解析还原成 JSON 对象

  • 定义 secret 密钥
    为了 保证 JWT 字符串的安全性 ,防止 JWT 字符串在网络传输过程中被别人破解,我们需要专门定义一个用于 加密 和 解密 的 secret 密钥:

    • 当生成 JWT 字符串的时候,需要使用 secret 密钥对用户的信息 进行加密 ,最终得到加密好的 JWT 字符串
    • 当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 密钥 进行解密
    // secret 密钥的本质:就是一个字符串
    const secretKey = "giao No1 ^_^";
  • 案例

    const express = require("express");
    // 创建 express 的服务器实例
    const app = express();
    
    // TODO_01:安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt
    const jwt = require("jsonwebtoken");
    const expressJWT = require("express-jwt");
    
    // 允许跨域资源共享
    const cors = require("cors");
    app.use(cors());
    
    // 解析 post 表单数据的中间件
    const bodyParser = require("body-parser");
    app.use(bodyParser.urlencoded({ extended: false }));
    
    // TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey
    const secretKey = "giao No1 ^_^";
    
    // TODO_04:注册将 JWT 字符串解析还原成 JSON 对象的中间件
    // expressJWT({ secret: secretKey }) 就是用来解析 Token 的中间件
    // .unless({ path: [/^\/api\//] }) 用来指定哪些接口不需要访问权限
    // 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上
    app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }));
    
    // 登录接口
    app.post("/api/login", function (req, res) {
      // 将 req.body 请求体中的数据,转存为 userinfo 常量
      const userinfo = req.body;
      // 登录失败
      if (userinfo.username !== "admin" || userinfo.password !== "000000") {
        return res.send({
          status: 400,
          message: "登录失败!",
        });
      }
      // 登录成功
      // TODO_03:在登录成功之后,调用 jwt.sign() 加密方法生成 JWT 字符串。并通过 token 属性发送给客户端
      // 参数1:用户信息对象
      // 参数2:加密秘钥
      // 参数3:配置对象,可以配置当前 token 的有效期
      // 记住:千万不要把密码加密到 token 字符中
      const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, {
        expiresIn: "30s",
      });
      res.send({
        status: 200,
        message: "登录成功!",
        token: tokenStr, // 要发送给客户端的 token 字符串
      });
    });
    
    // 这是一个有权限的 API 接口
    app.get("/admin/getinfo", function (req, res) {
      // TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
      console.log(req.user);
      res.send({
        status: 200,
        message: "获取用户信息成功!",
        data: req.user, // 要发送给客户端的用户信息
      });
    });
    
    // TODO_06:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
    app.use((err, req, res, next) => {
      // 这次错误是由 token 解析失败导致的
      if (err.name === "UnauthorizedError") {
        return res.send({
          status: 401,
          message: "无效的token",
        });
      }
      res.send({
        status: 500,
        message: "未知的错误",
      });
    });
    
    // 调用 app.listen 方法,指定端口号并启动web服务器
    app.listen(8888, function () {
      console.log("Express server running at http://127.0.0.1:8888");
    });

# 实战

大事件后台 API 项目,API 接口文档请参考:https://www.showdoc.com.cn/escook/3707158761215217
大事件项目 api_server:http://escook.cn:8088/#/

# 实战代码 api_server

  • package.json

    {
      "name": "api_server",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "@escook/express-joi": "^1.1.1",
        "@hapi/joi": "^17.1.0",
        "bcryptjs": "^2.4.3",
        "cors": "^2.8.5",
        "express": "^4.17.1",
        "express-jwt": "^5.3.3",
        "jsonwebtoken": "^8.5.1",
        "mysql": "^2.18.1"
      }
    }
  • config.js

    // 这是一个全局的配置文件
    
    module.exports = {
      // 加密和解密的 token
      jwtSecretKey: "giao",
      // token 的有效期
      expiresIn: "10h",
    };
  • app.js

    const express = require("express");
    const app = express();
    
    const joi = require("joi");
    
    // 配置 cors 跨域
    const cors = require("cors");
    app.use(cors());
    
    // 配置解析表单数据的中间件
    // 这个中间件只能解析 application/x-www-form-urlencoded 格式的表单数据
    app.use(express.urlencoded({ extended: false }));
    
    // 优化 res.send() 代码
    // 一定要在路由之前,封装 res.cc 函数,如果放在了路由之后,那么在路由里面就访问不了 res.cc 函数了
    app.use((req, res, next) => {
      // status 默认值为 1,表示失败的情况
      // err 的值,可能是一个错误对象,也可能是一个错误的描述字符串
      res.cc = function (err, status = 1) {
        res.send({ status, message: err instanceof Error ? err.message : err });
      };
      next();
    });
    
    // 一定要在路由之前配置解析 Token 的中间件
    const expressJWT = require("express-jwt");
    const config = require("./config");
    // 使用 .unless({ path: [/^\/api\//] }) 指定哪些接口不需要进行 Token 的身份认证
    app.use(
      expressJWT({ secret: config.jwtSecretKey }).unless({ path: [/^\/api/] })
    );
    
    // 导入并使用用户路由模块
    const userRouter = require("./router/user");
    // 访问userRouter模块的接口时,必须加上 /api 前缀
    app.use("/api", userRouter);
    
    // 导入并使用用户信息路由模块
    const userinfoRouter = require("./router/userinfo");
    // 注意:以 /my 开头的接口,都是有权限的接口,需要进行 Token 身份认证
    app.use("/my", userinfoRouter);
    
    // 导入并使用文章分类的路由模块
    const artCateRouter = require("./router/artcate");
    app.use("/my/article", artCateRouter);
    
    // 定义错误级别的中间件
    app.use((err, req, res, next) => {
      // 这是 /api/reguser 这种类似接口导致的Joi参数错误
      // Joi验证失败导致的错误
      if (err instanceof joi.ValidationError) {
        return res.cc(err);
      }
    
      // 身份认证失败后的错误
      // 如果请求 /giao/test 这个接口会报 身份认证token 失败的错误,原因是因为 /giao/test 不是 unless 方法中不需要验证的接口,他需要验证身份,但是他的请求头当中没添加 Authorization 这个字段,如果这个字段的值不是服务端返回的正确的token也会报错
      if (err.name === "UnauthorizedError") {
        return res.cc("身份认证token失败");
      }
      // 未知的错误
      res.cc(err);
      // 在 Express 中不允许连续两次调用 res.send()
    });
    
    app.listen(3007, () => {
      console.log("server running at:http://127.0.0.1:3007");
    });
  • db 文件夹

    • index.js

      const mysql = require("mysql");
      
      const db = mysql.createPool({
        host: "127.0.0.1", // 数据库的 IP
        port: "3307", // 端口号
        user: "root", // 登录数据库的账号
        password: "admin123", // 登录数据库的密码
        database: "my_db_01", // 指定要操作哪个数据库
      });
      module.exports = db;
  • schema 文件夹

    • user.js

      // 为表单中携带的每个数据项,定义验证规则
      const joi = require("joi");
      
      /**
       * string() 值必须是字符串
       * alphanum() 值只能是包含 a-zA-Z0-9 的字符串
       * min(length) 最小长度
       * max(length) 最大长度
       * required() 值是必填项,不能为 undefined
       * pattern(正则表达式) 值必须符合正则表达式的规则
       */
      
      // 定义用户名的验证规则
      const username = joi.string().alphanum().min(1).max(10).required();
      // 定义密码的验证规则
      // \S 表示非空字符,非空字符6-12位之间
      const password = joi
        .string()
        .pattern(/^[\S]{6,12}$/)
        .required();
      
      // 定义 id, nickname, emial 的验证规则
      const id = joi.number().integer().min(1).required();
      const nickname = joi.string().required();
      const email = joi.string().email().required();
      
      // dataUri() 指的是如下格式的字符串数据:
      // 举例:data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
      const avatar = joi.string().dataUri().required();
      
      // 定义注册和登录表单的验证规则对象
      // 具体的规则去 npm 中搜索 @escook/express-joi 的用法
      exports.reg_login_schema = {
        // 表示需要对 req.body 中的数据进行验证
        body: {
          username,
          password,
        },
      };
      
      // 规则验证对象 - 更新用户基本信息
      exports.update_userinfo_schema = {
        body: {
          id,
          nickname,
          email,
        },
      };
      
      // 验证规则对象 - 重置密码
      exports.update_password_schema = {
        body: {
          // 使用 password 这个规则,验证 req.body.oldPwd 的值
          oldPwd: password,
          // 使用 joi.not(joi.ref('oldPwd')).concat(password) 规则,验证 req.body.newPwd 的值
          // 解读:
          // 1. joi.ref('oldPwd') 表示 newPwd 的值必须和 oldPwd 的值保持一致
          // 2. joi.not(joi.ref('oldPwd')) 表示 newPwd 的值不能等于 oldPwd 的值
          // 3. .concat() 用于合并 joi.not(joi.ref('oldPwd')) 和 password 这两条验证规则
          newPwd: joi.not(joi.ref("oldPwd")).concat(password),
        },
      };
      
      // 验证规则对象 - 更新头像
      exports.update_avatar_schema = {
        body: {
          avatar,
        },
      };
    • artcate.js

      // 为表单中携带的每个数据项,定义验证规则
      const joi = require("joi");
      
      // 定义 分类名称 和 分类别名 的校验规则
      const name = joi.string().required();
      const alias = joi.string().alphanum().required();
      
      // 定义 分类Id 的校验规则
      const id = joi.number().integer().min(1).required();
      
      exports.add_cate_schema = {
        // 表示需要对 req.body 中的数据进行验证
        body: {
          name,
          alias,
        },
      };
      
      // 校验规则对象 - 删除分类
      exports.delete_cate_schema = {
        params: {
          id,
        },
      };
      
      // 校验规则对象 - 根据 Id 获取分类
      exports.get_cate_schema = {
        params: {
          id,
        },
      };
      
      // 校验规则对象 - 更新分类
      exports.update_cate_schema = {
        body: {
          Id: id,
          name,
          alias,
        },
      };
  • router 文件夹

    • user.js

      const express = require("express");
      const router = express.Router();
      
      // 导入用户路由处理函数模块
      const userHandler = require("../router_handler/user");
      
      // 导入验证表单数据的中间件
      const expressJoi = require("@escook/express-joi");
      // 导入需要的验证规则对象
      const { reg_login_schema } = require("../schema/user");
      
      // 注册新用户
      // 在注册新用户的路由中,声明局部中间件,对当前请求中携带的数据进行验证
      // 数据验证通过后,会把这次请求流转给后面的路由处理函数
      // 数据验证失败后,终止后续代码的执行,并抛出一个全局的 Error 错误,进入全局错误级别中间件中进行处理
      // 请求 /api/reguser 接口的时候,会先判断 expressJoi(reg_login_schema) 是否执行成功,如果成功才会往下走,如果不成功就会走到自己定义的全局错误中间件。
      router.post("/reguser", expressJoi(reg_login_schema), userHandler.regUser);
      
      // 登录
      router.post("/login", expressJoi(reg_login_schema), userHandler.login);
      
      module.exports = router;
    • userinfo.js

      const express = require("express");
      const router = express.Router();
      
      // 导入验证数据合法性的中间件
      const expressJoi = require("@escook/express-joi");
      
      // 导入需要验证的规则对象
      const {
        update_userinfo_schema,
        update_password_schema,
        update_avatar_schema,
      } = require("../schema/user");
      
      // 导入用户信息的处理函数模块
      const userinfo_handler = require("../router_handler/userinfo");
      
      // 获取用户的基本信息的路由
      router.get("/userinfo", userinfo_handler.getUserInfo);
      // 更新用户信息的路由
      router.post(
        "/userinfo",
        expressJoi(update_userinfo_schema),
        userinfo_handler.updateUserInfo
      );
      // 重置密码的路由
      router.post(
        "/updatepwd",
        expressJoi(update_password_schema),
        userinfo_handler.updatePassword
      );
      // 更改用户头像路由
      router.post(
        "/update/avatar",
        expressJoi(update_avatar_schema),
        userinfo_handler.updateAvatar
      );
      
      module.exports = router;
    • artcate.js

      const express = require("express");
      const router = express.Router();
      
      // 导入验证数据的中间件
      const expressJoi = require("@escook/express-joi");
      // 导入文章分类的验证模块
      const {
        add_cate_schema,
        delete_cate_schema,
        get_cate_schema,
        update_cate_schema,
      } = require("../schema/artcate");
      
      // 导入文章分类的路由处理函数模块
      const artcate_handler = require("../router_handler/artcate");
      
      // 获取文章分类列表数据的路由
      router.get("/cates", artcate_handler.getArticleCates);
      
      // 新增文章分类的路由
      router.post(
        "/addcates",
        expressJoi(add_cate_schema),
        artcate_handler.addArticleCates
      );
      
      // 根据id删除文章分类的路由
      router.get(
        "/deletecate/:id",
        expressJoi(delete_cate_schema),
        artcate_handler.deleteCateById
      );
      
      // 根据id获取文章分类数据的路由
      router.get(
        "/cates/:id",
        expressJoi(get_cate_schema),
        artcate_handler.getArtCateById
      );
      
      // 根据 id 更新文章分类的路由
      router.post(
        "/updatecate",
        expressJoi(update_cate_schema),
        artcate_handler.updateCateById
      );
      
      module.exports = router;
  • router_handler 文件夹

    • user.js

      // 导入数据库操作模块
      const db = require("../db/index");
      
      // 导入 bcryptjs 这个包
      const bcrypt = require("bcryptjs");
      
      // 导入生成 token 的包
      const jwt = require("jsonwebtoken");
      
      // 导入全局的配置文件
      const conFig = require("../config");
      
      // 注册新用户的处理函数
      exports.regUser = (req, res) => {
        // 获取客户端提交到服务器的用户信息
        const userinfo = req.body;
        // 对表单中的数据,进行合法性的校验
        // if (!userinfo.username || !userinfo.password) {
        //   // return res.send({ status: 1, message: "用户名或密码不合法!" });
        //   return res.cc("用户名或密码不合法!");
        // }
      
        // 定义 SQL 语句,查询用户名是否被占用
        const sqlStr = `select * from ev_users where username=?`;
        db.query(sqlStr, userinfo.username, (err, results) => {
          // 执行 sql 语句失败
          if (err) {
            // return res.send({ status: 1, message: err.message });
            return res.cc(err);
          }
          // 判断用户名是否已被占用
          if (results.length > 0) {
            // return res.send({
            //   status: 1,
            //   message: "用户名已被占用,请更换其他用户名!",
            // });
            return res.cc("用户名已被占用,请更换其他用户名!");
          }
      
          // 用户名可以使用
          // 调用 bcrypt.hashSync(明文密码, 随机盐的长度) 对密码进行加密
          // 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
          userinfo.password = bcrypt.hashSync(userinfo.password, 10);
      
          // 定义插入新用户的 SQL 语句
          const sql = "insert into ev_users set ?";
          db.query(
            sql,
            { username: userinfo.username, password: userinfo.password },
            (err, results) => {
              // 判断 SQL 语句是否执行成功
              // if (err) {
              //   return res.send({ status: 1, message: err.message });
              // }
              if (err) return res.cc(err);
      
              // 判断影响行数是否为 1
              // if (results.affectedRows !== 1) {
              //   return res.send({ status: 1, message: "注册用户失败,请稍后重试" });
              // }
              if (results.length > 0) {
                return res.cc("注册用户失败,请稍后重试");
              }
      
              // 注册用户成功
              // res.send({ status: 0, message: "注册用户成功!" });
              res.cc("注册用户成功!", 0);
            }
          );
        });
      };
      
      // 登录的处理函数
      
      exports.login = (req, res) => {
        // 接收表单的数据
        const userinfo = req.body;
        // 定义 SQL 语句
        const sql = "select * from ev_users where username = ?";
        // 执行 SQL 语句,根据用户名查询用户的信息
        db.query(sql, userinfo.username, (err, results) => {
          if (err) {
            return res.cc(err);
          }
          // 执行 SQL 语句成功,但是获取到的数据条数不等于 1
          if (results.length !== 1) {
            return res.cc("登录失败!");
          }
      
          // 判断密码是否正确
          // 调用 bcrypt.compareSync(用户提交的密码, 数据库中的密码) 方法比较密码是否一致
          const compareResult = bcrypt.compareSync(
            userinfo.password,
            results[0].password
          );
          if (!compareResult) {
            return res.cc("登陆失败!");
          }
      
          // 在服务器端生成 Token 字符串
          const user = { ...results[0], password: "", user_pic: "" };
          // 对用户的信息进行加密,生成 Token 字符串
          const tokenStr = jwt.sign(user, conFig.jwtSecretKey, {
            expiresIn: conFig.expiresIn,
          });
          // 将 token 响应给客户端
          res.send({
            status: 0,
            message: "登陆成功",
            token: "Bearer " + tokenStr,
          });
        });
      };
    • userinfo.js

      // 导入数据库操作模块
      const db = require("../db/index");
      
      // 可使用 bcrypt.compareSync(提交的密码,数据库中的密码) 方法验证密码是否正确
      // compareSync() 函数的返回值为布尔值,true 表示密码正确,false 表示密码错误
      const bcrypt = require("bcryptjs");
      
      // 获取用户信息
      exports.getUserInfo = (req, res) => {
        // 根据用户的 id,查询用户的基本信息
        // 注意:为了防止用户的密码泄露,需要排除 password 字段
        const sqlStr = `select id, username, nickname, email, user_pic from ev_users where id=?`;
        // 注意:req 对象上的 user 属性,是 Token 解析成功,express-jwt 中间件帮我们挂载上去的
        db.query(sqlStr, req.user.id, (err, results) => {
          // 执行 SQL 语句失败
          if (err) return res.cc(err);
      
          // 执行 SQL 语句成功,但是查询到的数据条数不等于 1
          if (results.length !== 1) return res.cc("获取用户信息失败!");
      
          res.send({
            status: 0,
            message: "获取用户基本信息成功!",
            data: results[0],
          });
        });
      };
      
      // 更新用户信息
      exports.updateUserInfo = (req, res) => {
        const sqlStr = "update ev_users set ? where id = ?";
        db.query(sqlStr, [req.body, req.body.id], (err, results) => {
          if (err) return res.cc(err);
          if (results.affectedRows !== 1) return res.cc("修改用户基础信息失败");
          return res.cc("修改用户基本信息成功!", 0);
        });
      };
      
      // 重置密码
      exports.updatePassword = (req, res) => {
        // 根据 id 查询用户的信息
        const sql = "select * from ev_users where id = ?";
        // 执行根据 id 查询用户信息的 SQL 语句
        db.query(sql, req.user.id, (err, results) => {
          if (err) return res.cc(err);
      
          // 检查指定 id 的用户是否存在
          if (results.length !== 1) return res.cc("用户不存在!");
      
          // 判断密码是否正确
          const compareResult = bcrypt.compareSync(
            req.body.oldPwd,
            results[0].password
          );
          if (!compareResult) return res.cc("旧密码错误!");
      
          // 定义更新用户密码的 SQL 语句
          const sqlStr = `update ev_users set password=? where id=?`;
          // 对新密码进行 bcrypt 加密处理
          const newPwd = bcrypt.hashSync(req.body.newPwd, 10);
          // 执行 SQL 语句,根据 id 更新用户的密码
          db.query(sqlStr, [newPwd, req.user.id], (err, results) => {
            if (err) return res.cc(err);
            if (results.affectedRows !== 1) return res.cc("更新密码失败!");
            res.cc("更新密码成功!", 0);
          });
        });
      };
      
      // 更新用户头像
      exports.updateAvatar = (req, res) => {
        const sql = "update ev_users set user_pic = ? where id = ?";
      
        db.query(sql, [req.body.avatar, req.user.id], (err, results) => {
          if (err) return res.cc(err);
          if (results.affectedRows !== 1) return res.cc("更新头像失败");
          return res.cc("更新头像成功!", 0);
        });
      };
    • artcate.js

      // 导入数据库操作模块
      const db = require("../db/index");
      
      // 获取用户信息
      exports.getArticleCates = (req, res) => {
        // 根据分类的状态,获取所有未被删除的分类列表数据
        // is_delete 为 0 表示没有被标记为删除 的数据
        const sqlStr = `select * from ev_article_cate where is_delete = 0 order by id asc`;
        db.query(sqlStr, (err, results) => {
          if (err) return res.cc(err);
          res.send({
            status: 0,
            message: "获取文章分类列表成功!",
            data: results,
          });
        });
      };
      
      // 新增文章分类
      exports.addArticleCates = (req, res) => {
        // 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句
        const sql = `select * from ev_article_cate where name=? or alias=?`;
        db.query(sql, [req.body.name, req.body.alias], (err, results) => {
          if (err) return res.cc(err);
          // 分类名称 和 分类别名 都被占用
          if (results.length === 2) {
            return res.cc("分类名称与别名被占用,请更换后重试!");
          }
          if (
            results.length === 1 &&
            results[0].name === req.body.name &&
            results[0].alias === req.body.alias
          ) {
            return res.cc("分类名称与别名被占用,请更换后重试!");
          }
          // 分类名称 或 分类别名 被占用
          if (results.length === 1 && results[0].name === req.body.name) {
            return res.cc("分类名称被占用,请更换后重试!");
          }
          if (results.length === 1 && results[0].alias === req.body.alias) {
            return res.cc("分类别名被占用,请更换后重试!");
          }
      
          const sqlStr = `insert into ev_article_cate set ?`;
          db.query(sqlStr, req.body, (err, results) => {
            if (err) return res.cc(err);
            if (results.affectedRows !== 1) return res.cc("新增文章分类失败!");
            res.cc("新增文章分类成功!", 0);
          });
        });
      };
      
      // 删除文章分类的处理函数
      exports.deleteCateById = (req, res) => {
        // 标记删除的 SQL 语句
        const sql = `update ev_article_cate set is_delete=1 where id=?`;
        db.query(sql, req.params.id, (err, results) => {
          if (err) return res.cc(err);
          if (results.affectedRows !== 1) return res.cc("删除文章分类失败!");
          res.cc("删除文章分类成功!", 0);
        });
      };
      
      // 根据 id 获取文章分类的处理函数
      exports.getArtCateById = (req, res) => {
        const sql = `select * from ev_article_cate where id=?`;
        db.query(sql, req.params.id, (err, results) => {
          if (err) return res.cc(err);
          if (results.length !== 1) return res.cc("获取文章分类数据失败!");
          res.send({
            status: 0,
            message: "获取文章分类数据成功!",
            data: results[0],
          });
        });
      };
      
      // 根据 Id 更新文章分类数据
      exports.updateCateById = (req, res) => {
        // 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句
        const sql = `select * from ev_article_cate where Id!=? and (name=? or alias=?)`;
        db.query(
          sql,
          [req.body.Id, req.body.name, req.body.alias],
          (err, results) => {
            if (err) return res.cc(err);
      
            if (results.length === 2) {
              return res.cc("分类名称与别名被占用,请更换后重试!");
            }
            if (
              results.length === 1 &&
              results[0].name === req.body.name &&
              results[0].alias === req.body.alias
            ) {
              return res.cc("分类名称与别名被占用,请更换后重试!");
            }
            // 分类名称 或 分类别名 被占用
            if (results.length === 1 && results[0].name === req.body.name) {
              return res.cc("分类名称被占用,请更换后重试!");
            }
            if (results.length === 1 && results[0].alias === req.body.alias) {
              return res.cc("分类别名被占用,请更换后重试!");
            }
      
            const sqlStr = `update ev_article_cate set ? where Id=?`;
            db.query(sqlStr, [req.body, req.body.Id], (err, results) => {
              if (err) return res.cc(err);
              if (results.affectedRows !== 1) return res.cc("更新文章分类失败!");
              res.cc("更新文章分类成功!", 0);
            });
          }
        );
      };

文章作者: Mrr-cxh
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Mrr-cxh !
赏
 上一篇
JQuery JQuery
jQuery 是一个快速、简洁的 JavaScript 框架,是继 Prototype 之后又一个优秀的 JavaScript 代码库(框架)于 2006 年 1 月由 John Resig 发布。
2021-01-26
下一篇 
Node 2.0 Node 2.0
Node.js 是一个开源与跨平台的 JavaScript 运行时环境。 它是一个可用于几乎任何项目的流行工具!Node.js 在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核)。 这使 Node.js 表现得非常出色。前端可以通过node这个执行环境去写后端代码。
2021-01-17
  目录