jwt 刷新令牌_使用JWT和刷新令牌进行身份验证—第1部分。

jwt 刷新令牌

Authentication using JWT (JSON Web Token) is very useful for developing cross-platform applications. The flow of the authentication process is :

使用JWT(JSON Web令牌)进行身份验证对于开发跨平台应用程序非常有用。 身份验证过程的流程为:

  1. User logs in using their credentials. On a successful login, the server issues an access token which is valid for a certain period of time (say 10 minutes).

    用户使用其凭据登录。 成功登录后,服务器将发出在特定时间段(例如10分钟)内有效的访问令牌。
  2. On every request to a protected resource, the token must be provided in the request as a header.

    在对受保护资源的每个请求中,令牌都必须在请求中作为标头提供。
  3. When the token expires after the stipulated time (10 minutes in our case), the user gets logged out of the system and needs to log in again.

    如果令牌在规定的时间(在我们的情况下为10分钟)后过期,则用户将退出系统并需要重新登录。

The last step can be very irritating from the user’s point of view. Logging into the application every 10 minutes could lead to a very bad user experience. Therefore, to overcome this problem we use something called ‘refresh tokens’. The idea is to generate two tokens: an access token (valid for 10 minutes) and a refresh token ,with a longer lifetime. Every time the access token gets expired, the client side app sends a request to generate a new access token, using the refresh token. This continues throughout the lifetime of the refresh token. Once the refresh token is expired, the user needs to log in again. The lifetime of the refresh token varies from application to application. For apps dealing with sensitive data, we choose a lifetime of about 24 hours and simpler apps, we have refresh tokens valid for days or even months. However, the lifetime of a refresh token is significantly greater than that of an access token. This reduces the frequency at which the user needs to log in, resulting in better UX.

从用户的角度来看,最后一步可能会非常令人讨厌。 每10分钟登录到该应用程序可能会导致非常糟糕的用户体验。 因此,为了克服这个问题,我们使用了一种称为“刷新令牌”的方法。 这个想法是生成两个令牌:一个访问令牌(有效期为10分钟)和一个刷新令牌,它们具有更长的寿命。 每次访问令牌过期时,客户端应用程序都会使用刷新令牌发送请求以生成新的访问令牌。 这将在刷新令牌的整个生命周期中持续进行。 刷新令牌过期后,用户需要再次登录。 刷新令牌的生存期因应用程序而异。 对于处理敏感数据的应用程序,我们将其生命周期选择为约24小时,而对于更简单的应用程序,我们将使用有效期为几天甚至几个月的刷新令牌。 但是,刷新令牌的生存期显着大于访问令牌的生存期。 这样可以减少用户登录所需的频率,从而获得更好的用户体验。

Let’s see how we can implement the above strategy using the MERN stack. In this post, I shall discuss the server side code. The client side code and implementation (using React) has been covered in another post.

让我们看看如何使用MERN堆栈实现上述策略。 在这篇文章中,我将讨论服务器端代码。 客户端代码和实现(使用React)已在另一篇文章中介绍

(Note : This is code is not production grade and is only suitable for demonstrating the above idea.In production you would have to implement server side validations, optimizations and cleaning on top of everything shown here.)

(注意:这不是生产级别的代码,仅适合于证明上述想法。在生产中,您必须在此处显示的所有内容的基础上实现服务器端验证,优化和清理。)

服务器端 (Server Side)

Inside your project directory run the command to initialize a new node.js project : npm init --y

在您的项目目录中,运行命令来初始化一个新的node.js项目: npm init --y

Install the required dependencies using the following command :

使用以下命令安装所需的依赖项:

npm install --save express body-parser mongoose jsonwebtoken bcryptjs cors -D nodemon dotenv

The -D flag installs nodemon as a development dependency. It constantly watches files for changes and restarts the server automatically on every change, so that we do not have to do that manually every time.

-D标志将nodemon安装为开发依赖项。 它会持续监视文件中的更改,并在每次更改时自动重新启动服务器,因此我们不必每次都手动进行操作。

After the dependencies get installed, create a .env file. This is the file where we shall store our secret keys, against which the JWTs shall get generated. Add the following inside the .env file :

安装依赖项后,创建一个.env文件。 这是我们将存储密钥的文件,JWT将根据该密钥生成密钥。 在.env文件中添加以下内容:

ACCESS_TOKEN_SECRET=accesstokensecret
REFRESH_TOKEN_SECRET=refreshtokensecret

Finally, initialize an empty git repository using git init and create a file called .gitignore . Inside the file add the following :

最后,使用git init初始化一个空的git仓库,并创建一个名为.gitignore的文件。 在文件内添加以下内容:

node_modules/
.env

Now setup the project directory in the following manner :

现在以以下方式设置项目目录:

-controllers
-AuthController.js
-models
-user.model.js
-token.model.js
-node_modules
-routes
-index.js
-.gitignore
-app.js
-package.json
-package-lock.json

In the package.json file, under the scripts section, delete the test script and put the following scripts:

package.json文件的“ scripts部分下,删除test脚本并放入以下脚本:

"scripts": {
"dev": "nodemon app.js",
"start": "node app.js"
}

Now that our project setup is complete, let’s go ahead and write some code!

现在我们的项目设置已经完成,让我们继续编写一些代码!

In the app.js file, add the following code:

app.js文件中,添加以下代码:

require('dotenv').config();const express = require("express");const bodyparser = require("body-parser");const mongoose = require("mongoose");
const cors = require('cors');
const
api = require("./routes");//initialize express appconst app = express();
app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: false }));
app.use(cors());//setup database connection
mongoose.connect("mongodb://localhost:27017/authdemo", {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
}).then(() => console.log("Database connected!"))
.catch((err) => console.error(err));//setup routes:
app.get("/", (req, res) => {
return res.status(200).send("ok");
});
app.use("/api", api);//start server and listen on port 4000
app.listen(4000, () => console.log("Server Running on port 4000"));

Next, we create the user model. Inside the user.model.js file under models folder, add the following code:

接下来,我们创建用户模型。 在user.model.js文件夹下的user.model.js文件中,添加以下代码:

const mongoose = require("mongoose");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const Token = require("./token.model");
const { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } = process.env;//define schema
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
});//define schema level methods to create access token and refresh token:
userSchema.methods = {
createAccessToken: async function () {
try {
let { _id, username } = this;
let accessToken = jwt.sign(
{ user: { _id, username } },
ACCESS_TOKEN_SECRET,
{
expiresIn: "10m",
}
);
return accessToken;
} catch (error) {
console.error(error);
return;
}
},
createRefreshToken: async function () {
try {
let { _id, username } = this;
let refreshToken = jwt.sign(
{ user: { _id, username } },
REFRESH_TOKEN_SECRET,
{
expiresIn: "1d",
}
);await new Token({ token: refreshToken }).save();
return refreshToken;
} catch (error) {
console.error(error);
return;
}
},
};//pre save hook to hash password before saving user into the database:
userSchema.pre("save", async function (next) {
try {
let salt = await bcrypt.genSalt(12); // generate hash salt of 12 rounds
let hashedPassword = await bcrypt.hash(this.password, salt); // hash the current user's password
this.password = hashedPassword;
} catch (error) {
console.error(error);
}
return next();
});module.exports = mongoose.model("User", userSchema);

In the above file, we declare the user schema. The schema level methods createAccessToken and createRefreshToken are used to generate the access token and refresh token respectively. We save the refresh token on the server side to verify it’s validity later. In this case, we save it into the database. However, you can also use caching systems like redis to improve performance.

在上面的文件中,我们声明用户架构。 模式级别的方法createAccessTokencreateRefreshToken分别用于生成访问令牌和刷新令牌。 我们将刷新令牌保存在服务器端,以便稍后验证其有效性。 在这种情况下,我们将其保存到数据库中。 但是,您也可以使用缓存系统(例如redis)来提高性能。

Both the tokens generated contain information about the user. In this case we are storing the entire object (except the password ofcourse!). But you can store the fields you need to, as per the need of your app.

生成的两个令牌都包含有关用户的信息。 在这种情况下,我们将存储整个对象(当然,当然还有密码!)。 但是您可以根据应用程序的需要存储所需的字段。

Now, we create our routes for the API. Inside the index.js file under routes folder, add the following code:

现在,我们为API创建路由。 在routes文件夹下的index.js文件中,添加以下代码:

const router = require("express").Router();
const AuthController = require("../controllers/AuthController");
const Middleware = require("../middlewares");//@route POST /api/auth/signup
router.post("/auth/signup", AuthController.signup);//@route POST /api/auth/login
router.post("/auth/login", AuthController.login);//@route POST /api/auth/refresh_token
router.post("/auth/refresh_token", AuthController.generateRefreshToken);//@route DELETE /api/auth/logout
router.delete("/auth/logout", AuthController.logout);//@route GET /api/protected_resource
//@access to only authenticated users
router.get("/protected_resource", Middleware.checkAuth, (req, res) => {
return res.status(200).json({ user: req.user });
});module.exports = router;

In the above file, we declare the various routes of our API and map them to their respective controllers. There are 4 routes associated with the authentication process which are :

在上面的文件中,我们声明了API的各种路由,并将它们映射到各自的控制器。 与认证过程相关的4条路由是:

POST /api/auth/signup
POST /api/auth/login
POST /api/auth/refresh_token
DELETE /api/auth/logout

Another route GET /api/protected_resource is a protected route which displays the information of the logged in user.

另一条路由GET /api/protected_resource是一条受保护的路由,它显示已登录用户的信息。

We have a middleware function (Middleware.checkAuth ) to check and validate the access token, the working of which will be discussed later.

我们有一个中间件功能( Middleware.checkAuth )来检查和验证访问令牌,稍后将讨论其工作方式。

Now, we write our controller functions to control the authentication routes. Inside the AuthController.js file under the controllers folder, add the following code :

现在,我们编写控制器功能来控制身份验证路由。 里面的AuthController.js下文件controllers文件夹,添加以下代码:

const User = require("../models/user.model");
const Token = require("../models/token.model");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } = process.env;exports.signup = async (req, res) => {
try {
//check if username is already taken:
let user = await User.findOne({ username: req.body.username });
if (user) {
return res.status(400).json({ error: "Username taken." });
} else {
//create new user and generate a pair of tokens and send
user = await new User(req.body).save();
let accessToken = await user.createAccessToken();
let refreshToken = await user.createRefreshToken();return res.status(201).json({ accessToken, refreshToken });
}
} catch (error) {
console.error(error);
return res.status(500).json({ error: "Internal Server Error!" });
}
};
exports.login = async (req, res) => {
try {
//check if user exists in database:
let user = await User.findOne({ username: req.body.username });
//send error if no user found:
if (!user) {
return res.status(404).json({ error: "No user found!" });
} else {
//check if password is valid:
let valid = await bcrypt.compare(req.body.password, user.password);
if (valid) {
//generate a pair of tokens if valid and send
let accessToken = await user.createAccessToken();
let refreshToken = await user.createRefreshToken();return res.status(201).json({ accessToken, refreshToken });
} else {
//send error if password is invalid
return res.status(401).json({ error: "Invalid password!" });
}
}
} catch (error) {
console.error(error);
return res.status(500).json({ error: "Internal Server Error!" });
}
};
exports.generateRefreshToken = async (req, res) => {
try {
//get refreshToken
const { refreshToken } = req.body;
//send error if no refreshToken is sent
if (!refreshToken) {
return res.status(403).json({ error: "Access denied,token missing!" });
} else {
//query for the token to check if it is valid:
const tokenDoc = await Token.findOne({ token: refreshToken });
//send error if no token found:
if (!tokenDoc) {
return res.status(401).json({ error: "Token expired!" });
} else {
//extract payload from refresh token and generate a new access token and send it
const payload = jwt.verify(tokenDoc.token, REFRESH_TOKEN_SECRET);
const accessToken = jwt.sign({ user: payload }, ACCESS_TOKEN_SECRET, {
expiresIn: "10m",
});
return res.status(200).json({ accessToken });
}
}
} catch (error) {
console.error(error);
return res.status(500).json({ error: "Internal Server Error!" });
}
};
exports.logout = async (req, res) => {
try {
//delete the refresh token saved in database:
const { refreshToken } = req.body;
await Token.findOneAndDelete({ token: refreshToken });
return res.status(200).json({ success: "User logged out!" });
} catch (error) {
console.error(error);
return res.status(500).json({ error: "Internal Server Error!" });
}
};

We have exported 4 functions from this module:

我们已经从该模块导出了4个函数:

  1. signup — queries the database to check if there is already and existing with the given username. After passing this check, it creates a new user in the database and then logs the user in, ie, generates an access token and a refresh token and sends them as a response.

    signup —查询数据库,以检查是否存在给定用户名的现有用户。 通过此检查后,它将在数据库中创建一个新用户,然后将该用户登录,即,生成访问令牌和刷新令牌,并将其作为响应发送。

  2. login — verifies the credentials of the user and then logs the user in, ie, generates an access token and a refresh token and sends them as a response.

    login —验证用户的凭据,然后登录用户,即生成访问令牌和刷新令牌,并将其作为响应发送。

  3. generateRefreshToken — checks if the refresh token sent in the request body is valid by querying it in the database. After passing this check, it takes the payload stored in the refresh token and generates a new access token using it and sends it back to the client. This ensures that the user remains logged in.

    generateRefreshToken —通过在数据库中查询来检查在请求正文中发送的刷新令牌是否有效。 通过此检查后,它将获取存储在刷新令牌中的有效负载,并使用它生成一个新的访问令牌,并将其发送回客户端。 这样可以确保用户保持登录状态。

  4. logout — deletes the refresh token sent in the request body from the database. The access token has to be deleted from the client side, which I shall cover in Part 2.

    logout —从数据库中删除在请求正文中发送的刷新令牌。 访问令牌必须从客户端删除,这将在第2部分中介绍。

Finally, we need to add the middleware function to check and verify the validity of the token. The client must include a custom header containing the access token, on every request to a protected resource. In this case, we call the header x-auth-token . Inside the index.js file under the middlewares folder,add the following code :

最后,我们需要添加中间件功能来检查和验证令牌的有效性。 在对受保护资源的每次请求中,客户端都必须包括一个包含访问令牌的自定义标头。 在这种情况下,我们将标头称为x-auth-token 。 在middlewares文件夹下的index.js文件中,添加以下代码:

const jwt = require("jsonwebtoken");
const privateKey = process.env.ACCESS_TOKEN_SECRET;//middleware function to check if the incoming request in authenticated:
exports.checkAuth = (req, res, next) => {
// get the token stored in the custom header called 'x-auth-token'
const token = req.get("x-auth-token");//send error message if no token is found:
if (!token) {
return res.status(401).json({ error: "Access denied, token missing!" });
} else {
try {
//if the incoming request has a valid token, we extract the payload from the token and attach it to the request object.
const payload = jwt.verify(token, privateKey);
req.user = payload.user;
next();
} catch (error) {
// token can be expired or invalid. Send appropriate errors in each case:
if (error.name === "TokenExpiredError") {
return res
.status(401)
.json({ error: "Session timed out,please login again" });
} else if (error.name === "JsonWebTokenError") {
return res
.status(401)
.json({ error: "Invalid token,please login again!" });
} else {
//catch other unprecedented errors
console.error(error);
return res.status(400).json({ error });
}
}
}
};

The above module exports the following function :

上面的模块导出以下功能:

checkAuth — Checks if the incoming request has the x-auth-token header and verifies it against the private key. After passing these checks, the payload inside the access token(which is the user object in our case) is added to the request object, to make it available to subsequent middlewares/handlers. The control is then passed on to the route handler using next() . Appropriate errors are sent back to the client as and when required.

checkAuth —检查传入的请求是否具有x-auth-token标头,并根据私钥对其进行验证。 通过这些检查后,访问令牌(在本例中为用户对象)内部的有效负载将添加到请求对象,以使其可用于后续的中间件/处理程序。 然后使用next()将控件传递到路由处理程序。 适当的错误会在需要时发送回客户端。

This function is to be chained on as a middleware function to all routes that are to be restricted only to logged in users. (ie, GET /api/protected_resource in this case).

该功能将作为中间件功能链接到仅限于登录用户的所有路由。 (在这种情况下,即GET /api/protected_resource )。

So now we are all set with the backend code! Lets run the server .

所以现在我们都设置了后端代码! 让我们运行服务器。

Open a terminal window inside the project directory and run npm run dev . This shall start the server on port 4000 (or whatever port you have specified in app.js)

在项目目录中打开一个终端窗口,然后运行npm run dev 。 这将在端口4000(或您在app.js指定的任何端口)上启动服务器。

Image for post
After running the command
运行命令后

So we have our server up and running. You can test out the various endpoints using Postman.

这样我们就可以启动服务器并运行它。 您可以使用Postman测试各种端点。

All the code discussed in this post is available here. I will be writing another part soon to illustrate the client side implementation using React.

这篇文章中讨论的所有代码都可以在这里找到 。 我将很快写另一部分来说明使用React的客户端实现。

Thanks for reading through 😊

感谢您阅读😊

翻译自: https://medium.com/swlh/authentication-using-jwt-and-refresh-token-part-1-aca5522c14c8

jwt 刷新令牌

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值