# 密钥认证
# 安装库
现在我们将在后端实现基于令牌的认证
首先,我们需要安装一个用于生成令牌的库,这里我们使用 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' | |
}) | |
} |