当前位置: 首页 > 工具软件 > jwt-node-vue > 使用案例 >

vue使用jwt加密_如何使用Vue和Node构建轻量级发票应用程序:JWT身份验证和发送发票

阚英睿
2023-12-01

vue使用jwt加密

介绍 (Introduction)

In the previous parts of the series, we looked at how to create the User Interface of our Invoicing Application that allowed users to create and view existing invoices. In this final part of the series, you will set up persisting user sessions on the client and configure a single view for invoices.

在本系列的前几部分中,我们研究了如何创建发票应用程序的用户界面,该界面允许用户创建和查看现有发票。 在本系列的最后一部分中,您将在客户端上设置持久性用户会话,并为发票配置单个视图。

先决条件 (Prerequisites)

To follow this article adequately, you need the following:

要充分了解本文,您需要满足以下条件:

  • Node installed on your machine.

    节点已安装在您的计算机上。
  • NPM installed on your machine.

    NPM安装在您的计算机上。
  • To have read the first and second parts of this series.

    阅读了本系列的第一部分和第二部分。

To confirm your installation, run the following command:

要确认安装,请运行以下命令:

  • node --version

    节点-版本
  • npm --version

    npm --version

If you get their version numbers as results then you’re good to go.

如果您得到它们的版本号作为结果,那么您就很好了。

第1步-使用JWToken在客户端上保留用户会话 (Step 1 — Persisting User Sessions on Client Using JWTokens)

To verify that our application is secure and only authorized users can make requests, we are going to make use of JWTokens. JWTokens, or JSON Web Tokens consist of a three-part string containing the header, payload, and signature of the request. The core idea of it is to create a token for each authenticated user to use when performing requests to the backend server.

为了验证我们的应用程序是否安全并且只有授权用户可以发出请求,我们将使用JWTokens。 JWTokens或JSON Web令牌由三部分组成的字符串组成,其中包含请求的标头,有效负载和签名。 它的核心思想是为每个经过身份验证的用户创建一个令牌,以在执行对后端服务器的请求时使用。

To get started, change into the invoicing-app directory. After doing that, install the jsonwebtoken node module that will be used to create and verify our JSON Web Tokens:

首先,请转到invoicing-app目录。 之后,安装jsonwebtoken节点模块,该模块将用于创建和验证我们的JSON Web令牌:

cd invoicing-app 
npm install jsonwebtoken nodemon --save

nodemon is a node module that restarts your server once file changes occur.

nodemon是一个节点模块,一旦发生文件更改,该模块就会重新启动服务器。

Now, update the server.js file by adding the following:

现在,通过添加以下内容来更新server.js文件:

server.js
server.js
// import node modules
    [...]
    const jwt = require("jsonwebtoken");

    // create express app
    [...]
    app.set('appSecret', 'secretforinvoicingapp'); // this will be used later

Next thing to do is to tweak the /register and /login routes to create tokens and pass them back once a user has successfully registered or logged in. To do this, add the following to your server.js file:

接下来要做的是调整/register/login路由以创建令牌,并在用户成功注册或登录后将它们传递回。为此,请将以下内容添加到server.js文件中:

server.js
server.js
// edit the /register route
    app.post("/register", multipartMiddleware, function(req, res) {
      // check to make sure none of the fields are empty
      [...]
      bcrypt.hash(req.body.password, saltRounds, function(err, hash) {
        // create sql query 
        [...]
        db.run(sql, function(err) {
          if (err) {
            throw err;
          } else {
            let user_id = this.lastID;
            let query = `SELECT * FROM users WHERE id='${user_id}'`;
            db.all(query, [], (err, rows) => {
              if (err) {
                throw err;
              }
              let user = rows[0];
              delete user.password;
              //  create payload for JWT
              const payload = {
                user: user 
              }
              // create token
              let token = jwt.sign(payload, app.get("appSecret"), {
                expiresInMinutes: "24h" // expires in 24 hours
              });
              // send response back to client
              return res.json({
                status: true,
                token : token
              });
            });
          }
        });
        db.close();
      });
    });

    [...]

Do the same for the /login route:

/login路径执行相同的操作:

server.js
server.js
app.post("/login", multipartMiddleware, function(req, res) {
      //  connect to db 
      [...]
      db.all(sql, [], (err, rows) => {
        // attempt to authenticate the user
        [...]
        if (authenticated) {
          //  create payload for JWT
          const payload = { user: user };
          // create token
          let token = jwt.sign( payload, app.get("appSecret"),{
            expiresIn: "24h" // expires in 24 hours
          });
          return res.json({
            status: true,
            token: token
          });
        }

        return res.json({
          status: false,
          message: "Wrong Password, please retry"
        });
      });
    });

Now that this is done, the next thing to do is to test it. Run your server using the following command:

既然完成了,接下来要做的就是对其进行测试。 使用以下命令运行服务器:

  • nodemon server.js

    nodemon server.js

Your app will now create tokens on successful logins and registrations. The next step is to verify tokens for incoming requests. To do this, add the following middleware above the routes you want to protect:

您的应用现在将在成功登录和注册后创建令牌。 下一步是验证传入请求的令牌。 为此,请在要保护的路由上方添加以下中间件:

server.js
server.js
[...]
    // unprotected routes

    [...]
    // Create middleware for protecting routes
    app.use(function(req, res, next) {
      // check header or url parameters or post parameters for token
      let token =
        req.body.token || req.query.token || req.headers["x-access-token"];
      // decode token
      if (token) {
        // verifies secret and checks exp
        jwt.verify(token, app.get("appSecret"), function(err, decoded) {
          if (err) {
            return res.json({
              success: false,
              message: "Failed to authenticate token."
            });
          } else {
            // if everything is good, save to request for use in other routes
            req.decoded = decoded;
            next();
          }
        });
      } else {
        // if there is no token
        // return an error
        return res.status(403).send({
          success: false,
          message: "No token provided."
        });
      }
    });

    // protected routes 
    [...]

In the SignUp.vue file you need to store the token obtained from the server and the user data in the localStorage so that it can persist across different pages when the user is using your application. To do this, update the login and register methods of your frontend/src/components/SignUp.vue file to look like this:

SignUp.vue文件中,您需要将从服务器获取的令牌和用户数据存储在localStorage以便在用户使用您的应用程序时,该令牌可以在不同页面上持久保存。 为此,请更新frontend/src/components/SignUp.vue文件的loginregister方法,如下所示:

frontend/src/components/SignUp.vue
frontend / src / components / SignUp.vue
[...]
    export default {
      name: "SignUp",
      [...]
      methods:{
        register(){
          const formData = new FormData();
          let valid = this.validate();
          if(valid){
            // prepare formData
            [...]
            // Post to server
            axios.post("http://localhost:3128/register", formData)
            .then(res => {
              // Post a status message
              this.loading = "";
              if (res.data.status == true) {
                // store the user token and user data in localStorage
                localStorage.setItem('token', res.data.token);
                localStorage.setItem('user', JSON.stringify(res.data.user));
                // now send the user to the next route
                this.$router.push({
                  name: "Dashboard",
                });
              } else {
                this.status = res.data.message;
              }
            });
          }
          else{
            alert("Passwords do not match");
          }
        }
        [...]

Let’s also update the login method:

让我们还更新登录方法:

frontend/src/components/SignUp.vue
frontend / src / components / SignUp.vue
login() {
          const formData = new FormData();
          formData.append("email", this.model.email);
          formData.append("password", this.model.password);
          this.loading = "Signing in";
          // Post to server
          axios.post("http://localhost:3128/login", formData).then(res => {
            // Post a status message
            console.log(res);
            this.loading = "";
            if (res.data.status == true) {
              // store the data in localStorage
              localStorage.setItem("token", res.data.token);
              localStorage.setItem("user", JSON.stringify(res.data.user));
              // now send the user to the next route
              this.$router.push({
                name: "Dashboard"
              });
            } else {
              this.status = res.data.message;
            }
          });

Previously, user data was passed using route parameters, but now the app gets the data from the local storage. Let’s see how this changes our components.

以前,用户数据是使用route参数传递的,但是现在该应用程序从本地存储中获取数据。 让我们看看这如何改变我们的组件。

The Dashboard component previously looked like this:

Dashboard组件以前看起来像这样:

frontend/src/components/Dashboard.vue
frontend / src / components / Dashboard.vue
<script>
    import Header from "./Header";
    import CreateInvoice from "./CreateInvoice";
    import ViewInvoices from "./ViewInvoices";
    export default {
      name: "Dashboard",
      components: {
        Header,
        CreateInvoice,
        ViewInvoices,
      },
      data() {
        return {
          isactive: 'create',
          title: "Invoicing App",
          user : (this.$route.params.user) ? this.$route.params.user : null
        };
      }
    };
    </script>

This meant that when a user signed in or registered that they were redirected to the Dashboard page, and then the user property of the Dashboard component was updated accordingly. If the user had decided to refresh the page, there would be no way to identify the user since this.$route.params.user no longer exists.

这意味着,当用户登录或注册时,他们将被重定向到“ Dashboard页面,然后相应地更新了Dashboard组件的user属性。 如果用户决定刷新页面,将无法识别用户,因为this.$route.params.user不再存在。

Edit your Dashboard component to now use the browser’s localStorage:

编辑您的Dashboard组件,以现在使用浏览器的localStorage

frontend/src/components/Dashboard.vue
frontend / src / components / Dashboard.vue
import Header from "./Header";
    import CreateInvoice from "./CreateInvoice";
    import ViewInvoices from "./ViewInvoices";
    export default {
      name: "Dashboard",
      components: {
        Header,
        CreateInvoice,
        ViewInvoices,
      },
      data() {
        return {
          isactive: 'create',
          title: "Invoicing App",
          user : null,
        };
      },
      mounted(){
        this.user = JSON.parse(localStorage.getItem("user"));
      }
    };

Now the user data will persist after refreshing the page. When requests are being made, you also have to add the token to the requests.

现在,用户数据将在刷新页面后保留。 发出请求时,还必须将令牌添加到请求中。

Take a look at the ViewInvoices component. Here’s what the JavaScript for the component looks like:

看一下ViewInvoices组件。 组件JavaScript如下所示:

frontend/src/components/ViewInvoices.vue
frontend / src / components / ViewInvoices.vue
<script>
    import axios from "axios";
    export default {
      name: "ViewInvoices",
      components: {},
      data() {
        return {
          invoices: [],
\          user: '',
        };
      },
      mounted() {
        this.user = JSON.parse(localStorage.getItem('user'));
        axios
          .get(`http://localhost:3128/invoice/user/${this.user.id}`)
          .then(res => {
            if (res.data.status == true) {
              console.log(res.data.invoices);
              this.invoices = res.data.invoices;
            }
          });
      }
    };
    </script>

If you currently attempt to view invoices for a logged in user, you will get an error when retrieving invoices due to an absence of tokens.

如果当前尝试查看已登录用户的发票,则由于缺少令牌,因此在检索发票时会出现错误。

This is because the invoice/user/:user_id route of the application is now protected with the token middleware that you set up earlier. Add it to the request to fix this error:

这是因为应用程序的invoice/user/:user_id路由现在受到您之前设置的令牌中间件的保护。 将其添加到请求中以解决此错误:

frontend/src/components/ViewInvoices.vue
frontend / src / components / ViewInvoices.vue
<script>
    import axios from "axios";
    export default {
      name: "ViewInvoices",
      components: {},
      data() {
        return {
          invoices: [],
          user: '',
        };
      },
      mounted() {
        this.user = JSON.parse(localStorage.getItem('user'));
        axios
          .get(`http://localhost:3128/invoice/user/${this.user.id}`,
            {
              headers: {"x-access-token": localStorage.getItem("token")}
            }
          )
          .then(res => {
            if (res.data.status == true) {
              console.log(res.data.invoices);
              this.invoices = res.data.invoices;
            }
          });
      }
    };
    </script>

When you save this and go back to your browser, you can now fetch the invoices successfully:

保存并返回浏览器后,您现在可以成功获取发票了:

第2步-为发票创建单一视图 (Step 2 — Creating a Single View for Invoices)

When the TO INVOICE button is clicked nothing happens. To fix this, create a SingleInvoice.vue file and edit it as follows:

单击“ TO INVOICE”按钮时,没有任何React。 要解决此问题,请创建SingleInvoice.vue文件,并按如下所示进行编辑:

<template>
      <div class="single-page">
        <Header v-bind:user="user"/>
        <!--  display invoice data -->
        <div class="invoice">
          <!-- display invoice name here -->
          <div class="container">
            <div class="row">
                <div class="col-md-12">
                  <h3>Invoice #{{ invoice.id }} by {{ user.company_name }}</h3>
                  <table class="table">
                    <thead>
                      <tr>
                        <th scope="col">#</th>
                        <th scope="col">Transaction Name</th>
                        <th scope="col">Price ($)</th>
                      </tr>
                    </thead>
                    <tbody>
                      <template v-for="txn in transactions">
                        <tr :key="txn.id">
                          <th>{{ txn.id }}</th>
                          <td>{{ txn.name }}</td>
                          <td>{{ txn.price }} </td>
                        </tr>
                      </template>
                    </tbody>
                    <tfoot>
                      <td></td>
                      <td style="text-align: right">Total :</td>
                      <td><strong>$ {{ total_price }}</strong></td>
                    </tfoot>
                  </table>
                </div>
              </div>
            </div>
          </div>
      </div>
    </template>

The v-for directive is used to allow you to loop through all the fetched transactions for the particular invoice.

v-for指令用于允许您遍历特定发票的所有提取的交易。

The component structure can be seen below. You first import the necessary modules and components. When the component is mounted, a POST request using axios is made to the backend server to fetch the data. When the response is obtained, we assign them to the respective component properties.

组件结构如下所示。 首先,导入必要的模块和组件。 mounted组件后,将使用axios后端服务器发出POST请求以获取数据。 获得响应后,我们将它们分配给相应的组件属性。

<script>
    import Header from "./Header";
    import axios from "axios";
    export default {
      name: "SingleInvoice",
      components: {
        Header
      },
      data() {
        return {
          invoice: {},
          transactions: [],
          user: "",
          total_price: 0
        };
      },
      methods: {
        send() {}
      },
      mounted() {
        // make request to fetch invoice data
        this.user = JSON.parse(localStorage.getItem("user"));
        let token = localStorage.getItem("token");
        let invoice_id = this.$route.params.invoice_id;
        axios
          .get(`http://localhost:3128/invoice/user/${this.user.id}/${invoice_id}`, {
            headers: {
              "x-access-token": token
            }
          })
          .then(res => {
            if (res.data.status == true) {
              this.transactions = res.data.transactions;
              this.invoice = res.data.invoice;
              let total = 0;
              this.transactions.forEach(element => {
                total += parseInt(element.price);
              });
              this.total_price = total;
            }
          });
      }
    };
    </script>

Note: There’s a send() method that is currently empty. As you move on through the article, you will get a better understanding as to why and how to add the necessary functionality.

注意:目前有一个send()方法为空。 在继续阅读本文时,您将更好地理解为什么以及如何添加必要的功能。

The component has the following scoped styles:

该组件具有以下作用域样式:

frontend/src/components/SingleInvoice.vue
frontend / src / components / SingleInvoice.vue
<!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped>
    h1,
    h2 {
      font-weight: normal;
    }
    ul {
      list-style-type: none;
      padding: 0;
    }
    li {
      display: inline-block;
      margin: 0 10px;
    }
    a {
      color: #426cb9;
    }
    .single-page {
      background-color: #ffffffe5;
    }
    .invoice{
      margin-top: 20px;
    }
    </style>

Now, if you back to the application and click the TO INVOICE button in the View Invoices tab, you will see the single invoice view.

现在,如果你回到应用程序,并单击该发票按钮View Invoices选项卡,你会看到一张发票视图。

步骤3 —通过电子邮件发送发票 (Step 3 — Sending Invoice via Email)

This is the final step of the invoicing application is to allow your users to send invoices. In this step, you will use the nodemailer module to send emails to specified recipients on the backend server. To get started, first install the module:

开票应用程序的最后一步是允许您的用户发送发票。 在此步骤中,您将使用nodemailer模块将电子邮件发送到后端服务器上的指定收件人。 首先,请先安装模块:

  • npm install nodemailer

    npm安装nodemailer

Now that the module is installed, update the server.js file as follows:

现在已经安装了该模块,如下更新server.js文件:

server.js
server.js
// import node modules
    [...]
    let nodemailer = require('nodemailer')

    // create mail transporter
    let transporter = nodemailer.createTransport({
      service: 'gmail',
      auth: {
        user: 'COMPANYEMAIL@gmail.com',
        pass: 'userpass'
      }
    });

    // create express app
    [...]

This email will be set on the backend server and will be the account sending emails on behalf of the user. Also, you will need to temporarily allow non-secure sign-in for your Gmail account for testing purposes in your security settings.

该电子邮件将在后端服务器上设置,并将成为代表用户发送电子邮件的帐户。 另外,您还需要暂时允许Gmail帐户进行非安全登录,以在安全性设置中进行测试。

server.js
server.js
// configure app routes
    [...]
    app.post("/sendmail", multipartMiddleware, function(req, res) {
      // get name  and email of sender
      let sender = JSON.parse(req.body.user);
      let recipient = JSON.parse(req.body.recipient);
      let mailOptions = {
        from: "COMPANYEMAIL@gmail.com",
        to: recipient.email,
        subject: `Hi, ${recipient.name}. Here's an Invoice from ${
          sender.company_name
        }`,
        text: `You owe ${sender.company_name}`
      };
      transporter.sendMail(mailOptions, function(error, info) {
        if (error) {
          return res.json({
            status: 200,
            message: `Error sending main to ${recipient.name}`
          });
        } else {
          return res.json({
            status: 200,
            message: `Email sent to ${recipient.name}`
          });
        }
      });
    });

At this point, you have configured the emails to work when a POST request is made to the /sendmail route. You also need to allow the user to perform this action on the frontend and give them a form to enter in the recipient’s email address. To do this, update the SingleInvoice component by doing the following:

至此,您已经将电子邮件配置为在对/sendmail路由发出POST请求时可以正常工作。 您还需要允许用户在前端上执行此操作,并为他们提供一个表单以输入收件人的电子邮件地址。 为此,请执行以下操作来更新SingleInvoice组件:

frontend/src/components/SingleInvoice.vue
frontend / src / components / SingleInvoice.vue
<template>
     <Header v-bind:user="user"/>
        <!--  display invoice data -->
        <div class="invoice">
          <!-- display invoice name here -->
          <div class="container">
            <div class="row">
              <div class="col-md-12">
                // display invoice
              </div>
            </div>
            <div class="row">
              <form @submit.prevent="send" class="col-md-12">
                <h3>Enter Recipient's Name and Email to Send Invoice</h3>
                <div class="form-group">
                  <label for="">Recipient Name</label>
                  <input type="text" required class="form-control" placeholder="eg Chris" v-model="recipient.name">
                </div>
                <div class="form-group">
                  <label for="">Recipient Email</label>
                  <input type="email" required placeholder="eg chris@invoiceapp.com" class="form-control" v-model="recipient.email">
                </div>
                <div class="form-group">
                    <button class="btn btn-primary" >Send Invoice</button>
                    {{ loading }}
                    {{ status }}
                </div>
              </form>
            </div>
          </div>
        </div> 
    </template>

Also, the component properties are updated as follows:

此外,组件属性更新如下:

frontend/src/components/SingleInvoice.vue
frontend / src / components / SingleInvoice.vue
<script>
    import Header from "./Header";
    import axios from "axios";
    export default {
      name: "SingleInvoice",
      components: {
        Header
      },
      data() {
        return {
          invoice: {},
          transactions: [],
          user: '',
          total_price: 0,
          recipient : {
            name: '',
            email: ''
          },
          loading : '',
          status: '',
        };
      },
      methods: {
        send() {
          this.status = "";
          this.loading = "Sending Invoice, please wait....";
          const formData = new FormData();
          formData.append("user", JSON.stringify(this.user));
          formData.append("recipient", JSON.stringify(this.recipient));
          axios.post("http://localhost:3128/sendmail", formData, {
            headers: {"x-access-token": localStorage.getItem("token")}
          }).then(res => {
            this.loading = '';
            this.status = res.data.message
          }); 
        }
      },
      mounted() {
        // make request to fetch invoice data
      }
    };
    </script>

After making these changes, your users will be able to enter in a recipient email and receive a “Sent Invoice” notification from the app.

进行这些更改之后,您的用户将能够输入收件人电子邮件,并从应用程序接收“已发送发票”通知。

You can further edit the email by reviewing the nodemailer guide.

您可以通过查看nodemailer指南进一步编辑电子邮件。

结论 (Conclusion)

In this part of the series, we looked at how to use JWTokens and the Browser’s Local Storage to keep users signed in. We also created the view for a single invoice.

在本系列的这一部分中,我们研究了如何使用JWToken和浏览器的本地存储来保持用户登录。我们还为单个发票创建了视图。

翻译自: https://www.digitalocean.com/community/tutorials/how-to-build-a-lightweight-invoicing-app-with-vue-and-node-jwt-authentication-and-sending-invoices

vue使用jwt加密

 类似资料: