Read the original


JWT briefly

JWT (JSON Web Token) is an open standard based on JSON to transfer claims between network application environments. JWT declaration is generally adopted to transfer authenticated identity information between the identity provider and the server provider to obtain resources from the resource server.


Application scenarios of JWT

JWT is typically used for user login and authentication. In this scenario, once the user has logged in, the JWT is included in every subsequent request involving user permissions to verify user identity, routes, services, and access to resources.

For example, if a e-commerce sites, after a user logs in, the need to authenticate users actually has a lot of, such as a shopping cart, order page, personal center, etc., to access these pages normal logic is to verify user access and login first, if verification through, access to the page, or redirected to the login page.

Before JWT, such authentication was mostly implemented through cookies and sessions. We will compare the differences between the two methods below.


JWT contrast the cookie/session

Cookie /session process:

Due to browser requests are stateless, the existence of the cookie is to brought some status information to the server, the server receives the request, carries on the verification (is when logging in, server) sent to the browser, if the verification through the normal return a result, if verification is not through is redirected to the login page, The server compares the results stored in the session with the received information to determine whether the authentication is successful. Of course, this is just a brief description of the process.

Cookie /session problem:

It can be seen from the above that the server will bring cookies with each request after planting cookies, which wastes bandwidth. Moreover, cookies do not support cross-domain access, which is not convenient for cross-domain access with other systems. The server will use session to store the information verified by users, which wastes the server’s memory. When multiple servers want to share a session, they need to copy it.

JWT process:

When a user sends a request to bring the user information to the server, the server no longer stores the information in the session as in the past, but adds the information to the content sent by the browser through the internal key, and generates a token token with the user information and returns it to the browser using encryption algorithms such as SHA256 and RSA. When verifying all requests of the user, only the token and user information need to be sent to the server. The server signs the user information and its own key through the established algorithm, and then compares the sent signature with the generated signature. If the signature is strictly equal, the user information has not been tampered or forged and the authentication succeeds.

In the process of JWT, the server no longer needs extra memory to store user information, and only the shared key between multiple servers can enable multiple servers to have authentication ability, and also solves the problem that cookies cannot cross domains.


The structure of the JWT

The reason why JWT can be used as a standard for declaration transmission is that it has its own structure, which is not just to issue a token casually. The structure of JWT for token generation has three parts. Separated.

1, the Header

The Header contains the token type and the encryption algorithm, for example, {TYP: “JWT “, ALG: “HS256”}. HS256 refers to the SHA256 algorithm, which converts the object to Base64.

2, Payload

Payload Is a place where valid information is stored. Payload is classified into registered claims, public claims, and private claims.

(1) Declaration of registration in the standard

The following are the declarations registered in the standard, which are recommended but not mandatory.

  • Iss:jwtIssued;
  • Sub:jwtTargeted users;
  • Aud: receivejwtThe party;
  • Exp:jwtThe expiration time must be greater than the issue time, which is a number of seconds;
  • NBF: Define before what time shouldjwtIt’s not available;
  • Iat:jwtIssue time of.

Common declarations registered in the above standard are EXP and NBF.

(2) Public statement

Public declaration can add any information, generally add user information or other necessary business information, but it is not recommended to add sensitive information, because this part can be decrypted on the client, such as {“id”, username: “panda”, adress: “Beijing”} will convert this object to base64.

(3) Private declaration

Private declarations are defined by both providers and consumers. Sensitive information is generally not recommended because Base64 is symmetrically decrypted, meaning that part of the information can be classified as plaintext information.

3, Signature

The secret key is stored on the server and is not sent to anyone. Therefore, the JWT transmission mode is secure.

Finally, concatenate the three parts into a string using a., which is the token to be returned to the browser. The browser typically stores this token in localStorge for other requests that need to be authenticated by the user.

After the above description of JWT, we may not fully understand what JWT is and how to operate it. Next, we will implement a small case. For convenience, the server uses Express framework, the database uses Mongo to store user information, and the front end uses Vue. Make a login page After login, enter the order page to verify the token function.


File directory

jwt-apply
  |- jwt-client
  | |- src
  | | |- views
  | | | |- Login.vue
  | | | |- Order.vue
  | | |- App.vue
  | | |- axios.js
  | | |- main.js
  | | |- router.js
  | |- .gitignore
  | |- babel.config
  | |- package.json
  |- jwt-server
  | |- model
  | | |- user.js
  | |- app.js
  | |- config.js
  | |- jwt-simple.js
  | |- package.json


Server-side implementation

Before setting up the server, you need to install the dependencies we use. Here, you need to install the dependencies using YARN.

yarn add express body-parse mongoose jwt-simple

1. Configuration files

// File location: ~ jwt-apply/jwt-server/config.js
module.exports = {
    "db_url": "mongodb://localhost:27017/jwt".// The mongo operation automatically generates the database
    "secret": "pandashen" / / key
};
Copy the code

In the above configuration file, db_URL stores the address of the Mango database, the operation database is created automatically, and secret is the key used to generate the token.

2. Create database model

// File location: ~ jwt-apply/jwt-server/model/user.js
// Operate the database logic
const mongoose = require("mongoose");
let { db_url } = require(".. /config");

// Connect to the database with port 27017 by default
mongoose.connect(db_url, {
    useNewUrlParser: true // Remove the warning
});

// Create a skeleton Schema. The data will be stored in this skeleton format
let UserSchema = new mongoose.Schema({
    username: String.password: String
});

// Create a model
module.exports = mongoose.model("User", UserSchema);
Copy the code

We put the code of connecting database, defining database field and value type and creating data model in user.js under model folder, and export the data model to facilitate searching operation in server code.

3. Implement basic services

// File location: ~ jwt-apply/jwt-server/app.js
const express = require("express");
const bodyParser = require('body-parser');
const jwt = require("jwt-simple");
const User = require("./model/user");
let { secret } = require("./config");

// Create a server
const app = express();

/** * Set middleware */

/** * Register interface */

/** * Login interface */

/** * Verify the token interface */

// Listen on the port number
app.listen(3000);
Copy the code

Above is a basic server that introduces dependencies to ensure startup, followed by middleware to handle POST requests and middleware to implement CORS across domains.

4. Add middleware

// File location: ~ jwt-apply/jwt-server/app.js
// Set up cross-domain middleware
app.use((req, res, next) = > {
    // Allow cross-domain headers
    res.setHeader("Access-Control-Allow-Origin"."*");

    // Allow the browser to send the header
    res.setHeader("Access-Control-Allow-Headers"."Content-Type,Authorization");

    // Which request methods are allowed
    res.setHeader("Access-Control-Allow-Methods"."GET,POST,PUT,DELETE,OPTIONS");

    // If the current request is OPTIONS end, otherwise continue
    req.method === "OPTIONS" ? res.end() : next();
});

// Sets the middleware to handle the PARAMETERS of the POST request
app.use(bodyParser.json());
Copy the code

The reason why we set the middleware for processing POST request parameters is that BOTH registration and login need to use POST request. The reason why we set the cross-domain middleware is that although our project is small, it is separated from the front end and needs to use the front-end port 8080 to access the server port 3000. Therefore, the server needs to use CORS to handle cross-domain problems.

5. Register the implementation of the interface

// File location: ~ jwt-apply/jwt-server/app.js
// Register the implementation of the interface
app.post("/reg".async (req, res, next) => {
    // Get the data for the POST request
    let user = req.body;

    // Error validation
    try {
        // Add the database successfully, return the result of the add
        user = await User.create(user);

        // The registration success message is returned
        res.json({
            code: 0.data: {
                user: {
                    id: user._id,
                    username: user.username
                }
            }
        });
    } catch (e) {
        // Returns a registration failure message
        res.json({ code: 1.data: "Registration failed"}); }});Copy the code

Above, the user registration information is stored in the Mongo database, and the return value is the stored data. If the storage is successful, the information of successful registration is returned, otherwise the information of failed registration is returned.

6. Implementation of login interface

// File location: ~ jwt-apply/jwt-server/app.js
// The user can log in
app.post("/login".async (req, res, next) => {
    let user = req.body;
    try {
        // Check whether the user exists
        user = await User.findOne(user);

        if (user) {
            / / token is generated
            let token = jwt.encode({
                id: user._id,
                username: user.username,
                exp: Date.now() + 1000 * 10
            }, secret);

            res.json({
                code: 0.data: { token }
            });
        } else {
            res.json({ code: 1.data: "User does not exist"}); }}catch (e) {
        res.json({ code: 1.data: "Login failed"}); }});Copy the code

During the login, the system takes the user’s account and password into the database for serious and searching. If the account and password exist, the login succeeds and the token is returned. If the account and password do not exist, the login fails.

7. Token verification interface

// File location: ~ jwt-apply/jwt-server/app.js
// Middleware for token verification interface only
let auth = (req, res, next) = > {
    // Get request header authorization
    let authorization = req.headers["authorization"];
    // If so, obtain the token
    if (authorization) {
        let token = authorization.split("") [1];
        try {
            // Verify the token
            req.user = jwt.decode(token, secret);
            next();
        } catch (e) {
            res.status(401).send("Not Allowed"); }}else {
        res.status(401).send("Not Allowed"); }}// Users can verify whether or not they have been logged in by requesting authorization: Bearer token
app.get("/order", auth, (req, res, next) => {
    res.json({
        code: 0.data: {
            user: req.user
        }
    });
});
Copy the code

In the verification process, each time the browser will bring the token to the server through the request header authorization. The value of the request header is Bearer token, which is stipulated by JWT. The server takes out the token and decodes it using decode method, and uses try… Catch, or if decoding fails, a try… Catch, indicating that the token has expired, been tampered with, or been forged, and returns a 401 response.


Front-end implementation

We use version 3.0 vuE-CLI scaffolding to generate vUE projects and install AXIos to send requests.

yarn add global @vue/cli

yarn add axios

1. Import files

// File location: ~ jwt-apply/jwt-client/src/main.js
import Vue from "vue"
import App from "./App.vue"
import router from "./router"

// Whether it is in production mode
Vue.config.productionTip = false

new Vue({
    router,
    render: h= > h(App)
}).$mount("#app")
Copy the code

The above file is automatically generated by vue-CLI. We haven’t changed it, but we will post the main file code one by one for easy viewing.

2. Main component App

<! -- File location: &#126; jwt-apply/jwt-client/src/App.vue -->
<template>
    <div id="app">
        <div id="nav">
            <router-link to="/login">The login</router-link> |
            <router-link to="/order">The order</router-link>
        </div>
        <router-view/>
    </div>
</template>
Copy the code

In the main component, router-link corresponds to /login and/ORDER routes respectively.

3. Route configuration

// File location: ~ jwt-apply/jwt-client/src/router.js
import Vue from "vue"
import Router from "vue-router"
import Login from "./views/Login.vue"
import Order from "./views/Order.vue"

Vue.use(Router)

export default new Router({
    mode: "history".base: process.env.BASE_URL,
    routes: [{path: "/login".name: "login".component: Login
        },
        {
            path: "/order".name: "order".component: Order
        }
    ]
})
Copy the code

We defined two routes, one corresponding to the Login page and the other corresponding to the Order page, and introduced components Login and Order. There was no registration module written in the front end, and postman could be used to send the registration request to generate an account for later verification.

4. Login component

<! -- File location: &#126; jwt-apply/jwt-client/src/views/Login.vue -->
<template>
    <div class="login">The user name<input type="text" v-model="user.username">password<input type="text" v-model="user.password">
        <button @click="login">submit</button>
    </div>
</template>

<script>
import axios from ".. /axios"
export default {
    data() {
        return {
            user: {
                username: "".password: ""}}},methods: {
        login() {
            // Send a request to the login interface of the server
            axios.post('/login'.this.user).then(res= > {
                // Store the returned token to localStorage and jump to the order page
                localStorage.setItem("token", res.data.token);
                this.$router.push("/order");
            }).catch(err= > {
                // Error pop-upalert(err.data); }); }}}</script>
Copy the code

The Login component synchronizes the values of the two input boxes to data, which is used to store the account and password. When the submit button is clicked, the click event Login is triggered to send a request. After the request is successful, the returned token is stored in localStorage and the route is jumped to the order page.

5. Order component Order

<! -- File location: &#126; jwt-apply/jwt-client/src/views/Order.vue -->
<template>
    <div class="order">{{username}}</div>
</template>

<script>
import axios from ".. /axios"
export default {
    data() {
        return {
            username: ""
        }
    },
    mounted() {
        axios.get("/order").then(res= >{
            this.username = res.data.user.username;
        }).catch(err= >{ alert(err); }); }},</script>
Copy the code

The content of the Order page is “Order for XXX”. When loading the Order component is mounted, a request is sent to obtain the user name, that is, to access the server’s authentication token interface, because the Order page is a page involving authentication of users. When the request is successful, the user name is synchronized to data, otherwise an error message will pop up.

The callback to the request in the Login and Order components seems to be too simple. In fact, the return value of AXIos is wrapped around the return value of the server, storing information about the HTTP response. The request address of both interfaces is also the same server. In addition, the error processing in the server response is the processing of status 401. In the request involving authentication of user information, request header Authorization needs to be set and token is sent.

We don’t see any of this logic in the component request code, because we use the AXIos API to set up baseURL request interception and response interception. We can see that axios is not directly imported from node_modules. Instead, we introduced our own exported Axios.

6. Configure axiOS

// File location: ~ jwt-apply/jwt-client/src/axios.js
import axios from "axios";
import router from "./router";

// Set the default access address
axios.defaults.baseURL = "http://localhost:3000";

// Response interception
axios.interceptors.response.use(res= > {
    // Error executing the axios then method error callback, successfully returning the correct data
    returnres.data.code ! = =0 ? Promise.reject(res.data) : res.data;
}, res => {
    // If token authentication fails, jump back to the landing page and perform the axios then method error callback
    if (res.response.status === 401) {
        router.history.push("/login");
    }
    return Promise.reject("Not Allowed");
});

// Request interception is used to unify requests with tokens
axios.interceptors.request.use(config= > {
    // Obtain the token from localStorage
    let token = localStorage.getItem("token");

    // Set the request header if it exists
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }

    return config;
});

export default axios;
Copy the code

The first argument in AXIos is concatenated after axios.defaults.baseurl as the request address when accessing the server.

Axios. Interceptors. Response. Use in response to intercept, axios all after sending a request response to carry out the method of internal logic, the return value for the data, passed as a parameter to axios then methods return values.

Axios. Interceptors. Request. Use for the request to intercept, axios all requests will be sent to carry out the method of logic, and then sent to the server, are commonly used to set the request header.


Implementation principle of JWT-Simple module

I believe that the above process has been very clear how JWT is generated, what is the format of the token, and how to verify the token through front-end interaction. Based on these, we will further study the entire generation and verification process of the token. Let’s take a look at how the encode method of the Jwt-Simple module we used generates tokens and how the decode method validates tokens.

1. Create a module

// File location: ~ jwt-apply/jwt-server/jwt-simple.js
const crypto = require("crypto");

/** * other methods */

// Create an object
module.exports = {
    encode,
    decode
};
Copy the code

We know that there are two methods encode and decode for JWT-simple, so there are two methods on the exported object at last. Crypto needs to be used for signature using salt algorithm, so we introduce it in advance.

2. Convert strings to Base64

// File location: ~ jwt-apply/jwt-server/jwt-simple.js
// Convert the child string to Base64
function stringToBase64(str) {
    return Buffer.from(str).toString("base64");
}

// Convert Base64 to a string
function base64ToString(base64) {
    return Buffer.from(base64, "base64").toString("utf8");
}
Copy the code

The name of the method makes it easy to see the purpose and parameters, so it is put together here. In fact, the essence of the method is to convert between the two encodings, so it should be converted to Buffer before conversion.

3. Methods of generating signatures

// File location: ~ jwt-apply/jwt-server/jwt-simple.js
function createSign(str, secret) {
    // Use the salt algorithm to encrypt
    return crypto.createHmac("sha256", secret).update(str).digest("base64");
}
Copy the code

This step is to use sha256 and secret to generate signatures by adding salt algorithm. But for the convenience of writing down the encryption algorithm we use, normally we should retrieve the map corresponding to the alG value and the encryption algorithm name according to the value of the ALG field in the Header. To generate the signature using the set algorithm.

4, encode

// File location: ~ jwt-apply/jwt-server/jwt-simple.js
function encode(payload, secret) {
    / / to the head
    let header = stringToBase64(JSON.stringify({
        typ: "JWT".alg: "HS256"
    }));

    / / load
    let content = stringToBase64(JSON.stringify(payload));

    / / signature
    let sign = createSign([header, content].join("."), secret);

    // Generate a signature
    return [header, content, sign].join(".");
}
Copy the code

Convert Header and Payload into base64. Then use the secret key to generate a signature. Finally, pass the base64 of Header and Payload. And the generated signature, which forms a “plaintext” + “plaintext” + “cryptic” three-paragraph token.

5, decode

// File location: ~ jwt-apply/jwt-server/jwt-simple.js
function decode(token, secret) {
    let [header, content, sign] = token.split(".");

    // Re-sign and validate the first two parts of the received token (base64) without throwing an error
    if(sign ! == createSign([header, content].join("."), secret)) {
        throw new Error("Not Allow");
    }

    // Convert content to an object
    content = JSON.parse(base64ToString(content));

    // Detect expiration time, if the past throws an error
    if (content.exp && content.exp < Date.now()) {
        throw new Error("Not Allow");
    }

    return content;
}
Copy the code

In decode, the first two sections of the token are extracted respectively, and the signature is generated again with the first two sections, and compared with the third section of the token. If they are the same, they pass the verification, but if they are different, they are tampered with and throw an error. The content of the token is converted into an object, that is, the content is converted into an object. Extract exp field against current time to verify expiration, throw error if expired.


conclusion

In the token generated by JWT, the first two paragraphs can be understood in plaintext, so that others can know our encryption algorithm and rules and the information we transmit after interception. They can also use JWT-Simple to encrypt a paragraph of cryptic text and spliced it into token format for the server to verify. Why is JWT still so secure? No matter how much other people know about the information we are transmitting, we can’t get the authentication of the server after tampering or forging it, because we can’t get the secret of the server. The secret is the real security guarantee, and the Header and Payload are not secure. It can be hacked, so it can’t store sensitive information.