Web全栈开发学习笔记—Part4 测试 Express 服务端程序, 以及用户管理—d.密钥认证

目录

Limiting creating new notes to logged in users

Error handling


现在将让后端支持基于令牌的认证下面的时序图描述了基于令牌认证的原理:

fullstack content

  • 用户首先在 React 中通过登录表单登录

  • 这会使得 React 代码将用户名和密码通过/api/login 作为一个 HTTP POST 请求发送给服务器。

  • 如果用户名和密码是正确的,服务器会生成一个 token,用来标识登录的用户。
  • 这个 Token 是数字化签名的,也就是它不可能被伪造(使用加密手段)。
  • 后台通过状态码返回一个 response, 表示操作成功,同时返回的还有这个 token。
  • 浏览器将这个 token 保存到 React 应用的状态中
  • 当用户请求创建一个新的 Note(或者做一些需要认证的操作), React 会通过 requset 发送这个 token 给 server
  • server 便可以通过这个 token 来验证用户

先来实现登录的功能。安装jsonwebtoken 库, 它会允许我们生成 Json Web Token

npm install jsonwebtoken

登录功能的代码放到 controllers/login.js 中

const jwt = require('jsonwebtoken')
const bcrypt = require('bcrypt')
const loginRouter = require('express').Router()
const User = require('../models/user')

loginRouter.post('/', async (request, response) => {
  const body = request.body

  const user = await User.findOne({ username: body.username })
  const passwordCorrect = user === null
    ? false
    : await bcrypt.compare(body.password, user.passwordHash)

  if (!(user && passwordCorrect)) {
    return response.status(401).json({
      error: 'invalid username or password'
    })
  }

  const userForToken = {
    username: user.username,
    id: user._id,
  }

  const token = jwt.sign(userForToken, process.env.SECRET)

  response
    .status(200)
    .send({ token, username: user.username, name: user.name })
})

module.exports = loginRouter

代码首先从数据库中根据 request 提供的 username 搜索用户。

然后通过检查 request 中的password, 由于 password 在数据库中并不是明文存储的,而是存储的通过 password 计算的 Hash 值, bcrypt.compare 方法用来检查 password 是否正确。

await bcrypt.compare(body.password, user.passwordHash)

如果用户没有找到, 或者是密码错误,request 会被 response 成401 unauthorized, 失败的原因会被放到 response 的 body 体中。

如果密码正确,通过 jwt.sign 方法创建一个 token, 这个 token 包含了数字签名表单中的用户名以及 user id。

const userForToken = {
  username: user.username,
  id: user._id,
}

const token = jwt.sign(userForToken, process.env.SECRET)

token 通过环境变量中的SECRET 作为密码 来生成数字化签名。

数字化签名确保只有知道了这个 secret 的组织才能够生成合法的 token

环境变量的值必须放到 .env文件中。

一个成功的请求会返回 200 OK的状态码。这个生成的 token 以及用户名放到了返回体中被返回。

现在登录代码通过新的路由加到了 app.js中。

const loginRouter = require('./controllers/login')

//...

app.use('/api/login', loginRouter)

尝试使用 VS Code REST-client 登录:

fullstack content

没法正常工作,以下是控制台信息:

(node:32911) UnhandledPromiseRejectionWarning: Error: secretOrPrivateKey must have a value
    at Object.module.exports [as sign] (/Users/mluukkai/opetus/_2019fullstack-koodit/osa3/notes-backend/node_modules/jsonwebtoken/sign.js:101:20)
    at loginRouter.post (/Users/mluukkai/opetus/_2019fullstack-koodit/osa3/notes-backend/controllers/login.js:26:21)
(node:32911) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)

jwt.sign(userForToken, process.env.SECRET) 方法失败了。因为我们忘记了给环境变量一个 SECRET。它可以是任何 string, 只要我们放到 .env中,登录就正常了。

一次成功的登录将返回用户详细信息和 token:

错误的用户名或密码会返回错误信息和相应的状态码

fullstack content

Limiting creating new notes to logged in users

【为登录用户限制创建 Note】

更改创建新 Note 的逻辑,只有合法 token 的 request 才能被通过。

有几种方法可以将令牌从浏览器发送到服务器中。我们将使用Authorization 头信息。头信息还包含了使用哪一种authentication scheme 。如果服务器提供多种认证方式,那么认证 Scheme 就十分必要, 用来告诉服务器应当如何解析发来的认证信息。

这里使用 Bearer schema。

假设我们有一个 token 字符串eyJhbGciOiJIUzI1NiIsInR5c2VybmFtZSI6Im1sdXVra2FpIiwiaW, 认证头信息的值则为:

Bearer eyJhbGciOiJIUzI1NiIsInR5c2VybmFtZSI6Im1sdXVra2FpIiwiaW

新建 Note 的代码修改如下:

const jwt = require('jsonwebtoken')

// ...
const getTokenFrom = request => {
  const authorization = request.get('authorization')
  if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
    return authorization.substring(7)
  }
  return null
}

notesRouter.post('/', async (request, response) => {
  const body = request.body
  const token = getTokenFrom(request)
  const decodedToken = jwt.verify(token, process.env.SECRET)
  if (!token || !decodedToken.id) {
    return response.status(401).json({ error: 'token missing or invalid' })
  }
  const user = await User.findById(decodedToken.id)

  const note = new Note({
    content: body.content,
    important: body.important === undefined ? false : body.important,
    date: new Date(),
    user: user._id
  })

  const savedNote = await note.save()
  user.notes = user.notes.concat(savedNote._id)
  await user.save()

  response.json(savedNote)
})

getTokenFrom 这个 辅助函数将 token 与认证头信息相分离。token 的有效性通过 jwt.verify 进行检查。这个方法同样解码了 token, 或者返回了一个 token 所基于的对象

const decodedToken = jwt.verify(token, process.env.SECRET)

这个对象通过 token 解码后得到username 和 id ,用来告诉 server 谁创建了这次 request。

如果没有 token, 或者对象解析后没有获得用户认证 (decodedToken.id is undefined),就会返回错误码401 unauthorized,并在 response 的 body 体中包含了失败的原因

if (!token || !decodedToken.id) {
  return response.status(401).json({
    error: 'token missing or invalid'
  })
}

当请求的创建者被成功解析,就会继续执行。

使用 Postman 赋值正确的 authorization 头信息,即bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ, 第二个值是登录操作返回的令牌,新的 Note 就能创建了。

使用 Postman :

或者使用 Visual Studio Code REST client:

fullstack content

Error handling

【错误处理】

Token 认证也可能引起JsonWebTokenError, 如果我们从 token 中删除了几个字符并提交创建 Note, 就会有如下报错:

JsonWebTokenError: invalid signature
    at /Users/mluukkai/opetus/_2019fullstack-koodit/osa3/notes-backend/node_modules/jsonwebtoken/verify.js:126:19
    at getSecret (/Users/mluukkai/opetus/_2019fullstack-koodit/osa3/notes-backend/node_modules/jsonwebtoken/verify.js:80:14)
    at Object.module.exports [as verify] (/Users/mluukkai/opetus/_2019fullstack-koodit/osa3/notes-backend/node_modules/jsonwebtoken/verify.js:84:10)
    at notesRouter.post (/Users/mluukkai/opetus/_2019fullstack-koodit/osa3/notes-backend/controllers/notes.js:40:30)

有许多原因会产生解码错误。token 可能是错误的(本例)、或者是伪造的或过期的。展开 errorHandler 中间件,来考虑不同的解码错误。

const unknownEndpoint = (request, response) => {
  response.status(404).send({ error: 'unknown endpoint' })
}

const errorHandler = (error, request, response, next) => {
  if (error.name === 'CastError') {
    return response.status(400).send({
      error: 'malformatted id'
    })
  } else if (error.name === 'ValidationError') {
    return response.status(400).json({
      error: error.message 
    })
  } else if (error.name === 'JsonWebTokenError') {
    return response.status(401).json({
      error: 'invalid token'
    })
  }

  logger.error(error.message)

  next(error)
}

如果应用有很多接口都需要认证,JWT 认证应当被分拆到它们自己的中间件中。可以使用一些现成的类库,如express-jwt

猜你喜欢

转载自blog.csdn.net/qq_39389123/article/details/112343370
今日推荐