密钥认证步骤:

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

安装jsonwebtoken库,用于生成json web token

  1. npm install jsonwebtoken

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

  1. const jwt = require('jsonwebtoken')
  2. const bcrypt = require('bcryptjs')
  3. const loginRouter = require('express').Router()
  4. const User = require('../models/user')
  5. loginRouter.post('/', async (request, response) => {
  6. const body = request.body
  7. const user = await User.findOne({ username: body.username })
  8. const passwordCorrect = user === null
  9. ? false
  10. : await bcrypt.compare(body.password, user.passwordHash)
  11. if (!(user && passwordCorrect)) {
  12. return response.status(401).json({
  13. error: 'invalid username or password'
  14. })
  15. }
  16. const userForToken = {
  17. username: user.username,
  18. id: user._id,
  19. }
  20. const token = jwt.sign(userForToken, process.env.SECRET)
  21. response
  22. .status(200)
  23. .send({ token, username: user.username, name: user.name })
  24. })
  25. module.exports = loginRouter

将login路由添加到app.js中

  1. const loginRouter = require('./controllers/login')
  2. //...
  3. app.use('/api/login', loginRouter)

别忘了在.env中添加环境变量SECRET
测试login, 成功,生成了一个token
image.png

Limiting creating new notes to logged in users

更改创建note的逻辑,只有合法token的request才能通过
有几种方法可以将令牌从浏览器发送到服务器中。我们将使用Authorization 头信息。头信息还包含了使用哪一种authentication scheme 。如果服务器提供多种认证方式,那么认证 Scheme 就十分必要。这种 Scheme 用来告诉服务器应当如何解析发来的认证信息。
Bearer schema 正是我们需要的。

  1. Bearer eyJhbGciOiJIUzI1NiIsInR5c2VybmFtZSI6Im1sdXVra2FpIiwiaW

修改创建note的代码

  1. const jwt = require('jsonwebtoken')
  2. // ...
  3. const getTokenFrom = request => {
  4. const authorization = request.get('authorization')
  5. if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
  6. return authorization.substring(7)
  7. }
  8. return null
  9. }
  10. notesRouter.post('/', async (request, response) => {
  11. const body = request.body
  12. const token = getTokenFrom(request)
  13. const decodedToken = jwt.verify(token, process.env.SECRET)
  14. if (!token || !decodedToken.id) {
  15. return response.status(401).json({ error: 'token missing or invalid' })
  16. }
  17. const user = await User.findById(decodedToken.id)
  18. const note = new Note({
  19. content: body.content,
  20. important: body.important === undefined ? false : body.important,
  21. date: new Date(),
  22. user: user._id
  23. })
  24. const savedNote = await note.save()
  25. user.notes = user.notes.concat(savedNote._id)
  26. await user.save()
  27. response.json(savedNote)
  28. })

测试,在request中加入authorization
注意: JSON的最后一行不能有逗号
image.png

Error handling

token 可能是错误的(本例)、或者是伪造的或过期的。让我们来展开 errorHandler 中间件,来考虑不同的解码错误。

  1. const unknownEndpoint = (request, response) => {
  2. response.status(404).send({ error: 'unknown endpoint' })
  3. }
  4. const errorHandler = (error, request, response, next) => {
  5. if (error.name === 'CastError') {
  6. return response.status(400).send({
  7. error: 'malformatted id'
  8. })
  9. } else if (error.name === 'ValidationError') {
  10. return response.status(400).json({
  11. error: error.message
  12. })
  13. } else if (error.name === 'JsonWebTokenError') {
  14. return response.status(401).json({
  15. error: 'invalid token'
  16. })
  17. }
  18. logger.error(error.message)
  19. next(error)
  20. }

Problems of Token-based authentication

限制token 的有效时间
login.js中修改, 添加参数

  1. const token = jwt.sign(
  2. userForToken,
  3. process.env.SECRET,
  4. { expireIn: 60 * 60 }
  5. )

一旦token 过期, 客户端程序需要重新获取一个新的token。通常通过强制用户重新登录app 的方式实现。
一旦token过期, 错误处理中间件应当扩展来给出一个合适的错误提示

  1. const errorHandler = (error, request, response, next) => {
  2. logger.error(error.message)
  3. if (error.name === 'CastError') {
  4. -- snip --
  5. } else if (error.name === 'TokenExpiredError') {
  6. return response.status(401).json({
  7. error: 'token expired'
  8. })
  9. }
  10. next(error)
  11. }