Понимание JWT на примере

внешний интерфейс сервер алгоритм JavaScript Express Vue.js vue-router


читать оригинал


Краткое описание JWT

JWT (веб-токен json) основан наjsonВ качестве открытого стандарта утверждения JWT обычно используются для передачи аутентифицированной идентификационной информации между поставщиками удостоверений и поставщиками серверов для получения ресурсов от серверов ресурсов.


Сценарии применения JWT

JWT обычно используется для входа пользователя в систему.В этом сценарии, после завершения входа пользователя, JWT включается в каждый последующий запрос, включающий разрешения пользователя, которые могут получить доступ к идентификатору пользователя, маршрутизации, службам и ресурсам.

Например, если на веб-сайте электронной коммерции после входа пользователя в систему есть много мест, где необходимо подтвердить пользователя, например, корзина покупок, страница заказа, персональный центр и т. д. Обычная логика доступа к этим страницам заключается в следующем: сначала проверьте права пользователя и статус входа в систему. , если проверка пройдена, войдите на посещенную страницу, в противном случае перенаправьте на страницу входа.

До JWT мы в основном проходили такую ​​проверкуcookieа такжеsessionЧтобы достичь этого, мы сравним различия между следующими двумя методами.


JWT против файла cookie/сеанса

Процесс cookie/сеанса:

Поскольку запросы браузера не имеют состояния,cookieСуществование его заключается в том, чтобы донести на сервер некоторую информацию о статусе.Сервер проверит ее, когда получит запрос (на самом деле она отправляется браузеру сервером при входе в систему).Если проверка пройдет, то результат будет возвращаться в обычном режиме. Если проверка не удалась, то перенаправляет на страницу входа, в то время как сервер основан наsessionСравните результаты, хранящиеся на сервере, с полученной информацией, чтобы определить, пройдена ли проверка.Конечно, это лишь краткое описание процесса.

Проблемы с куки/сессиями:

Из вышесказанного видно, что сервер посаженcookieПосле каждого запроса будет приноситьcookie, расходует пропускную способность иcookieМеждоменный доступ не поддерживается, неудобен для междоменного доступа с другими системами, и сервер будет использоватьsessionдля хранения этой информации аутентификации пользователя, которая тратит впустую память сервера, когда несколько серверов хотят поделитьсяsessionСкопируйте все, что нужно.

Процесс JWT:

Когда пользователь отправляет запрос на передачу информации о пользователе на сервер, сервер больше не сохраняет ее вsessionВместо этого контент, отправленный браузером, добавляется к информации через внутренний ключ, используяsha256а такжеRSAподождите, пока алгоритм шифрования сгенерируетtokenТокен возвращается в браузер вместе с информацией о пользователе.Все запросы, касающиеся аутентификации пользователя, должны использовать только этотtokenи информация о пользователе отправляется на сервер, и сервер подписывает информацию о пользователе и свой ключ по заранее определенному алгоритму, а затем сравнивает отправленную подпись с сгенерированной подписью.Строгое равенство означает, что информация о пользователе не была подделана или подделана , Подтвердите проход.

В процессе JWT серверу больше не требуется дополнительная память для хранения информации о пользователе, и ему нужно только поделиться ключом с несколькими серверами, чтобы несколько серверов могли иметь возможности проверки, а также решить проблему.cookieНе удается пересечь доменную проблему.


Структура JWT

Причина, по которой JWT может быть принят в качестве декларативного стандарта, заключается в том, что он имеет свою собственную структуру, и это не случайная проблема.tokenПросто отлично, JWT используется для генерацииtokenСтруктура состоит из трех частей, использующих.разделены.

1. Заголовок

HeaderГолова в основном состоит из двух частей,tokenтип и алгоритм шифрования, такие как{typ: "jwt", alg: "HS256"},HS256означаетsha256алгоритм, который преобразует этот объект вbase64.

2. Полезная нагрузка

PayloadПолезная нагрузка — это место, где хранится действительная информация, а действительная информация делится на утверждения, зарегистрированные в стандарте, общедоступные утверждения и частные утверждения.

(1) Заявления, зарегистрированные в стандарте

Ниже приведены объявления, зарегистрированные в стандарте, и их использование рекомендуется, но не обязательно.

  • исс:jwtэмитент;
  • суб:jwtпредполагаемые пользователи;
  • ауд: получитьjwtвечеринка;
  • эксп:jwtВремя истечения должно быть больше, чем время выдачи, которое равно количеству секунд;
  • nbf: определить, до какого времениjwtнедоступны;
  • IAT:jwtвремя выдачи.

Обычно используемые среди деклараций, зарегистрированных в вышеуказанном стандарте,expа такжеnbf.

(2) Публичное заявление

Публичное заявление может добавлять любую информацию, как правило, добавлять информацию, связанную с пользователем, или другую необходимую информацию для нужд бизнеса, но не рекомендуется добавлять конфиденциальную информацию, потому что эта часть может быть расшифрована на стороне клиента, например{"id", username: "panda", adress: "Beijing"}, который преобразует этот объект вbase64.

(3) Частное заявление

Частные объявления — это объявления, совместно определенные поставщиками и потребителями, и обычно не рекомендуется хранить конфиденциальную информацию, посколькуbase64Это симметричное дешифрование, что означает, что эта часть информации может быть классифицирована как информация открытого текста.

3. Подпись

SignatureЭта часть относится кHeaderа такжеPayloadпо ключуsecretПодпись, созданная после шифрования с помощью алгоритма соления,secret, ключ хранится на сервере и никому не будет отправлен, поэтому метод передачи JWT очень безопасен.

Наконец, три части будут использоваться.Объединяется в строку, которая должна быть возвращена в браузерtokenБраузеры обычно используют этоtokenсохранить вlocalStorgeДля других запросов, требующих аутентификации пользователя.

После вышеприведенного описания JWT вы еще можете не до конца понять, что такое JWT и как с ним работать.Далее реализуем небольшой кейс.Для удобства сервер используетexpressфреймворк, использование базы данныхmongoДля хранения информации о пользователе интерфейс используетVueДля этого создайте страницу входа, чтобы войти в систему, и войдите на страницу заказа, чтобы подтвердитьtokenфункция.


Каталог файлов

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

реализация на стороне сервера

Перед сборкой сервера нам нужно установить зависимости, которые мы используем, здесь мы используемyarnДля установки команда следующая.

yarn add express body-parse mongoose jwt-simple

1. Файл конфигурации

// 文件位置:~jwt-apply/jwt-server/config.js
module.exports = {
    "db_url": "mongodb://localhost:27017/jwt", // 操作 mongo 自动生成这个数据库
    "secret": "pandashen" // 密钥
};

В приведенном выше файле конфигурацииdb_urlхранитсяmangoАдрес базы данных, оперативная база данных создается автоматически,secretиспользуется для созданияtokenключ.

2. Создайте модель базы данных

// 文件位置:~jwt-apply/jwt-server/model/user.js
// 操作数据库的逻辑
const mongoose = require("mongoose");
let { db_url } = require("../config");

// 连接数据库,端口默认 27017
mongoose.connect(db_url, {
    useNewUrlParser: true // 去掉警告
});

// 创建一个骨架 Schema,数据会按照这个骨架格式存储
let UserSchema = new mongoose.Schema({
    username: String,
    password: String
});

// 创建一个模型
module.exports = mongoose.model("User", UserSchema);

Мы помещаем код для подключения к базе данных, определения полей и типов значений базы данных и создания модели данных в одном месте.modelв папкеuser.jsСреди них модель данных экспортируется для облегчения операции поиска в коде сервера.

3. Внедрить базовые услуги

// 文件位置:~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");

// 创建服务器
const app = express();

/**
* 设置中间件
*/

/**
* 注册接口
*/

/**
* 登录接口
*/

/**
* 验证 token 接口
*/

// 监听端口号
app.listen(3000);

Выше приведен базовый сервер, который вводит соответствующие зависимости для обеспечения запуска, а затем добавляет обработкуpostЗапросить промежуточное ПО и реализациюcorsМеждоменное промежуточное ПО.

4. Добавьте промежуточное ПО

// 文件位置:~jwt-apply/jwt-server/app.js
// 设置跨域中间件
app.use((req, res, next) => {
    // 允许跨域的头
    res.setHeader("Access-Control-Allow-Origin", "*");

    // 允许浏览器发送的头
    res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");

    // 允许哪些请求方法
    res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");

    // 如果当前请求是 OPTIONS 直接结束,否则继续执行
    req.method === "OPTIONS" ? res.end() : next();
});

// 设置处理 post 请求参数的中间件
app.use(bodyParser.json());

Зачем устанавливать обработкуpostПромежуточное программное обеспечение параметра запроса связано с тем, что необходимо использовать как регистрацию, так и вход в систему.postЗапрос, причина, по которой мы устанавливаем промежуточное программное обеспечение для перекрестных домен, заключается в том, что наш проект невелик, а передние и задние концы разделены, поэтому нам нужно использовать интерфейс8080Сервер доступа к портам3000порт, поэтому он должен использоваться серверомcorsРешайте междоменные проблемы.

5. Реализация интерфейса регистрации

// 文件位置:~jwt-apply/jwt-server/app.js
// 注册接口的实现
app.post("/reg", async (req, res, next) => {
    // 获取 post 请求的数据
    let user = req.body;

    // 错误验证
    try {
        // 存入数据库,添加成功后返回的就是添加后的结果
        user = await User.create(user);

        // 返回注册成功的信息
        res.json({
            code: 0,
            data: {
                user: {
                    id: user._id,
                    username: user.username
                }
            }
        });
    } catch (e) {
        // 返回注册失败的信息
        res.json({ code: 1, data: "注册失败" });
    }
});

Регистрационная информация пользователя хранится в вышеуказанномmongoБаза данных, возвращаемое значение — это сохраненные данные.Если сохранение прошло успешно, возвращается информация об успешной регистрации, в противном случае возвращается информация об ошибке регистрации.

6. Реализация интерфейса входа

// 文件位置:~jwt-apply/jwt-server/app.js
// 用户能登录
app.post("/login", async (req, res, next) => {
    let user = req.body;
    try {
        // 查找用户是否存在
        user = await User.findOne(user);

        if (user) {
            // 生成 token
            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: "用户不存在" });
        }
    } catch (e) {
        res.json({ code: 1, data: "登录失败" });
    }
});

Во время процесса входа учетная запись и пароль пользователя будут введены в базу данных для серьезного и поиска.Если он существует, вход будет успешным, и возврат будет возвращен.token, если он не существует, вход невозможен.

7. Интерфейс проверки токена

// 文件位置:~jwt-apply/jwt-server/app.js
// 只针对 token 校验接口的中间件
let auth = (req, res, next) => {
    // 获取请求头 authorization
    let authorization = req.headers["authorization"];
    // 如果存在,则获取 token
    if (authorization) {
        let token = authorization.split(" ")[1];
        try {
            // 对 token 进行校验
            req.user = jwt.decode(token, secret);
            next();
        } catch (e) {
            res.status(401).send("Not Allowed");
        }
    } else {
        res.status(401).send("Not Allowed");
    }
}

// 用户可以校验是否登录过,通过请求头 authorization: Bearer token
app.get("/order", auth, (req, res, next) => {
    res.json({
        code: 0,
        data: {
            user: req.user
        }
    });
});

В процессе проверки каждый раз, когда браузер будетtokenчерез заголовок запросаauthorizationПринесите его на сервер, значение заголовка запроса равноBearer token, указанный JWT, сервер получаетtokenиспользоватьdecodeметод декодирования и использованияtry...catchСделайте захват, который сработает, если декодирование не удастсяtry...catch, проиллюстрироватьtokenпросроченный, подделанный или поддельный, возврат401отклик.


реализация интерфейса

Мы используем3.0версияvue-cliГенерация строительных лесовVueспроектировать и установитьaxiosпослать запрос.

yarn add global @vue/cli

yarn add axios

1. Входной файл

// 文件位置:~jwt-apply/jwt-client/src/main.js
import Vue from "vue"
import App from "./App.vue"
import router from "./router"

// 是否为生产模式
Vue.config.productionTip = false

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

Вышеупомянутый файлvue-cliАвтоматически сгенерировано, мы не внесли изменения, но для удобства мы рассмотрим код одиннадцати основных файлов.

2. Приложение основного компонента

<!-- 文件位置:&#126;jwt-apply/jwt-client/src/App.vue -->
<template>
    <div id="app">
        <div id="nav">
            <router-link to="/login">登录</router-link> |
            <router-link to="/order">订单</router-link>
        </div>
        <router-view/>
    </div>
</template>

В основном компоненте мы будемrouter-linkсоответствующий/loginа также/orderдва маршрута.

3. Конфигурация маршрутизации

// 文件位置:&#126;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
        }
    ]
})

Мы определили два маршрута, один для страницы входа и один для страницы заказа, и ввели компонентыLoginа такжеOrder, фронтенд не пишет модуль регистрации, можно использоватьpostmanОтправьте запрос на регистрацию, чтобы создать учетную запись для последующей проверки.

4. Компонент входа в систему

<!-- 文件位置:&#126;jwt-apply/jwt-client/src/views/Login.vue -->
<template>
    <div class="login">
        用户名
        <input type="text" v-model="user.username">
        密码
        <input type="text" v-model="user.password">
        <button @click="login">提交</button>
    </div>
</template>

<script>
import axios from "../axios"
export default {
    data() {
        return {
            user: {
                username: "",
                password: ""
            }
        }
    },
    methods: {
        login() {
            // 发送请求访问服务器的登录接口
            axios.post('/login', this.user).then(res => {
                // 将返回的 token 存入 localStorage,并跳转订单页
                localStorage.setItem("token", res.data.token);
                this.$router.push("/order");
            }).catch(err => {
                // 弹出错误
                alert(err.data);
            });
        }
    }
}
</script>

LoginКомпонент синхронизирует значения двух полей ввода сdata, используется для хранения учетной записи и пароля, при нажатии кнопки отправки запускается событие clickloginОтправить запрос, который будет возвращен после успешного выполнения запросаtokenдепозитlocalStorage, и перейти на страницу заказа, и при неправильном запросе появится сообщение об ошибке.

5. Компонент заказа Заказ

<!-- 文件位置:&#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>

OrderКонтент, отображаемый на странице, является «заказом XXX», после загрузкиOrderПри монтировании компонента отправляется запрос на получение имени пользователя, то есть проверка доступа к серверуtokenинтерфейс, потому что страница заказа – это страница, на которой выполняется аутентификация пользователей. При успешном выполнении запроса имя пользователя синхронизируется сdata, иначе появится сообщение об ошибке.

существуетLoginа такжеOrderОбратный вызов запроса в двух компонентах кажется слишком простым для написания на самом деле, потому чтоaxiosВозвращаемое сервером значение будет обернуто вокруг возвращаемого сервером значения, сохраняя некоторыеhttpСоответствующая информация ответа, когда осуществляется доступ к двум интерфейсам, адрес запроса также является одним и тем же сервером, и обработка ошибок при ответе сервера верна?401обработка, заголовки запросов должны быть установлены в запросах, связанных с аутентификацией информации о пользователе.AuthorizationОтправитьtoken.

Кажется, мы не видим этой логики в коде, связанном с запросом компонента, потому что мы используемaxiosAPI настроенbaseURLПерехват запроса и перехват ответа, можно узнать, что на самом деле введеноaxiosне напрямую изnode_modulesимпортные, но наши собственные экспортныеaxios.

6. Конфигурация Аксиос

// 文件位置:&#126;jwt-apply/jwt-client/src/axios.js
import axios from "axios";
import router from "./router";

// 设置默认访问地址
axios.defaults.baseURL = "http://localhost:3000";

// 响应拦截
axios.interceptors.response.use(res => {
    // 报错执行 axios then 方法错误的回调,成功返回正确的数据
    return res.data.code !== 0 ? Promise.reject(res.data) : res.data;
}, res => {
    // 如果 token 验证失败则跳回登陆页,并执行 axios then 方法错误的回调
    if (res.response.status === 401) {
        router.history.push("/login");
    }
    return Promise.reject("Not Allowed");
});

// 请求拦截,用于将请求统一带上 token
axios.interceptors.request.use(config => {
    // 在 localStorage 获取 token
    let token = localStorage.getItem("token");

    // 如果存在则设置请求头
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }

    return config;
});

export default axios;

При доступе к серверуaxiosПервый параметр в объединен вaxios.defaults.baseURLпозади как адрес запроса.

axios.interceptors.response.useЧтобы ответить на перехват,axiosПосле отправки запроса все ответы сначала будут выполнять логику внутри этого метода, а возвращаемое значение — это данные, которые передаются в качестве параметра вaxiosвозвращаемое значениеthenметод.

axios.interceptors.request.useдля перехвата запросов,axiosВсе отправленные запросы будут сначала выполнять логику этого метода, а затем отправлять их на сервер, который обычно используется для установки заголовков запросов.


Принцип реализации модуля jwt-simple

Я считаю, что благодаря описанному выше процессу очень ясно, как генерируется JWT.tokenЧто такое формат и как взаимодействовать с интерфейсом для проверкиtoken, будем дальше изучать эти основыtokenВесь процесс генерации и процесс проверки, который мы используемjwt-simpleмодульныйencodeКак генерируются методыtoken,использоватьdecodeКак проверяется методtoken, посмотрите нижеjwt-simpleпринцип реализации.

1. Создайте модуль

// 文件位置:&#126;jwt-apply/jwt-server/jwt-simple.js
const crypto = require("crypto");

/**
* 其他方法
*/

// 创建对象
module.exports = {
    encode,
    decode
};

мы знаемjwt-simpleМы используем два методаencodeа такжеdecode, значит есть эти два метода на окончательно экспортированном объекте, нужно использовать алгоритм соления для подписиcrypto, поэтому вводим его заранее.

2. Преобразование между строками и Base64

// 文件位置:&#126;jwt-apply/jwt-server/jwt-simple.js
// 将子子符串转换成 Base64
function stringToBase64(str) {
    return Buffer.from(str).toString("base64");
}

// 将 Base64 转换成字符串
function base64ToString(base64) {
    return Buffer.from(base64, "base64").toString("utf8");
}

Из названия метода несложно понять назначение и параметры, поэтому они собраны здесь, по сути, суть в том, чтобы конвертировать между двумя кодировками, поэтому перед конвертацией его нужно конвертировать в Buffer.

3. Как сгенерировать подпись

// 文件位置:&#126;jwt-apply/jwt-server/jwt-simple.js
function createSign(str, secret) {
    // 使用加盐算法进行加密
    return crypto.createHmac("sha256", secret).update(str).digest("base64");
}

Этот шаг заключается в использовании алгоритма соленияsha256и ключsecretСгенерируйте подпись, но для удобства мы записали алгоритм шифрования насмерть, в нормальных условиях он должен основываться наHeaderсерединаalgзначение поля для полученияalgЗначение алгоритма шифрования, соответствующее названиюmapСгенерировать подпись по заданному алгоритму.

4. кодировать

// 文件位置:&#126;jwt-apply/jwt-server/jwt-simple.js
function encode(payload, secret) {
    // 头部
    let header = stringToBase64(JSON.stringify({
        typ: "JWT",
        alg: "HS256"
    }));

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

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

    // 生成签名
    return [header, content, sign].join(".");
}

существуетencodeгенерал-лейтенантHeader,PayloadПеревести вbase64,пройти через.соединены вместе, затем используйтеsecretКлюч генерирует подпись, и, наконец,Headerа такжеPayloadизbase64пройти через.В сочетании с сгенерированной подписью это формирует трехсегментный формат «открытый текст» + «открытый текст» + «зашифрованный текст».token.

5. декодировать

// 文件位置:&#126;jwt-apply/jwt-server/jwt-simple.js
function decode(token, secret) {
    let [header, content, sign] = token.split(".");

    // 将接收到的 token 的前两部分(base64)重新签名并验证,验证不通过抛出错误
    if (sign !== createSign([header, content].join("."), secret)) {
        throw new Error("Not Allow");
    }

    // 将 content 转成对象
    content = JSON.parse(base64ToString(content));

    // 检测过期时间,如果过去抛出错误
    if (content.exp && content.exp < Date.now()) {
        throw new Error("Not Allow");
    }

    return content;
}

в методе проверкиdecode, прежде всегоtokenВыньте три абзаца отдельно и перегенерируйте подпись с первыми двумя абзацами,signНапротив, то же самое прошло проверку, но были изменены другие инструкции и выдана ошибка,PayloadСодержаниеcontentПреобразовать в объект, вынутьexpПоле сравнивается с текущим временем, чтобы проверить, не истек ли срок его действия, и выдается ошибка, если он истек.


Суммировать

Сгенерировано в JWTtoken, первые два абзаца открытого текста разрешимы, чтобы другие знали наш алгоритм шифрования и правила после перехвата, а также знали информацию, которую мы передаем, и тоже могли использоватьjwt-simpleЗашифровать фрагмент зашифрованного текста, вставленный вtokenЭто самый важный момент, сколько бы другие люди не знали о передаваемой нами информации, после подделки и подделки она не может пройти проверку сервера, потому что сервер не может быть получен.secret, единственное, что действительно может гарантировать безопасность, этоsecret, пока доказываетHeaderа такжеPayloadОн небезопасен и может быть взломан, поэтому он не может хранить конфиденциальную информацию.