Принцип CORS и анализ исходного кода @koa/cors

HTTP

Началось вличный блог

содержание

  • перекрестный домен
  • Простые и сложные запросы
  • Как настроить CORS на стороне сервера
  • Как @koa/cors это удалось

перекрестный домен

Почему возникают междоменные проблемы?

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

Обязательно обратите внимание на междоменное ограничение браузера.На самом деле, вы можете использовать инструмент капитана для получения данных интерфейса, вы можете видеть, что интерфейс вернулся обратно, просто ограничение браузера, вы можете' т получить данные. Используйте интерфейс запроса Postman для запроса данных. Это еще раз подтвердило, что междоменное ограничение является ограничением браузера.

Как решить междомен?

  • jsonp: можно использовать все теги с атрибутом src, но они могут обрабатывать только запросы GET.
  • document.domain + перекрестный домен iframe
  • location.hash + iframe
  • window.name + iframe
  • postMessage кросс-доменный
  • Nginx настроить обратный прокси
  • CORS (Cross-Origin Resource Sharing): поддерживает все типы HTTP-запросов. Я считаю, что все знакомы с вышеуказанными решениями.Я не буду здесь объяснять каждый метод.Далее я буду в основном говорить о CORS;

Простые и непростые запросы

браузер будетМеждоменный запрос CORSДелятся на простые запросы и непростые запросы;

Если вы используете междоменную проблему, решаемую обратным прокси-сервером nginx, междоменного запроса не будет, потому что обратный прокси-сервер nginx делает внешний и внутренний интерфейсы в одном и том же домене, и междоменная проблема отсутствует.

Пока два условия выполняются одновременно, это простой запрос. (1) Используйте один из следующих методов:

  • head
  • get
  • post

(2) Запрашиваемый хедер

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type: ограничено тремя значениями:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

Если два вышеуказанных условия не выполняются одновременно, это непростая заявка. Браузеры обрабатывают эти два типа по-разному.

простой запрос

пример

Для простых запросов браузер делает запросы CORS напрямую. В частности, к информации заголовка добавляется поле Origin.

简单请求
Вышеприведенный пример,postпросить,Content-Typeдляapplication/x-www-form-urlencoded, который удовлетворяет условиям простого запроса, заголовок ответа возвращаетAccess-Control-Allow-Origin: http://127.0.0.1:3000; Когда браузер обнаруживает, что этот междоменный запрос является простым запросом, он автоматически добавляетOriginполе;OriginПоле используется для указания источника запроса (протокол + имя домена + номер порта). На основе этого значения сервер решает, соглашаться ли на запрос.

Все поля, относящиеся к запросу CORS, начинаются сAccess-Control-начало

  • Access-Control-Allow-Origin:требуется
    • заголовок запросаOriginзначение поля
    • *: принимает любое доменное имя
  • Access-Control-Allow-Credentials: необязательный,
    • true: указывает, что в настоящее время разрешена отправка файлов cookie.Access-Control-Allow-Originне может быть установлено на*, вы должны указать явное, согласованное доменное имя с запрашивающей страницей.
    • Оставьте это поле незаполненным: браузер не обязан отправлять куки.
  • Access-Control-Expose-Headers: необязательный
    • Заголовки ответа указывают, какие заголовки могут быть представлены как часть ответа, путем перечисления их имен. По умолчанию отображаются только 6 простых заголовков ответа:
      • Cache-Control
      • Content-Language
      • Content-Type
      • Expires
      • Last-Modified
      • Pragma
    • Если вы хотите, чтобы клиент имел доступ к другим заголовкам, вы можете перечислить их в Access-Control-Expose-Headers.

свойство withCredentials

Запросы CORS не отправляют файлы cookie и данные аутентификации HTTP по умолчанию.Если вы хотите отправить файлы cookie на сервер, с одной стороны, сервер должен согласиться установить заголовок ответа.Access-Control-Allow-Credentials: true, с другой стороны, также требуются некоторые настройки, когда клиент отправляет запрос;

// XHR
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true); 
xhr.withCredentials = true; 
xhr.send(null);

// Fetch
fetch(url, {
  credentials: 'include'  
})

не простой запрос

Непростой запрос — это запрос, который имеет особые требования к серверу. Например, метод запросаPUTилиDELETE,илиContent-TypeПолеapplication/json;

1. Предварительные запросы и ответы

Для запросов CORS, которые не являются простыми запросами, перед формальным общением будет добавлен запрос HTTP-запроса, который называется предварительным запросом; Браузер сначала спрашивает сервер, находится ли доменное имя текущей веб-страницы в списке разрешений сервера, и какие HTTP-глаголы и поля заголовка можно использовать, и только при получении положительного ответа браузер выдает формальный запрос интерфейса, иначе сообщит об ошибке. ;

preflight

Метод HTTP-запроса — POST, а заголовок запросаContent-TypeПолеapplication/json. Когда браузер узнает, что это непростой запрос, он автоматически выдает预检запрос, прося сервер подтвердить, что такой запрос может быть сделан.

1.1 Предполетный запрос

预检Метод запроса, используемый для запроса, — OPTIONS, что означает, что запрос используется для запроса. В информации заголовка ключевым полем являетсяOrigin, указывающее, из какого домена пришел запрос. КромеOrigin,预检Информация заголовка запроса включает два специальных поля:

  • Access-Control-Request-Method: Требуется, используется для перечисления того, какие способы HTTP будут использоваться в запросе CORSers браузера. Приведенный выше примерPOST
  • Access-Control-Request-Headers: это поле представляет собой строку, разделенную запятыми, которая является дополнительным полем заголовка, которое будет отправлено при выполнении запроса CORS браузера.Приведенный выше пример:Content-Type;

1.2 Предполетный ответ

сервер получает预检После запроса проверьтеOrigin,Access-Control-Request-Methodа такжеAccess-Control-Request-HeadersПосле подтверждения того, что междоменные запросы разрешены, вы можете ответить. В приведенном выше ответе HTTP ключом является поле Access-Control-Allow-Origin, указывающее, что http://127.0.0.1:3000 может запрашивать данные. В этом поле также можно установить звездочку, чтобы указать согласие на любой запрос из другого источника.

Если браузер отклоняет предварительный запрос, он вернет обычный HTTP-ответ, но без каких-либо полей заголовка, связанных с CORS. В это время браузер определит, что сервер не согласен с предварительным запросом, что вызовет ошибку. , который перехватывается функцией обратного вызова onerror объекта XMLHttpRequest.

Дополнительные поля CORS, на которые ответил сервер

  • Access-Control-Allow-Methods: Обязательный; его значение представляет собой строку с разделителями-запятыми, указывающую все методы, поддерживаемые сервером для междоменных запросов. Обратите внимание, что возвращаются все поддерживаемые методы, а не только запрошенные браузером. Это во избежание множественных预检просить.
  • Access-Control-Allow-Headers: Если заголовок запроса браузера включаетAccess-Control-Request-Headersполе, тоAccess-Control-Allow-HeadersПоле, обязательное для заполнения. Это также строка с разделителями-запятыми, указывающая все поля заголовка, поддерживаемые сервером, не ограничиваясь预检поля, запрошенные в .
  • Access-Control-Allow-Credentials: То же значение, что и для простых запросов.
  • Access-Control-Allow-Max-Age: Необязательный, используется для указания периода действия этого запроса предварительной проверки. Единица - секунды. В течение срока действия нет необходимости оформлять еще один предполетный запрос

2. Обычный запрос и ответ

Как только сервер прошел预检запрос, каждый раз, когда браузер делает обычный запрос CORS, он будет таким же, как и простой запрос, будетOriginполе заголовка. Ответ сервера также будет иметьAccess-Control-Allow-Originинформационное поле заголовка;

normal

Как настроить CORS на стороне сервера

Отдельный интерфейс для отдельной обработки

Например, простая страница входа должна передавать интерфейсу два поля: имя пользователя и пароль; доменное имя внешнего интерфейса — localhost:8900, а доменное имя внутреннего интерфейса — localhost:3200, что представляет собой кросс-сервер. домен.

1. Если установлен заголовок запроса'Content-Type': 'application/x-www-form-urlencoded', который представляет собой простой запрос;

Будут междоменные проблемы, напрямую устанавливайте заголовок ответаAccess-Control-Allow-Originдля*или конкретное доменное имя; обратите внимание, что если заголовок ответа установленAccess-Control-Allow-Credentialsдляtrue, указывая, что вы хотите отправитьcookie, то в это времяAccess-Control-Allow-OriginЗначение не может быть равно звездочке и должно указывать явное, согласованное доменное имя с запрашивающей страницей.

const login = ctx => {
    const req = ctx.request.body;
    const userName = req.userName;
    ctx.set('Access-Control-Allow-Origin', '*');
    ctx.response.body = {
        data: {},
        msg: '登陆成功'
    };
}

2. Если установлен заголовок запроса'Content-Type': 'application/json', который является непростым запросом

Для обработки запросов OPTIONS сервер может написать отдельный маршрут для обработкиloginОПЦИИ запрос

app.use(route.options('/login', ctx => {
    ctx.set('Access-Control-Allow-Origin', '*');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type');
    ctx.status = 204;
    
}));

Всем известно, что когда фронтенд обращается к серверу, он вызывает множество интерфейсов, и логика каждого интерфейса, обрабатывающего междоменные запросы, абсолютно одинакова, мы можем извлечь эту часть и использовать ее как промежуточное ПО;

Написать промежуточное ПО для обработки

Сначала разберитесь с моделью «лукового кольца» промежуточного программного обеспечения koa.

洋葱圈

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

const Koa = require("koa");
const app = new Koa();
app.use((ctx, next) => {
    console.log('a - 1');
    next();
    console.log('a - 2');
})
app.use((ctx, next) => {
    console.log('b - 1');
    next();
    console.log('b - 2');
})
app.use((ctx, next) => {
    console.log('c - 1');
    next();
    console.log('c - 2');
})

app.listen(3200, () => {
    console.log('启动成功');
});

выход

a - 1
b - 1
c - 1
c - 2
b - 2
a - 2

В официальных документах Koa внешнее промежуточное ПО называется «восходящим», а внутреннее промежуточное ПО — «нисходящим». Общее промежуточное ПО будет выполняться дважды, вызываяnextОднажды, позвонивnextПри передаче управления нижестоящему промежуточному ПО по порядку. Когда в нисходящем направлении больше нет промежуточного программного обеспечения или промежуточного программного обеспечения не выполняетсяnextфункция, поведение вышестоящего промежуточного ПО в свою очередь будет восстановлено, и вышестоящее промежуточное ПО будет выполнено.nextкод после;

Простой пример промежуточного ПО, работающего с междоменными данными.

const Koa = require("koa");
const app = new Koa();
const route = require('koa-route');
var bodyParser = require('koa-bodyparser');

app.use(bodyParser()); // 处理post请求的参数

const login = ctx => {
    const req = ctx.request.body;
    const userName = req.userName;
    const expires = Date.now() + 3600000; // 设置超时时间为一小时后
    
    var payload = { 
        iss: userName,
        exp: expires
    };
    const Token = jwt.encode(payload, secret);
    ctx.response.body = {
        data: Token,
        msg: '登陆成功'
    };
}

// 将公共逻辑方法放到中间件中处理
app.use((ctx, next)=> {
    const headers = ctx.request.headers;
    if(ctx.method === 'OPTIONS') {
        ctx.set('Access-Control-Allow-Origin', '*');
        ctx.set('Access-Control-Allow-Headers', 'Authorization');
        ctx.status = 204;
    } else {
        next();
    }
})
app.use(route.post('/login', login));

app.listen(3200, () => {
    console.log('启动成功');
});

вышесказанноеОбразец кода адреса

Как @koa/cors это удалось

'use strict';

const vary = require('vary');

/**
 * CORS middleware
 *
 * @param {Object} [options]
 *  - {String|Function(ctx)} origin `Access-Control-Allow-Origin`, default is request Origin header
 *  - {String|Array} allowMethods `Access-Control-Allow-Methods`, default is 'GET,HEAD,PUT,POST,DELETE,PATCH'
 *  - {String|Array} exposeHeaders `Access-Control-Expose-Headers`
 *  - {String|Array} allowHeaders `Access-Control-Allow-Headers`
 *  - {String|Number} maxAge `Access-Control-Max-Age` in seconds
 *  - {Boolean} credentials `Access-Control-Allow-Credentials`
 *  - {Boolean} keepHeadersOnError Add set headers to `err.header` if an error is thrown
 * @return {Function} cors middleware
 * @api public
 */
module.exports = function (options) {
    const defaults = {
        allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
    };
    // 默认的配置项和使用时设置的options进行一个融合
    options = Object.assign({}, defaults, options);

    // 因为函数的一些参数,exposeHeaders,allowMethods,allowHeaders的形式既可以是String,也可以是Array类型,
    // 如果是Array类型,也转换为用逗号分隔的字符串。
    if (Array.isArray(options.exposeHeaders)) {
        options.exposeHeaders = options.exposeHeaders.join(',');
    }

    if (Array.isArray(options.allowMethods)) {
        options.allowMethods = options.allowMethods.join(',');
    }

    if (Array.isArray(options.allowHeaders)) {
        options.allowHeaders = options.allowHeaders.join(',');
    }

    if (options.maxAge) {
        options.maxAge = String(options.maxAge);
    }

    options.credentials = !!options.credentials;
    options.keepHeadersOnError = options.keepHeadersOnError === undefined || !!options.keepHeadersOnError;

    return async function cors(ctx, next) {
        // If the Origin header is not present terminate this set of steps.
        // The request is outside the scope of this specification.
        const requestOrigin = ctx.get('Origin');

        // Always set Vary header
        // https://github.com/rs/cors/issues/10
        ctx.vary('Origin');
        // 如果请求头不存在 origin,则直接跳出该中间件,执行下一个中间件
        if (!requestOrigin) return await next();

        // 对origin参数的不同类型做一个处理
        let origin;
        if (typeof options.origin === 'function') {
            origin = options.origin(ctx);
            if (origin instanceof Promise) origin = await origin;
            if (!origin) return await next();
        } else {
            origin = options.origin || requestOrigin;
        }

        const headersSet = {};

        function set(key, value) {
            ctx.set(key, value);
            headersSet[key] = value;
        }
        /**
        * 非OPTIONS请求的处理
        * 
        */
       
        if (ctx.method !== 'OPTIONS') {
            // Simple Cross-Origin Request, Actual Request, and Redirects
            set('Access-Control-Allow-Origin', origin);

            if (options.credentials === true) {
                set('Access-Control-Allow-Credentials', 'true');
            }

            if (options.exposeHeaders) {
                set('Access-Control-Expose-Headers', options.exposeHeaders);
            }

            if (!options.keepHeadersOnError) {
                return await next();
            }
            try {
                return await next();
            } catch (err) {
                const errHeadersSet = err.headers || {};
                const varyWithOrigin = vary.append(errHeadersSet.vary || errHeadersSet.Vary || '', 'Origin');
                delete errHeadersSet.Vary;

                err.headers = Object.assign({}, errHeadersSet, headersSet, {
                    vary: varyWithOrigin
                });

                throw err;
            }
        } else {
            // Preflight Request

            // If there is no Access-Control-Request-Method header or if parsing failed,
            // do not set any additional headers and terminate this set of steps.
            // The request is outside the scope of this specification.
            if (!ctx.get('Access-Control-Request-Method')) {
                // this not preflight request, ignore it
                return await next();
            }

            ctx.set('Access-Control-Allow-Origin', origin);

            if (options.credentials === true) {
                ctx.set('Access-Control-Allow-Credentials', 'true');
            }

            if (options.maxAge) {
                ctx.set('Access-Control-Max-Age', options.maxAge);
            }

            if (options.allowMethods) {
                ctx.set('Access-Control-Allow-Methods', options.allowMethods);
            }

            let allowHeaders = options.allowHeaders;
            if (!allowHeaders) {
                allowHeaders = ctx.get('Access-Control-Request-Headers');
            }
            if (allowHeaders) {
                ctx.set('Access-Control-Allow-Headers', allowHeaders);
            }

            ctx.status = 204;
        }
    };
};

Выше@koa/corsРеализация исходного кода версии 3.0.0, если вы действительно понимаете CORS, логика исходного кода будет очень простой.

Он в основном обрабатывается двумя логиками: с предварительными запросами и без предварительных запросов.

Для обработки не-OPTIONS запросов добавить по ситуацииAccess-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Expose-HeadersЭти три заголовка ответа;

Для обработки запроса OPTIONS (preflight request) добавить по ситуацииAccess-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Max-Age,Access-Control-Allow-Methods,Access-Control-Allow-HeadersЭти заголовки ответа;