用NodeJS进行JWT认证的实用指南
你是否试图将JWT认证集成到你的Node.js应用程序中,但从未找到合适的解决方案?那么你就来对地方了。在这篇文章中,我们将通过使用npm包jsonwebtoken ,引导你了解Node.js中JWT认证的更多细节。
如果你还不清楚JWT到底是什么以及它是如何工作的,你可以在继续执行之前,先看看我们之前的文章。正如我们在上一篇文章中所讨论的(用JWT保护应用程序的简要介绍),我们将在这个实现中遵循JWT认证的最佳实践。如果你想先复习一下JWT的知识,让我们来看看在本教程中我们将遵循哪些最佳实践:
- 在cookie中而不是在HTTP头中发送JWT令牌
- 为令牌设置一个较短的过期时间
- 使用刷新令牌来重新发布在短时间内到期的访问令牌
在进入细节之前,我想强调两个注意事项:
- 编写你自己的认证实现并不总是最好的解决方案。有几个第三方的产品可以以非常安全的方式为你处理所有的问题。
- 本教程中介绍的代码是一个单片机应用的实现。如果你想把这段代码用于微服务,你将不得不使用公钥/私钥的组合来签署和验证令牌。
现在我们已经设定了目标,让我们开始实施。
最初的准备工作...
如果你还没有,为这个项目设置好Node环境。然后,安装以下我们将在本教程中使用的软件包:
- Express:我们将使用的Node.js框架
- Cookie-Parser:由于我们将在cookie中发送JWT令牌,因此使用这个包来解析与请求一起发送的cookie。
- Body-Parser:这个包用来解析传入请求的正文,以提取POST参数。
- Dotenv: 这个包将环境变量从.env文件加载到应用程序环境中。
- Json-Web-Token:这是一个帮助我们实现JWT的软件包。
你可以使用以下命令安装这些包:
npm install express cookie-parser body-parser dotenv json-web-token --save
现在我们将在项目的主文件中设置应用程序的后端,app.js:
require('dotenv').config()
const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const app = express()
const {login, refresh} = require('./authentication')
app.use(bodyParser.json())
app.use(cookieParser())
app.post('/login', login)
app.post('/refrsh', refresh)
在接下来的步骤中,我们将在controller.js里面实现函数,我们在上面的代码中已经使用了这些函数。我们使用login函数来处理发送到/login路线上的帖子请求,并登录用户。我们使用refresh函数来处理发送到/refresh路由的帖子请求,并使用refresh令牌发布新的访问令牌。
设置环境变量
在实现登录用户的逻辑之前,我们需要设置配置JWTs所需的环境变量。创建一个.env文件,添加这两个我们将在应用程序中使用的变量:
ACCESS_TOKEN_SECRET=swsh23hjddnns
ACCESS_TOKEN_LIFE=120
REFRESH_TOKEN_SECRET=dhw782wujnd99ahmmakhanjkajikhiwn2n
REFRESH_TOKEN_LIFE=86400
你可以添加任何字符串作为秘密。建议使用带有随机字符的较长的秘密,作为一种安全措施。我们创建的访问令牌的过期时间将是120秒。我们还设置了秘密来签署刷新令牌和它的过期时间。请注意,与访问令牌相比,刷新令牌的寿命更长。
处理用户登录和JWT令牌的创建
现在我们可以进入实现我们导入到app.js 文件中的登录函数的步骤,以处理/login 路线。
为了这个实现,我们将在应用程序中存储一个用户对象的数组。在真实世界的情况下,你将从数据库或任何其他地方检索这些用户信息。另外,这只是为了演示的目的,永远不要存储实际的密码。
不要以纯文本形式保存密码
let users = {
john: {password: "passwordjohn"},
mary: {password:"passwordmary"}
}
当实现登录功能时,首先我们需要检索与登录POST请求一起发送的用户名和密码:
const jwt = require('json-web-token')
// Never do this!
let users = {
john: {password: "passwordjohn"},
mary: {password:"passwordmary"}
}
exports.login = function(req, res){
let username = req.body.username
let password = req.body.password
// Neither do this!
if (!username || !password || users[username] !== password){
return res.status(401).send()
}
}
如果请求中不包含用户名或密码,服务器会以401未授权状态作出响应。如果与请求一起发送的密码与存储在数据库中的该特定用户名的密码不匹配,同样的响应也适用。
如果客户端向服务器发送了正确的凭证,那么我们就会通过发布新的JWT令牌来将用户登录到系统中。在这种情况下,新登录的用户会收到两个令牌:访问令牌和刷新令牌。访问令牌会和响应一起以cookie的形式发回给客户端。刷新令牌被存储在数据库中,以便在将来发行访问令牌。在我们的案例中,我们将把刷新令牌存储在我们先前创建的用户数组中:
const jwt = require('json-web-token')
// Never do this!
let users = {
john: {password: "passwordjohn"},
mary: {password:"passwordmary"}
}
exports.login = function(req, res){
let username = req.body.username
let password = req.body.password
// Neither do this!
if (!username || !password || users[username].password !== password){
return res.status(401).send()
}
//use the payload to store information about the user such as username, user role, etc.
let payload = {username: username}
//create the access token with the shorter lifespan
let accessToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
algorithm: "HS256",
expiresIn: process.env.ACCESS_TOKEN_LIFE
})
//create the refresh token with the longer lifespan
let refreshToken = jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
algorithm: "HS256",
expiresIn: process.env.REFRESH_TOKEN_LIFE
})
//store the refresh token in the user array
users[username].refreshToken = refreshToken
//send the access token to the client inside a cookie
res.cookie("jwt", accessToken, {secure: true, httpOnly: true})
res.send()
}
当在cookie中发送访问令牌时,记得设置httpOnly标志,以防止攻击者从客户端访问cookie。在上面的例子中,我们也设置了安全标志。然而,如果你只是在HTTP连接上尝试这段代码,而不是在HTTPS连接上,请删除安全标志,将其与响应一起发送。
添加一个中间件来验证用户请求
服务器需要在给予某些路由访问权之前检查用户是否已经登录。我们可以使用每次请求时在cookie中发送的访问令牌来验证用户是否真的通过了认证。这个过程是在一个中间件中进行的。
让我们创建一个名为middleware.js 的新文件,并实现验证方法,以检查用户是否已被认证。
首先,我们应该从与请求一起发送的cookie中检索出访问令牌。如果请求不包含访问令牌,它将不会进入预定路线,而是返回一个403禁止的错误:
const jwt = require('json-web-token')
exports.verify = function(req, res, next){
let accessToken = req.cookies.jwt
//if there is no token stored in cookies, the request is unauthorized
if (!accessToken){
return res.status(403).send()
}
}
如果请求包含访问令牌,那么服务器将验证它是否是由服务器本身使用存储的秘密发出的。如果令牌已经过期或被认为不是由服务器签署的,jsonwebtoken的验证方法将抛出一个错误。我们可以处理这个错误,将401错误返回给客户端:
const jwt = require('json-web-token')
exports.verify = function(req, res, next){
let accessToken = req.cookies.jwt
//if there is no token stored in cookies, the request is unauthorized
if (!accessToken){
return res.status(403).send()
}
let payload
try{
//use the jwt.verify method to verify the access token
//throws an error if the token has expired or has a invalid signature
payload = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET)
next()
}
catch(e){
//if an error occured return request unauthorized error
return res.status(401).send()
}
}
现在我们可以使用这个中间件来保护任何需要用户在访问前登录的路由。将中间件导入到你处理路由的地方,在我们的例子中,是app.js 。 如果我们试图保护一个名为/comments的路由,可以通过在路由处理程序之前添加中间件来轻松实现:
const {verify} = require('./middleware')
app.get('/comments', verify, routeHandler)
只有当用户被认证后,请求才会被传递给路由处理程序。
使用刷新令牌发布新的访问令牌
还记得我们在app.js 文件的初始代码中使用的/refresh路由和刷新函数吗?现在我们可以实现这个刷新函数,使用存储的刷新令牌发行新的访问令牌。
这里使用的函数,refresh,也在我们之前用来实现登录函数的controller.js 文件里面。
这个函数的第一部分很像我们验证访问令牌的方法。检查访问令牌是否被发送,并验证令牌:
exports.refresh = function (req, res){
let accessToken = req.cookies.jwt
if (!accessToken){
return res.status(403).send()
}
let payload
try{
payload = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET)
}
catch(e){
return res.status(401).send()
}
}
然后,我们将使用存储在访问令牌有效载荷中的用户名来检索这个特定用户的刷新令牌。一旦验证了刷新令牌,服务器将发出一个新的访问令牌,正如在登录函数中实现的那样。
如果刷新令牌已经过期或无法验证,服务器将返回一个未经授权的错误。否则,新的访问令牌将被发送到cookie中:
exports.refresh = function (req, res){
let accessToken = req.cookies.jwt
if (!accessToken){
return res.status(403).send()
}
let payload
try{
payload = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET)
}
catch(e){
return res.status(401).send()
}
//retrieve the refresh token from the users array
let refreshToken = users[payload.username].refreshToken
//verify the refresh token
try{
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET)
}
catch(e){
return res.status(401).send()
}
let newToken = jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET,
{
algorithm: "HS256",
expiresIn: process.env.ACCESS_TOKEN_LIFE
})
res.cookie("jwt", newToken, {secure: true, httpOnly: true})
res.send()
}
这一步完成了我们用Node.js实现JWT认证的过程。
总结
在本教程中,我们经历了在Node.js中用JWT实现认证的步骤。作为上一篇文章的延续,我们讨论了JWT认证背后的理论,我们的实现主要是坚持我们之前讨论的最佳实践。因此,我们的JWT实现利用cookies来发送JWT和刷新令牌来生成新的访问令牌。如果你愿意比这个实现领先一步,你可以想出一个解决方案,在很短的时间范围内重新发行刷新令牌,以避免安全漏洞。
谢谢你的阅读!