passport身份验证_如何使用Passport创建身份验证系统

本文翻译自Medium,介绍了如何使用Passport.js实现本地身份验证系统,详细讲解了使用这个流行的Node.js中间件进行用户认证的过程。
摘要由CSDN通过智能技术生成

passport身份验证

This is a step by step guide to help you create email/password authentication with passport.js.

这是一个逐步指南,可帮助您使用passport.js创建电子邮件/密码验证。

先决条件 (Prerequisites)

In order to follow along this post, you need to have a basic understanding of Node.js, Express.js and MongoDB.

为了继续阅读本文,您需要对Node.js,Express.js和MongoDB有基本的了解。

通行证简介 (Introduction to passport.js)

According to Passport’s website, passport is defined as follows:

根据Passport的网站,护照的定义如下:

“Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped into any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more.”

“ Passport是 Node.js的 身份验证中间件 Passport非常灵活和模块化,可以毫不费力地放入任何 基于 Express 的Web应用程序中。 一套全面策略支持认证使用 的用户名和密码 Facebook的 Twitter的 ,和 更多 “。

Yes, passport is a middleware. It is flexible and modular in a sense that we can install different strategies that help us achieve different tasks. Strategies are the tools that help us perform user authentication using username and password or other OAuth options. If you don’t know what strategies do, don’t worry, you will be able to understand what it does after we implement strategies in a while.

是的,护照是一种中间件。 从某种意义上说,它是灵活的和模块化的,我们可以安装不同的策略来帮助我们完成不同的任务。 策略是可以帮助我们使用用户名和密码或其他OAuth选项执行用户身份验证的工具。 如果您不知道策略的作用,请放心,在我们实施策略一段时间后,您将能够了解它的作用。

Now that we have understood what passport.js does, let’s start implementing it. For that, let’s open up the terminal in our main working directory and write the following command:

现在我们已经了解了passport.js的作用,让我们开始执行它。 为此,让我们在主工作目录中打开终端并编写以下命令:

npm init -y

The -y flag here sets every value to default.

这里的-y标志将每个值设置为默认值。

Let’s now install some of the packages we need.

现在让我们安装一些我们需要的软件包。

npm i bcrypt connect-mongo express express-session express-validator     mongoose passport passport-local

Let’s now create a file called index.js inside of our main project directory and write the following lines of code:

现在,在主项目目录中创建一个名为index.js的文件,并编写以下代码行:

First few lines are imports. From line 1 to 4, we have imported express, mongoose, express-session, and connect-mongo. Here, the task of express-session will be to create sessions; the task of connect-mongo is to help us store the sessions in our mongoDB database. Notice that, at line 4 connect-mongo takes in session as the argument.

前几行是导入。 从第1行到第4行,我们导入了expressmongooseexpress-sessionconnect-mongo 。 在这里, express-session的任务是创建会话。 connect-mongo的任务是帮助我们将会话存储在mongoDB数据库中。 请注意,在4号线connect-mongo发生在session作为参数。

Let’s now take a look at line 6, we have imported MONGO_URI and SECRET from ./config. MONGO_URI is the MongoDB database URI while SECRET is a secret value which we require while parsing cookies. We will be working on ./config shortly.

现在让我们看一下第6行,我们已经从./config导入了MONGO_URISECRETMONGO_URI是MongoDB数据库URI,而SECRET是我们在解析Cookie时需要的秘密值。 我们很快将在./config上工作。

We then imported passport and AuthRoute at line 8 and 9 respectively. We’ll work on passport and AuthRoute in a while.

然后,我们分别在第8行和第9行导入了passportAuthRoute 。 我们将在一段时间内处理passportAuthRoute

At line 11, we initialized our express app.

在第11行,我们初始化了Express应用程序。

Then, at line 13, we used express.json() middleware with the help of app.use(express.json()).

然后,在第13行,借助于app.use(express.json())使用了express.json()中间件。

Then, from line 14 to 24, we have written code which uses session middleware in our app. The session takes in key value pairs which takes multiple options. Let’s discuss those options. At line 16, we have a secret option. It is self-explanatory, right? At line 17, we have resave option which takes in Boolean value. We don’t want to resave the session. So, we have set it to false. At line 18, we have saveUninitialized option which also takes in Boolean. We don’t want to save uninitialized sessions. So, we have set it to false. Then, at line 19, we have the cookie option, which takes in the secure option. The secure option takes in Boolean value. In our case we have set secure to false. Since, we will be working on localhost, the secure option is set to false and if we set the secure option to true we need to have https:// . Remember that we need to set secure to true when we publish our app in production. Now, let’s take a look at line 20. There, we have an option called store. The store takes in the information about the database or just the memory address where the session should be stored. In our case, we are storing our session in MongoDB. For that, we have used MongoStore. We created a new instance of MongoStore with an option called mongooseConnection. The mongooseConnection takes in the mongoose.connection.

然后,从第14行到第24行,我们编写了在app使用session中间件的代码。 会话采用键值对,键对具有多个选项。 让我们讨论这些选项。 在第16行,我们有一个secret选项。 这是不言自明的,对吧? 在第17行,我们有resave选项,它接受布尔值。 我们不想重新保存会话。 因此,我们将其设置为false 。 在第18行,我们有saveUninitialized选项,该选项也包含布尔值。 我们不想保存未初始化的会话。 因此,我们将其设置为false 。 然后,在第19行,我们有cookie选项,该选项接受secure选项。 secure选项采用布尔值。 在我们的情况下,我们将secure设置为false 。 既然,我们将上工作的localhostsecure选项设置为false ,如果我们设置secure选项, true我们需要有https://开头 请记住,在生产环境中发布应用程序时,需要将secure设置为true 。 现在,让我们看一下第20行。在这里,我们有一个名为store的选项。 store接收有关数据库的信息,或者仅接收应存储会话的内存地址。 就我们而言,我们将会话存储在MongoDB中。 为此,我们使用了MongoStore 。 我们创建的新实例MongoStore与所谓的选项mongooseConnectionmongooseConnection接受mongoose.connection

At line 26, we have initialized the passport, which we had imported at line 8, using passport.initialize(). Then, at line 27, we have written code to tell the passport that it has to use session for the authentication process.

在第26行,我们初始化了passport ,这是我们在第8行已经进口,使用passport.initialize() 然后,在第27行,我们编写了代码来告诉护照它必须使用会话进行身份验证过程。

At line 29, we have created an endpoint /api/auth with the help of AuthRoute.

在第29行,我们借助AuthRoute创建了一个端点/ api / auth

At line 31, we have written code to store the value of the port, in which our server will run. We check if the port exists in the environment variable. If it exists, we store the value from the environment variable, else we use the default value as 5000.

在第31行,我们编写了代码来存储服务器将在其中运行的端口的值。 我们检查端口是否存在于环境变量中。 如果存在,我们将存储环境变量中的值,否则将使用默认值5000。

From line 33 to 36, we have written code to connect to the MongoDB database. It’s simple right?

从第33行到第36行,我们编写了连接MongoDB数据库的代码。 很简单吧?

Then, at line 38, we have written code to start our server at the given PORT.

然后,在第38行,我们编写了代码以在给定PORT处启动服务器。

Now that we have understood what’s happening inside of index.js file, let’s now work on ./config. Let’s create a directory called config in our main project directory. After that, let’s create three files called index.js, prod.js, and dev.js inside of the config directory.

既然我们已经了解了index.js文件内部发生了什么,那么现在让我们在./config上工作 让我们在主项目目录中创建一个名为config的目录。 之后,让我们在config目录中创建三个名为index.jsprod.jsdev.js的文件。

Let’s now open up the file index.js of the config directory and write the following lines of code:

现在让我们打开config目录的文件index.js并编写以下代码行:

if (process.env.NODE_ENV === "production") {     module.exports = require("./prod");} else {     module.exports = require("./dev");} 

The above code either exports the modules from prod.js or dev.js file. If the process.env.NODE_ENV is equal to "production" , we will export modules from prod.js else we will export modules from dev.js.

上面的代码从prod.jsdev.js文件中导出模块。 如果process.env.NODE_ENV等于"production" ,那么我们将从prod.js导出模块,否则我们将从dev.js导出模块。

Let’s now open up dev.js file and write the following lines of code:

现在让我们打开dev.js文件并编写以下代码行:

module.exports = {MONGO_URI:    ,// add mongoDB URI hereSECRET:      ,// add Secret here};

Inside of this file, we are exporting MONGO_URI and SECRET. We need to enter their respective values as mentioned in the comment above. The MONGO_URI takes in the mongoDB database URL while the SECRET takes in some secret value. We can enter any random string in the SECRET. Remember, this file gets exported only when the NODE_ENV is not "production".

在此文件内部,我们正在导出MONGO_URISECRET 。 我们需要输入上面评论中提到的它们各自的值。 MONGO_URI接受mongoDB数据库URL,而SECRET接受一些秘密值。 我们可以在SECRET输入任何随机字符串。 请记住,仅当NODE_ENV不是"production"时, NODE_ENV导出该文件。

Now that we have worked on dev.js file, let’s now work on prod.js file. Let’s write the following lines of code inside of the prod.js file:

现在我们已经处理了dev.js文件,现在让我们处理prod.js文件。 让我们在prod.js文件中编写以下代码行:

module.exports = {MONGO_URI: process.env.MONGO_URI,SECRET: process.env.SECRET,};

Inside of prod.js file, we have written code to export MONGO_URI and SECRET. The MONGO_URI takes in process.env.MONGO_URI as the value from our environment variables. Similarly, SECRET takes in process.env.SECRET as the value. Remember, these modules get exported only when NODE_ENV is "production" . So, in this article, we won’t be spending time on creating environment variables, okay?

prod.js文件中,我们编写了导出MONGO_URISECRET代码。 MONGO_URIprocess.env.MONGO_URI作为我们环境变量的值。 同样, SECRETprocess.env.SECRET用作值。 请记住,仅当NODE_ENV"production"时, NODE_ENV导出这些模块。 因此,在本文中,我们不会花时间在创建环境变量上,好吗?

Let’s now work on our routes. For that, let’s create a directory called routes inside of our main project directory. Then, create a file called Auth.js inside of the routes directory. Let’s now write the following lines of code inside of the file Auth.js:

现在开始我们的路线。 为此,让我们在主项目目录中创建一个名为route的目录 然后,在路由目录中创建一个名为Auth.js的文件。 现在让我们在文件Auth.js内编写以下代码行:

First few lines are just imports. At line 1, we imported express. At line 2, we imported the passport. At line 3, we imported check and validationResult from express-validator. At line 5, we imported our controllers namely login, logout , me and signup.

前几行只是导入。 在第1行,我们导入了express 。 在第二行,我们进口了passport 。 在第3行,我们进口的check ,并validationResultexpress-validator 。 在第5行,我们导入了控制器,即loginlogoutmesignup

At line 7, we created the router with the help of express.Router().

在第7行,我们借助express.Router()创建了router

From line 10 to 50, we have written code for our api/auth/signup route. From line 12 to 37, we have written code for validating data using express-validator. If you don’t know the basics of express-validator, you might want to take a look at this article which I have written.

从第10行到第50行,我们已经为api/auth/signup编写了代码 路线。 从第12行到第37行,我们编写了使用express-validator验证数据的代码。 如果您不了解express-validator的基础知识,则可能需要看一下我写的这篇文章。

Let’s go through line 12 to 37 a bit fast. From line 13 to 16, we have written code to specify that the name field should have minimum length of 3. If the minimum length isn’t 3, we generate a validation error message with .withMessage. From line 18 to 21, we wrote code to check if the email field has an actual email address. Similarly, from line 23 to 29, we wrote code to validate the password field.

让我们快速通过第12至37行。 从第13行到第16行,我们编写了代码以指定name字段的最小长度应为3。如果最小长度不是3,则将使用.withMessage生成验证错误消息。 从第18行到第21行,我们编写了代码来检查email字段中是否有实际的电子邮件地址。 同样,从第23行到第29行,我们编写了代码来验证密码字段。

We specified that the password should have minimum length of 8 and max length of 15. We also specified that the password field should have at least one number and at least one special character. From line 31 to 36, we have written code to check if the confirmPassword and password field have the same value. At line 39, we extracted and formatted the errors. Then we checked if the error exists or not. If the error exists, we send the user with an array of error messages with status code 422 at line 44. If errors don’t exist, we run next() at line 46.

我们指定密码的最小长度为8,最大长度为15。我们还指定密码字段应至少具有一个数字和至少一个特殊字符。 从第31行到第36行,我们编写了代码来检查confirmPasswordpassword字段是否具有相同的值。 在第39行,我们提取并格式化了错误。 然后,我们检查错误是否存在。 如果存在错误,则在第44行向用户发送一系列错误消息,状态代码为422。如果不存在错误,则在第46行运行next()

Then at line 49, we specified that the controller for the /api/auth/signup route should be signup.

然后在第49行,我们指定/api/auth/signup的控制器 路线应该signup

From line 53 to 59, we created a route for /api/auth/login endpoint. We have used passport.authenticate as the middleware for this endpoint. The authenticate takes in two arguments. The first argument takes in strategy name. In our case it is local. The second argument takes in an object. We can see that we have used an option called failureMessage in our second argument. The purpose of this option is self explanatory, right? At line 58, we have specified that the controller for /api/auth/login is login.

从第53行到第59行,我们为/ api/auth/login创建了一条路由 终点 我们已使用passport.authenticate作为此端点的中间件。 authenticate接受两个参数。 第一个参数采用策略名称。 在我们的情况下,它是local 。 第二个参数接受一个对象。 我们可以看到我们在第二个参数中使用了一个名为failureMessage的选项。 这个选项的目的是不言自明的,对吧? 在第58行,我们指定/ api/auth/login的控制器为login

Then, at line 62, we have created an endpoint for /api/auth/logout. We have also specified that this route should have the logout as the controller.

然后,在第62行,我们为/api/auth/logout创建了一个端点。 我们还指定了该路由应以logout作为控制器。

Then at line 65, we have created an endpoint for /api/auth/me route. We have also specified, this route should have me as the controller. The purpose of the me controller will be to get the logged in user’s info.

然后在第65行,我们为/api/auth/me路由创建了一个端点。 我们还指定了,此路由应以me为控制者。 me控制器的目的是获取登录用户的信息。

Let’s now work on models. For that, let’s create a directory called models inside of the main project directory. Let’s now create a file called User.js in the models directory. Let’s now write the following lines of code inside of the file:

现在让我们来研究模型。 为此,让我们在主项目目录中创建一个名为models的目录。 现在让我们在models目录中创建一个名为User.js的文件。 现在让我们在文件内部编写以下代码行:

At line 1, we imported mongoose. Then, at line 2, we imported bcrypt. We then created a UserSchema from line 12 to 25. The user schema has name, email, password fields. All of these fields are String.

在第1行,我们进口了mongoose 。 然后,在第2行,我们导入了bcrypt 。 然后,我们从第12行到第25行创建了一个UserSchema 。该用户架构具有nameemailpassword字段。 所有这些字段都是String

Then, at line 12, we used the pre("save",...) method. At line 13, we stored this in the user constant. After that, at line 16, we checked if the password field of the user is modified or not. If the password field isn’t modified, we use next() else, we go on to hash the password. We have written code to hash the password and store it in the variable hash at line 18. Here, we used bcrypt.hash to hash the password. The bcrypt.hash takes in plain text password as the first argument and hash round as the second argument. We have specified 13 as the hash round. We then saved the hash in the user.password at line 19. After that, we used next() at line 20. If there occurs any error while hashing the password, we have written code to log the error at line 22. Then, we use next(error) at line 23.

然后,在第12行,我们使用了pre("save",...)方法。 在第13行,我们保存thisuser不断。 之后,在第16行,我们检查了userpassword字段是否被修改。 如果password字段未修改,则使用next() else,然后继续对password进行哈希处理。 我们已经编写了用于对密码进行哈希处理的代码,并将其存储在第18行的变量hash中。在这里,我们使用bcrypt.hash来对密码进行哈希处理。 bcrypt.hash将纯文本密码作为第一个参数,将哈希回合作为第二个参数。 我们已将13指定为哈希回合。 然后,将散列保存在第19行的user.password中。之后,在第20行使用next() 。如果在对密码进行散列时发生任何错误,我们已经编写了代码以在第22行记录该错误。然后,我们在第23行使用next(error)

Now, at line 27 we created the comparePassword method using UserSchema.methods.comparePassword. This method will help us to compare hashed password with plain text password. At line 29, we have written code to compare the plain text password (password) with the hashed password (this.password ) and stored it in the result variable. The result will be true if the passwords match. We then return the result at line 31. If there occurs any error while carrying out this method, we log the error at line 33. Then, we return false, at line 34. We will be using this method while working on passport middleware.

现在,在第27行,我们使用UserSchema.methods.comparePassword创建了comparePassword方法。 此方法将帮助我们比较哈希密码和纯文本密码。 在第29行,我们编写了代码,将纯文本密码( password )与哈希密码( this.password )进行比较,并将其存储在result变量中。 如果密码匹配,则result为true。 然后,我们在第31行返回result 。如果在执行此方法时发生任何错误,则在第33行记录错误。然后,在第34行返回false 。在护照中间件上工作时将使用此方法。

At line 38, we exported the user model.

在第38行,我们导出了用户模型。

Let’s now work on our controllers. For that, let’s create a directory called controller inside of our main project directory. After that, let’s create a file called AuthController.js inside of the controller directory.

现在让我们在控制器上工作。 为此,让我们在主项目目录中创建一个名为controller的目录。 之后,让我们在控制器目录内创建一个名为AuthController.js的文件。

At line 1, we have imported the User model.

在第1行,我们导入了User模型。

At line 3, you can see that we have a function called login. At line 4, we have written code to send the user with the message that the user has successfully logged in and the status code of 200. Remember that, the logging in process is carried out by the passort.authenticate middleware, and this controller only gets called when the authentication is successful.

在第3行,您可以看到我们有一个名为login的函数。 在第4行,我们编写了代码,向用户发送用户已成功登录的消息以及状态码200。请记住,登录过程由passort.authenticate中间件执行,并且仅此控制器认证成功时被调用。

Let’s take a look at the signup function from line 7 to 24. At line 8, we stored the values of email, name, password, from req.body. At line 10, we wrote the code to query our User model with the email. If there already exists the user with the same email, we send the user with an error message that the user already exists (from line 16 to 18). If the user with the provided email does not exist, we have written code to create a new user at line 12. After that, we saved the user data using user.save() at line 13. Then, we wrote code to send the user the success message at line 14. If there occurs any error during the process, we would send the user with the status code of 500 and error message ( at line 22 ).

让我们看一下第7到24行的signup功能。在第8行,我们存储了来自req.bodyemailnamepassword的值。 在第10行,我们编写了代码,通过email查询User模型。 如果已经存在使用相同email发送给用户的用户,我们将向用户发送一条错误消息,提示该用户已经存在(从第16行到第18行)。 如果提供的email用户不存在,我们将在第12行编写代码以创建新用户。此后,我们在第13行使用user.save()保存用户数据。然后,编写代码以发送在第14行向用户发送成功消息。如果在此过程中发生任何错误,我们将向用户发送状态代码500和错误消息(在第22行)。

Let’s now take a look at line 26. There, we have a function called logout. Remember that it gets called when a user makes a request to the route /api/auth/logout. At line 27, we used req.logout() to log the user out. After logging the user out, we have written code to send the user the success message at line 28.

现在让我们看一下第26行。在这里,我们有一个名为logout的函数。 请记住,当用户向路由/api/auth/logout请求时,它会被调用。 在第27行,我们使用req.logout()将用户注销。 在注销用户之后,我们编写了代码以在第28行向用户发送成功消息。

Let’s now take a look at theme function at line 31. Here, we checked if the user is logged in with the help of req.user. If the req.user doesn’t exist, we have written code to send the user with the error message and a status of 403 at line 32 and 33. If the req.user exists, we send the user info (at line 35).

现在让我们看一下第31行的me函数。在这里,我们检查了用户是否在req.user的帮助下req.user 。 如果req.user不存在,我们已经编写了代码以在32和33行向用户发送错误消息以及状态为403。如果req.user存在,我们将发送用户信息(在35行) 。

From line 38 to 43, we have exported login , signup, logout and me.

从第38行到第43行,我们导出了loginsignuplogoutme

Let’s now work on passport middleware. For that, let’s create a directory called services inside of the main project directory. Then, create a file called passport.js inside of the services directory and write the following lines of code:

现在让我们研究护照中间件。 为此,让我们在主项目目录中创建一个名为services的目录 然后,在services目录内创建一个名为passport.js的文件,并编写以下代码行:

Here, The first few lines are just imports. At line 1, we imported the passport. At line 2, we imported the LocalStrategy. At line 5, we imported the User model.

在这里,前几行只是导入。 在第一行,我们进口了passport 。 在第2行,我们导入了LocalStrategy 。 在第5行,我们导入了User模型。

At line 7, we have used passport.serializeUser. The function of serializeUser is to tell the passport which data of the user should be stored in the session. That specified data will later be used by deserializeUser to get user’s information. At line 8, we specified what value should be stored in session by using done(null, user._id). With this command we are telling the passport that the session should store user’s id.

在第7行,我们使用了passport.serializeUserserializeUser的功能是告诉护照应将用户的哪些数据存储在会话中。 该指定的数据稍后将由deserializeUser以获取用户的信息。 在第8行,我们通过使用done(null, user._id)指定了应在会话中存储的值。 使用此命令,我们告诉护照会话应该存储用户的ID。

Let’s now take a look at deserializeUser at line 11. The function of deserializeUser is to receive the information about the user from the given user’s information stored in the session. Let’s take a closer look. Here, deserializUser provides us with id and done. At line 13, we have queried the User model by id and stored it in the user variable. if the user doesn’t exist, we throw new errors at line 15. If the user exists, we run done(null,user) at line 17.

现在让我们来看看deserializeUser在第11行的功能deserializeUser是接收来自存储在会话中的给定用户的信息的用户的信息。 让我们仔细看看。 在这里, deserializUser为我们提供了iddone 。 在第13行,我们通过id查询了User模型并将其存储在user变量中。 如果该user不存在,则在第15行抛出新错误。如果该用户存在,则在第17行运行done(null,user)

From line 22 to 41, we have set up the local strategy. Let’s take a closer look. With the code, we have told passport to use local strategy by creating new a new instance of LocalStrategy. At line 26, we have a field called usernameField, which takes in "email" as the value. This is used to define the username field should be "email". By default, its value is "username".

从第22行到第41行,我们制定了本地策略。 让我们仔细看看。 有了代码,我们告诉passport通过创建新的LocalStrategy实例来使用本地策略。 在第26行,我们有一个名为usernameField的字段,该字段接受"email"作为值。 这用于定义用户名字段应为"email" 。 默认情况下,其值为"username"

Let’s take a look at the function at line 27. It has email, password, done as the arguments. At line 29, we made a query of User model by email and stored it in the user variable. If the user does not exist, we run done(null, false) at line 31. After that, at line 33, we compared the password with the help of comparePassword method, which we had created in our User model and stored it in passwordMatch constant. If the passwordMatch is false, we run done(null, false), at line 35. If the password matches, we run done(null,user) at line 37. By using done(null,user) we are telling passport to create a session for this particular user. If there occurs any error during the process we run done(err), at line 39. We then exported the passport at line 45.

让我们来看看在27行它具有功能emailpassworddone作为参数。 在第29行,我们通过email查询了User模型,并将其存储在user变量中。 如果用户不存在,则在第31行运行done(null, false) 。之后,在第33行,我们使用在User模型中创建的comparePassword方法比较了password ,并将其存储在passwordMatch不变。 如果passwordMatchfalse ,则在第35行运行done(null, false) 。如果密码匹配,则在第37行运行done(null,user) 。通过使用done(null,user)告诉护照创建此特定用户的会话。 如果在此过程中发生任何错误,请在第39行运行done(err) 。然后,在第45行导出护照。

If you are still stuck about serializeUser or deserializeUser, you can see this answer on stack overflow which might give you much clear picture.

如果您仍然对serializeUserdeserializeUser感到deserializeUser ,则可以在堆栈溢出时看到此答案,这可能会给您带来很多清晰的画面。

This means we now can authenticate our users.

这意味着我们现在可以验证我们的用户。

翻译自: https://medium.com/javascript-in-plain-english/how-to-implement-passport-js-local-authentication-7617a2ef93e8

passport身份验证

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值