栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Express实战(二) 登录验证、身份认证、增删改查

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Express实战(二) 登录验证、身份认证、增删改查

Express实战(二) 登录验证、身份认证、增删改查

个人博客:Express实战(二) 登录验证、身份认证、增删改查
最终结果:realworld-api-express-practise-

1. 数据验证(登录验证)

validate user.js

exports.login = [
  validate([body("user.email").notEmpty().withMessage("邮箱不能为空"), body("user.password").notEmpty().withMessage("密码不能为空")]),
  validate([
    // 只有上面的验证通过才会执行,利用的是中间件的机制
    body("user.email").custom(async (email, { req }) => {
      // 这里参数的req解构是官网文档用法
      const user = await User.findOne({
        email
      }).select(["password", 'username', 'email', 'bio', 'image'])    // 这里需要获取密码的话,因为用户密码的模式设计那里设置了select: false,即通过查找不能查到密码,此时需要通过select()实现能查出密码
      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对象,类似下面

{
    "姓名": "clz",
    "角色": "admin",
    "到期时间": "2022-02-28"
}

以后,用户和服务端通信,都要发回这个JSON对象,服务器只靠这个对象确认用户身份。为了防止用户篡改数据,服务器在生成这个对象时,会加上签名。

实际JWT:

JWT的三个部分:

Header(头部)Payload(负载)Signature(签名)

Header.Payload.Signature

2.1 Header

Header部分是一个JSON对象,描述JWT的元数据

{
    "alg": "HS256",		// 表示签名的算法,默认是HMAC SHA256
    "typ": "JWT"		// 表示令牌(token)的类型,JWT令牌写为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):编号

除了官方字段,还可以定义私有字段

{
    "sub": "134567890",
    "name": "clz",
    "admin": true
}

JWT默认是不加密的,所以需要保密的信息不应该放在这部分

最后通过base64URL算法将上面的JSON对象转成字符串

2.3 Signature

Signature是对前两部分的签名,防止数据篡改

首先,需要指定一个密钥(这个密钥只有服务器知道,不能泄露给用户)。然后,使用Header里面指定的签名算法,按以下公式产生签名。

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字段里面

Authorization: Bearer 
2.5 使用jsonwebtoken

jsonwebtoken仓库

了解jsonwebtoken的使用

先安装,npm install jsonwebtoken

const jwt = require('jsonwebtoken')

// 生成jwt:jwt.sign
// // 同步方式:
// const token = jwt.sign({ foo: 'bar' }, 'hello');
// console.log(token)

// 异步方式:就只是加多一个回调函数
const token = jwt.sign({
  foo: 'bar'
}, 'hello', (err, token) => {
  if (err) {
    return console.log('生成token失败')
  }
  console.log(token)
});

// 验证jwt:jwt.verify
// 同步方式:
// const result = jwt.verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
// eyJmb28iOiJiYXIiLCJpYXQiOjE2NDQ2NjY1NDd9.
// 0Vy596XulYTCxeTrBp27U2T4BMh93IPN5l2b0GqxAMY', 'hello')

// console.log(result)

// 异步方式:
jwt.verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJmb28iOiJiYXIiLCJpYXQiOjE2NDQ2NjY1NDd9.
0Vy596XulYTCxeTrBp27U2T4BMh93IPN5l2b0GqxAMY',
  'hello',
  (err, ret) => {
    if (err) {
      return console.log('验证token失败')
    }
    console.log(ret)
  })
2.5.1 生成token

util jwt.js

const jwt = require('jsonwebtoken')
const { promisify } = require('util')   // 将回调函数转换成Promise形式


exports.sign = promisify(jwt.sign)

exports.verify = promisify(jwt.verify)

exports.decode = promisify(jwt.decode)  // 不验证,直接解析

config config.default.js

module.exports = {
  "dbURL": "mongodb://localhost:27017/realworld",     // MongoDB默认端口17017
  "jwtSecret": "c06eddf5-78eb-494f-b2c6-4a6d45b56cd5"   // uuid随机生成(直接搜索uuid)
}

controller userController.js(只改登录部分)

// 类前面引入
const jwt = require('../util/jwt')
const { jwtSecret } = require('../config/config.default')

// 用户登录
async login(req, res, next) {
  try {
    // 1. 数据验证
    // 2. 生成token
    const user = req.user.toJSON()

    const token = await jwt.sign({
      userId: user._id    // 生成token不需要全部user信息,只要_id即可
    }, jwtSecret)

    // 3. 发送成功响应(包含token的用户信息)
    delete user.password
    res.status(200).json({
      ...user,
      token
    })
  } catch (err) {
    next(err)
  }
}

2.6 中间件统一处理JWT身份认证

middleware authorization.js

const { verify } = require('../util/jwt')
const { jwtSecret } = require('../config/config.default')
const { User } = require('../model/index')

module.exports = async (req, res, next) => {
  // 1. 从请求头获取token
  let token = req.headers['authorization']

  token = token ? token.split('Bearer ')[1] : null

  if (!token) {
    return res.status(401).end('请求头无token或token格式不对')
  }

  try {
    // 2. 验证token是否有效
    //    无效 ==> 响应401状态码
    //    有效 ==> 把用户信息读取出来,并挂载到req请求对象中,继续往后执行
    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

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

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',   // 存用户id,之后映射到用户模型去
    required: true
  }
})

module.exports = articleSchema

ref中的值需要时,model index.js中导出的模型类中启用的名字

3.3 文章相关路由

新增文章部分加上了JWT身份认证和数据验证

router article.js

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)   // slug类似id,用于确定特定文章

// 新增文章
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

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     // 作者在数据库中只存一个用户id,通过id去获取用户
      article.populate('author')        // 简单来说,就是通过populate()以及文章模型中的ref: 'User',可以通过id把用户信息放到author中

      // article.populate('author').execPopulate()     

      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(部分)

exports.getArticle = validate([
  param('slug').custom(async value => {
    if (!mongoose.isValidObjectId(value)) {

      return Promise.reject('文章ID类型错误')
    }

  })
])
4.2 路由

router article.js(部分)

// 获取单篇文章
router.get('/:slug', articlevalidate.getArticle, articleController.getArticle)   // slug类似id,用于确定特定文章
4.3 处理请求

controller articleController.js(部分)

// 获取单篇文章
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(部分)

// 获取所有文章(可增加条件筛选)
  async listArticles(req, res, next) {
    try {
      const {
        offset = 0,// 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  // 如果有这个作者,则获取作者的id用于筛选。没有则为null
      }

      const articlesCount = await Article.countdocuments()

      const articles = await Article.find(filter)   // 筛选出有这个标签的文章
        .skip(Number.parseInt(offset))       // 跳过多少条
        .limit(Number.parseInt(limit))      // 取多少条
        .sort({       // 排序, -1代表倒叙,1代表正序
          createdAt: -1
        })

      res.status(200).json({
        articles,
        articlesCount
      })
    } catch (err) {
      next(err)
    }
  }
6. 更新文章 6.1 封装验证ID是否有效

修改validate中间件

middle validate.js

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

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')    // 第一个参数是验证的数据的位置,第二个参数是验证数据字段

  // param('slug').custom(async value => {
  //   if (!mongoose.isValidObjectId(value)) {

  //     return Promise.reject('文章ID类型错误')
  //   }

  // })
])

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     // 把article挂载到req上

    if (!article) {   // 要修改的文章不存在
      return res.status(404).end()
    }
    next()    // 文章存在,下一个中间件处理
  },
  async (req, res, next) => {
    // 判断文章作者是否是登录用户(禁止修改别人的文章)

    if (req.user._id.toString() !== req.article.author.toString()) {    // ObjectId是一个对象,不能直接比较
      return res.status(403).end()
    }
    next()
  }
]
6.3 增加article的路由——更新文章

route article.js

// 更新文章
router.put('/:slug', authorization, articlevalidate.updateArticle, articleController.updateArticle)
6.4 处理请求(更新文章)

controller articleController.js(部分)

// 更新文章
  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(部分)

exports.deleteArticle = exports.updateArticle
7.2 路由

route article.js(部分)

// 删除文章
router.delete('/:slug', authorization, articlevalidate.deleteArticle, articleController.deleteArticle)
7.3 处理请求

controller articleController.js(部分)

// 删除文章
async deleteArticle(req, res, next) {
  try {
    const article = req.article
    await article.remove()
    res.status(204).end()
  } catch (err) {
    next(err)
  }
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/755624.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号