Express 实战(二) 登录验证、身份认证、增删改查
最终结果:realworld-api-express-practise-
1. 数据验证(登录验证)
validate \ user.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
| exports.login = [ validate([ body("user.email").notEmpty().withMessage("邮箱不能为空"), body("user.password").notEmpty().withMessage("密码不能为空"), ]), validate([ body("user.email").custom(async (email, { req }) => { const user = await User.findOne({ email, }).select(["password", "username", "email", "bio", "image"]); if (!user) { return Promise.reject("用户不存在"); }
req.user = user; }), ]), validate([ body("user.password").custom(async (password, { req }) => { if (md5(password) !== req.user.password) { return Promise.reject("密码错误"); }
console.log(req.user); }), ]), ];
|
user 的路由那里也要加上
router \ user.js
2. 基于 JWT 的身份认证
JSON Web Tokens - jwt.io
JWT 原理:服务器认证之后,生成一个 JSON 对象,类似下面
1 2 3 4 5
| { "姓名": "clz", "角色": "admin", "到期时间": "2022-02-28" }
|
以后,用户和服务端通信,都要发回这个 JSON 对象,服务器只靠这个对象确认用户身份。为了防止用户篡改数据,服务器在生成这个对象时,会加上签名。
实际 JWT:
JWT 的三个部分:
- Header(头部)
- Payload(负载)
- Signature(签名)
Header.Payload.Signature
Header 部分是一个 JSON 对象,描述 JWT 的元数据
1 2 3 4
| { "alg": "HS256", "typ": "JWT" }
|
最后通过 Base64URL
算法将上面的JSON对象转成字符串
2.2 Payload
Payload 也是一个 JSON 对象,用来存实际需要传的数据。JWT 规定了 7 个官方字段
- iss(issuer):签发人
- exp(expiration time):过期时间
- sub(subject):主题
- aud(audience):受众
- nbf(Not Before):生效时间
- iat(Issued At):签发时间
- jti(JWT ID):编号
除了官方字段,还可以定义私有字段
1 2 3 4 5
| { "sub": "134567890", "name": "clz", "admin": true }
|
JWT 默认是不加密的,所以需要保密的信息不应该放在这部分
最后通过 Base64URL
算法将上面的 JSON 对象转成字符串
2.3 Signature
Signature 是对前两部分的签名,防止数据篡改
首先,需要指定一个密钥(这个密钥只有服务器知道,不能泄露给用户)。然后,使用 Header 里面指定的签名算法,按以下公式产生签名。
1 2 3 4
| HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret );
|
得到签名后,将 Header、Payload、Signature 三个部分拼接成一个字符串,用 .
分隔,可以返回给用户
在 JWT 中,消息体是透明的,使用签名可以保证消息不被篡改,但不能实现数据加密功能
将 Header 和 Payload 串型化的算法是 BaseURL
,和 Base64
算法类似,但有一些不同。
JWT 作为一个令牌(token),有时候需要放到 URL 中(如 api.example.com/?token=xxx)。
- Base64 中的三个字符
+
, /
, =
,在 URL 中有特殊意义
- Base64URL:
=
被省略, +
替换成 -
, /
替换成 _
2.4 JWT 的使用方式
客户端收到服务器返回的 JWT,可以存在 Cookie 里,也可以存在 localStorage 中。之后,客户端与服务器通信,都要带上这个 JWT,可以将 JWT 放在 Cookie 里自动发送,不过这样子不能跨域。更好的做法是:放在 HTTP 请求头的 Authorization
字段里面
1
| Authorization: Bearer <token>
|
2.5 使用 jsonwebtoken
jsonwebtoken 仓库
了解 jsonwebtoken 的使用
先安装, npm install jsonwebtoken
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
| const jwt = require("jsonwebtoken");
const token = jwt.sign( { foo: "bar", }, "hello", (err, token) => { if (err) { return console.log("生成token失败"); } console.log(token); } );
jwt.verify("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJmb28iOiJiYXIiLCJpYXQiOjE2NDQ2NjY1NDd9.\ 0Vy596XulYTCxeTrBp27U2T4BMh93IPN5l2b0GqxAMY", "hello", (err, ret) => { if (err) { return console.log("验证token失败"); } console.log(ret); });
|
2.5.1 生成 token
util \ jwt.js
1 2 3 4 5 6 7 8
| const jwt = require("jsonwebtoken"); const { promisify } = require("util");
exports.sign = promisify(jwt.sign);
exports.verify = promisify(jwt.verify);
exports.decode = promisify(jwt.decode);
|
config \ config.default.js
1 2 3 4
| module.exports = { dbURL: "mongodb://localhost:27017/realworld", jwtSecret: "c06eddf5-78eb-494f-b2c6-4a6d45b56cd5", };
|
controller userController.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
| const jwt = require('../util/jwt') const { jwtSecret } = require('../config/config.default')
async login(req, res, next) { try { const user = req.user.toJSON()
const token = await jwt.sign({ userId: user._id }, jwtSecret)
delete user.password res.status(200).json({ ...user, token }) } catch (err) { next(err) } }
|
2.6 中间件统一处理 JWT 身份认证
middleware \ authorization.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 { verify } = require("../util/jwt"); const { jwtSecret } = require("../config/config.default"); const { User } = require("../model/index");
module.exports = async (req, res, next) => { let token = req.headers["authorization"];
token = token ? token.split("Bearer ")[1] : null;
if (!token) { return res.status(401).end("请求头无token或token格式不对"); }
try { const decodedToken = await verify(token, jwtSecret); req.user = await User.findById(decodedToken.userId);
next(); } catch (err) { return res.status(401).end("token无效"); } };
|
2.7 JWT 过期时间
设置为 15 秒,体验下过期
2.8 Postman 自动添加 token
3. 新增文章
和注册类似
3.1 数据验证
validate \ article.js
1 2 3 4 5 6 7 8
| const validate = require("../middleware/validate"); const { body } = require("express-validator");
exports.createArticle = validate([ body("article.title").notEmpty().withMessage("文章标题不能为空"), body("article.description").notEmpty().withMessage("文章摘要不能为空"), body("article.body").notEmpty().withMessage("文章内容不能为空"), ]);
|
3.2 文章模型
model \ article.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
| const mongoose = require("mongoose");
const baseModel = require("./base-model");
const Schema = mongoose.Schema;
const articleSchema = mongoose.Schema({ ...baseModel, title: { type: String, required: true, }, description: { type: String, required: true, }, body: { type: String, required: true, }, tagList: { type: [String], default: null, }, favoritesCount: { type: Number, default: 0, }, author: { type: Schema.Types.ObjectId, ref: "User", required: true, }, });
module.exports = articleSchema;
|
ref 中的值需要时,model \ index.js 中导出的模型类中启用的名字
3.3 文章相关路由
新增文章部分加上了 JWT 身份认证和数据验证
router \ article.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
| const express = require("express");
const articleController = require("../controller/articleController"); const authorization = require("../middleware/authorization"); const articleValidate = require("../validate/article");
const router = express.Router();
router.get("/", articleController.listArticles);
router.get("/feed", articleController.feedArticles);
router.get("/:slug", articleController.getArticle);
router.post( "/", authorization, articleValidate.createArticle, articleController.createArticle );
router.put("/:slug", articleController.updateArticle);
router.delete("/:slug", articleController.deleteArticle);
router.post("/:slug/comments", articleController.addComments);
router.get("/:slug/comments", articleController.getComments);
router.delete("/:slug/comments/:id", articleController.deleteComment);
router.post("/:slug/favorite", articleController.likeArticle);
router.delete("/:slug/favorite", articleController.unlikeArticle);
module.exports = router;
|
3.4 处理请求
controller \ articleController.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 111 112 113 114
| const { Article } = require("../model/index");
class articleController { async listArticles(req, res, next) { try { res.send("获取所有文章"); } catch (err) { next(err); } }
async feedArticles(req, res, next) { try { res.send("获取关注用户的所有文章"); } catch (err) { next(err); } }
async getArticle(req, res, next) { try { res.send("获取单篇文章"); } catch (err) { next(err); } }
async createArticle(req, res, next) { try { const article = new Article(req.body.article);
article.author = req.user._id; article.populate("author");
await article.save(); res.status(201).json({ article, }); } catch (err) { next(err); } }
async updateArticle(req, res, next) { try { res.send("更新文章"); } catch (err) { next(err); } }
async deleteArticle(req, res, next) { try { res.send("删除文章"); } catch (err) { next(err); } }
async addComments(req, res, next) { try { res.send("增加一篇文章的评论"); } catch (err) { next(err); } }
async getComments(req, res, next) { try { res.send("获取一篇文章的评论"); } catch (err) { next(err); } }
async deleteComment(req, res, next) { try { res.send("删除文章的一条评论"); } catch (err) { next(err); } }
async likeArticle(req, res, next) { try { res.send("喜欢一篇文章"); } catch (err) { next(err); } }
async unlikeArticle(req, res, next) { try { res.send("取消喜欢一篇文章"); } catch (err) { next(err); } } }
module.exports = new articleController();
|
疑点:老师说查询时不需要 execPopulate(),new 出来时需要,相当于执行一次查询。但是个人试验时发现都不需要 execPopulate(),加上反而会出错,类似 "article.populate(...).execPopulate is not a function"
可能是时代变了,现在 new 出来的时候,也执行了
4. 查询文章
4.1 数据验证
model \ article.js(部分)
1 2 3 4 5 6 7
| exports.getArticle = validate([ param("slug").custom(async (value) => { if (!mongoose.isValidObjectId(value)) { return Promise.reject("文章ID类型错误"); } }), ]);
|
4.2 路由
router \ article.js(部分)
1 2
| router.get("/:slug", articleValidate.getArticle, articleController.getArticle);
|
4.3 处理请求
controller \ articleController.js(部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| async getArticle(req, res, next) { try { const article = await Article.findById(req.params.slug).populate('author')
if (!article) { return res.status(404).end() }
res.status(200).json( article ) } catch (err) { next(err) } }
|
5. 获取所有文章
controller \ articleController.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
| async listArticles(req, res, next) { try { const { offset = 0, limit = 20, tag, author } = req.query
const filter = {}
if (tag) { filter.tagList = tag }
if (author) { const user = await User.findOne({ username: author }) filter.author = user ? user._id : null }
const articlesCount = await Article.countDocuments()
const articles = await Article.find(filter) .skip(Number.parseInt(offset)) .limit(Number.parseInt(limit)) .sort({ createdAt: -1 })
res.status(200).json({ articles, articlesCount }) } catch (err) { next(err) } }
|
6. 更新文章
6.1 封装验证 ID 是否有效
修改 validate 中间件
middle \ validate.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
| const { validationResult, buildCheckFunction } = require("express-validator"); const { isValidObjectId } = require("mongoose");
exports = module.exports = (validations) => { return async (req, res, next) => { await Promise.all(validations.map((validation) => validation.run(req)));
const errors = validationResult(req); if (errors.isEmpty()) { return next(); }
res.status(400).json({ errors: errors.array() }); }; };
exports.isValidObjectId = (location, fields) => { return buildCheckFunction(location)(fields).custom(async (value) => { if (!isValidObjectId(value)) { return Promise.reject("ID不是有效的ObjectID"); } }); };
|
6.2 修改 article 的验证以及添加更新文章的验证
validate \ article.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
| const validate = require("../middleware/validate"); const { body, param } = require("express-validator"); const { Article } = require("../model");
exports.createArticle = validate([ body("article.title").notEmpty().withMessage("文章标题不能为空"), body("article.description").notEmpty().withMessage("文章摘要不能为空"), body("article.body").notEmpty().withMessage("文章内容不能为空"), ]);
exports.getArticle = validate([ validate.isValidObjectId(["params"], "slug"),
]);
exports.updateArticle = [ validate([ validate.isValidObjectId(["params"], "slug"), ]), async (req, res, next) => { const articleId = req.params.slug; const article = await Article.findById(articleId); req.article = article;
if (!article) { return res.status(404).end(); } next(); }, async (req, res, next) => {
if (req.user._id.toString() !== req.article.author.toString()) { return res.status(403).end(); } next(); }, ];
|
6.3 增加 article 的路由——更新文章
route \ article.js
1 2 3 4 5 6 7
| router.put( "/:slug", authorization, articleValidate.updateArticle, articleController.updateArticle );
|
6.4 处理请求(更新文章)
controller \ articleController.js(部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| async updateArticle(req, res, next) { try { const article = req.article const bodyArticle = req.body.article
article.title = bodyArticle.title || article.title article.description = bodyArticle.description || article.description article.body = bodyArticle.body || article.body
await article.save() res.status(200).json({ article }) } catch (err) { next(err) } }
|
7. 删除文章
7.1 数据验证
middle \ validate.js(部分)
1
| exports.deleteArticle = exports.updateArticle;
|
7.2 路由
route \ article.js(部分)
1 2 3 4 5 6 7
| router.delete( "/:slug", authorization, articleValidate.deleteArticle, articleController.deleteArticle );
|
7.3 处理请求
controller \ articleController.js(部分)
1 2 3 4 5 6 7 8 9 10
| async deleteArticle(req, res, next) { try { const article = req.article await article.remove() res.status(204).end() } catch (err) { next(err) } }
|