# 密钥认证

# 安装库

现在我们将在后端实现基于令牌的认证
首先,我们需要安装一个用于生成令牌的库,这里我们使用 jsonwebtoken
安装

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 { username, password } = request.body
 // 从数据库获取用户
  const user = await User.findOne({ username })
  // 检查用户名和密码
  // 由于密码本身并未存储明文,而是使用 bcrypt 进行加密,所以需要使用 bcrypt.compare 进行比较
  const passwordCorrect = user === null
    ? false
    : await bcrypt.compare(password, user.passwordHash)
  if (!(user && passwordCorrect)) {
    return response.status(401).json({
      error: 'invalid username or password'
    })
  }
    // 如果密码正确,将通过 `jwt.sign` 方法创建一个 token
  const userForToken = {
    username: user.username,
    id: user._id,
  }
    // SECRET 是自定义的密钥,是环境变量在.env 文件中设置,用于加密 token
  const token = jwt.sign(userForToken, process.env.SECRET)
    // 生成的 token 和用户名在响应体中返回
  response
    .status(200)
    .send({ token, username: user.username, name: user.name })
})
module.exports = loginRouter

将登录代码添加到应用中 app.js

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

当然不要忘记设置环境变量

# 实现只有在有效的 token 下才能 post

我们将使用 Authorization 头来验证 token。
实际上如果 token 是字符串 eyJhbGciOiJIUzI1NiIsInR5c2VybmFtZSI6Im1sdXVra2FpIiwiaW ,那么 Authorization 头应该设置为: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5c2VybmFtZSI6Im1sdXVra2FpIiwiaW

创建新的博客 controllers/blogs.js 修改为:

// ...
const jwt = require('jsonwebtoken')
// ...
// 将 token 从 authorization 头部分离出来
const getTokenFrom = request => {
  const authorization = request.get('authorization')
  if (authorization && authorization.startsWith('Bearer ')) {
    return authorization.replace('Bearer ', '')
  }
  return null
}
blogsRouter.post('/', async (request, response) => {
  if(!request.body.title || !request.body.url) {
    response.status(400).json({ error: 'title or url missing' });
    return
  }
  const body = request.body
    // 验证 token
  const decodedToken = jwt.verify(getTokenFrom(request), process.env.SECRET) 
  if(!decodedToken.id) {
    return response.status(401).json({ error: 'token missing or invalid' })
  }
  const user = await User.findById(decodedToken.id); // 通过 userId 找到对应的 user
  const blog = new Blog({
    title: request.body.title,
    author: request.body.author,
    url: request.body.url,
    likes: request.body.likes || 0,
    user: user.id
  })
  const savedBlog = await blog.save();
  user.blogs = user.blogs.concat(savedBlog._id);
  await user.save();
  response.status(201).json(savedBlog);
})

如果 token 丢失或无效,将引发异常 JsonWebTokenError,在错误处理中间件中添加错误处理程序

else if (error.name ===  'JsonWebTokenError') {
    return response.status(400).json({ error: 'token missing or invalid' })
  }

# 限制令牌的有效期

Token 认证之后。API 会对 token 拥有者盲目信任
我们可以设置 token 的过期时间,在 controllers/login.js 中修改如下

loginRouter.post('/', async (request, response) => {
  const { username, password } = request.body
  const user = await User.findOne({ username })
  const passwordCorrect = user === null
    ? false
    : await bcrypt.compare(password, user.passwordHash)
  if (!(user && passwordCorrect)) {
    return response.status(401).json({
      error: 'invalid username or password'
    })
  }
  const userForToken = {
    username: user.username,
    id: user._id,
  }
  // token expires in 60*60 seconds, that is, in one hour
  const token = jwt.sign(
    userForToken,
    process.env.SECRET,
    { expiresIn: 60*60 } // 设置 token 过期时间
  )
  response
    .status(200)
    .send({ token, username: user.username, name: user.name })
})

令牌过期后,客户端应用需要获取新的令牌。这样就可以避免令牌被窃取后被用于攻击
当然,也需要在令牌过期的时候给出适当的错误响应:
在错误处理中间件中添加如下代码:

else if (error.name === 'TokenExpiredError') {
    return response.status(401).json({
      error: 'token expired'
    })
  }
更新于 阅读次数