Postman отличный инструмент для работы с REST API, обладающий широким спектром возможностей, начиная от простой проверки эндпоинта и заканчивая написанием полноценных тестов для API. С его помощью можно замокать сервер на основе запросов или создать коллекцию и легко поделиться ею со всей командой. В современных приложениях часто используется авторизация пользователей по токенам, что может вызывать определенные неудобства при работе. Как правило, пользователю предоставляются два токена: access_token и refresh_token, причем access_token быстро устаревает, требуя обновления. В таких случаях приходится вручную выполнять запрос на получение нового токена из другой вкладки, что сильно замедляет работу и ухудшает пользовательский опыт. К счастью,у Postman есть отличный механизм, который позволяет полностью автоматизировать этот процесс.

Postman обладает механизмом выполнения JavaScript-скриптов перед отправкой запроса, что позволяет нам выполнять необходимые действия.

Например, у нас есть endpoint http://localhost:63001/api/user, и мы хотим получить данные с него. Но при попытке отправки запроса получаем вполне логичную ошибку 401 Unauthorized.

Ошибка ‘401 Unauthorized’

Логично, поскольку мы не включили заголовок Authorization с токеном, который в моем случае является Bearer token. Для этого мы устанавливаем соответствующий метод авторизации на вкладке Auth и заполняем соответствующие поля.

Вкладка ‘Authorization’

И все получилось:

Успешный ответ

Мы проверили данные, решили послать еще один запрос и:

Повторная ошибка ‘401 Unauthorized’

Опять же, получаем ошибку 401 Unauthorized. Постоянное обновление токена очень неудобно и утомительно, особенно если токен действует всего несколько минут. К счастью, решение этой проблемы находится на вкладке Pre-req. Добавив туда строку console.log("Pre-request")

Вкладка ‘Pre-req’

Для наглядности откроем Postman console, кликнув по кнопке внизу:

Окно ‘Postman console’

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

Вывод в окно консоли

Таким образом, мы можем поместить в этот блок код, который будет получать для нас нужный токен. Кроме того, там же мы можем найти сниппет для отправки запроса, который вставит следующий код.

console.log("Pre-request");
pm.sendRequest("https://postman-echo.com/get", function (err, response) {
    console.log(response.json());
});

Выполнив запрос с этим кодом в консоли, можем увидеть, что перед выполнением основного запроса был выполнен GET запрос на https://postman-echo.com/get

Вывод в консоль 2 запросов

Теперь попробуем написать код, который получит access token:

const request = {
    url: 'http://localhost:63001/api/token',
    method: 'POST',
    header: 'application/x-www-form-urlencoded',
    body: {
        mode: 'formdata',
        formdata: [
            { key: 'grant_type', value: 'password' },
            { key: 'username', value: 'admin' },
            { key: 'password', value: 'password123' }
        ]
    }
};
pm.sendRequest(request, (err, res) => {
    const { access_token } = res.json();
    pm.globals.set('ACCESS_TOKEN', access_token);
    console.log('ACCESS_TOKEN: ${access_token}');
});

Как видно, в ответе мы можем получить все необходимые данные и установить заголовок напрямую через pm.request. Однако у Postman есть более элегантный метод для этого. Для его реализации в строке pm.globals.set('ACCESS_TOKEN', access_token); глобальной переменной ACCESS_TOKEN присваивается значение полученного access token’а. Затем в любом месте мы можем получить это значение, используя литерал шаблона {{ACCESS_TOKEN}}. Например, перейдя на вкладку Auth и введя вместо токена строку {{ACCESS_TOKEN}}. При этом при наведении мы даже можем видеть первоначальное и текущее значение переменной, если она уже определена.

Пример access token’а в окне Postman

После этого, при выполнении следующего запроса, ACCESS_TOKEN уже будет определен, и запрос выполнится успешно. Однако такой код будет выполняться намного дольше, так как на каждый запрос будет выполняться еще один для авторизации. К счастью, этого можно избежать, просто написав логику проверки токена. Также логичным будет хранить refresh token и при истечении access token’а просто выполнять его обновление. В таком случае у нас может быть три варианта состояния:

  1. ACCESS_TOKEN и REFRESH_TOKEN отсутствуют, в этом случае следует авторизоваться в системе.
  2. ACCESS_TOKEN и REFRESH_TOKEN определены, но ACCESS_TOKEN уже невалиден, в этом случае необходимо просто получить новый ACCESS_TOKEN.
  3. ACCESS_TOKEN и REFRESH_TOKEN определены и валидны, в этом случае никаких дополнительных действий не требуется.

Для упрощения написания определим небольшие функции для работы с глобальными переменными, чтобы не захламлять код:

const value = (key, defaultVlue) => pm.globals.get(key) || defaultVlue;
const setValue = (key, value) => pm.globals.set(key, value);

После этого начнем с того, что определим функцию для отправки запроса на авторизацию и сохранения результата:

const getTimeStamp = (expires_in = 0) => {
    const expiryDate = new Date();
    expiryDate.setSeconds(expiryDate.getSeconds() + expires_in);
    return expiryDate.getTime();
}

const sendRequest = (urlencoded) => {
    const request = {
        url: 'http://localhost:63001/api/token',
        method: 'POST',
        header: {
            'contetn-type': 'application/x-www-form-urlencoded'
        },
        body: { mode: 'urlencoded', urlencoded }
    };
    pm.sendRequest(request, (err, res) => {
        const { access_token, refresh_token, expires_in } = res.json();
        setValue('ACCESS_TOKEN', access_token);
        setValue('REFRESH_TOKEN', refresh_token);
        setValue('ACCESS_TOKEN_EXPIRY', getTimeStamp(expires_in));
    });
}

const sendLoginRequest = () => {
    const username = pm.environment.get('AUTH_USERNAME') || 'admin';
    const password = pm.environment.get('AUTH_PASSWORD') || 'password123';
    console.log('Send login request');
    sendRequest([
        { key: 'grant_type', value: 'password' },
        { key: 'username', value: username },
        { key: 'password', value: password }
    ]);
}

Как видно из кода, дополнительно сохраняется значение ACCESS_TOKEN_EXPIRY. Это переменная, позволяющая определить, истек ли срок действия нашего токена или нет до вызова запроса. Для этого просто прибавляем к текущему времени количество секунд, указанное в поле expires_in.

Обратите внимание, что значения AUTH_USERNAME и AUTH_PASSWORD берутся не из глобальных переменных, а из переменных окружения. Это сделано для простоты смены пользователя при выполнении запроса. Достаточно просто определить эти переменные в окружении, и без дополнительных настроек вы сможете входить в систему под своим пользователем.

Далее рассмотрим случай, когда у нас уже есть ACCESS_TOKEN и REFRESH_TOKEN, но первый из них устарел. Для этого мы определим еще одну функцию sendRefreshTokenRequest:

const sendRefreshTokenRequest = () => {
    const refreshToken = value('REFRESH_TOKEN');
    console.log('Send refresh token request');
    sendRequest([
        { key: 'grant_type', value: 'refresh_token' },
        { key: 'refresh_token', value: refreshToken }
    ]);
}

Осталось только применить эти функции:

const token = value('ACCESS_TOKEN');
const accessTokenExpiry = value('ACCESS_TOKEN_EXPIRY');

if(!token) {
    sendLoginRequest();
} else if(accessTokenExpiry <= getTimeStamp()) {
    sendRefreshTokenRequest();
}

И вот, наконец, все работает. Очистим глобальные переменные и выполним серию запросов, чтобы посмотреть, как выполняется наш код, и куда направляются запросы:

Пример выполнения череды запросов с автоматическим обновлением токена

Как видно, сперва отправляется запрос на авторизацию пользователя, то есть вызывается метод sendLoginRequest(). Затем следует 6 успешных GET-запросов на /api/user. После истечения срока действия access token’а вызывается метод sendRefreshTokenRequest(), который обновляет нашу пару токенов.

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

Редактирование коллекции

Переносим написанный скрипт в секцию Pre-request script. Также не совсем правильно хардкодить URL, по которому происходит авторизация. Более правильным будет добавить переменную HOST в среду, где будет храниться хост, к которому нужно обращаться. Это можно сделать на вкладке Variables. А в поле URL запроса вставить конструкцию ${pm.environment.get('HOST')}/api/token:

Вкладка ‘Pre-request script’ для коллекции Вкладка ‘Variables’ для коллекции

После этого сохраняем все изменения. Теперь можно выполнять любые запросы, подставляя вместо http://localhost:63001 переменную {{HOST}}, и не думая об авторизации. Если обращение к какому-либо эндпоинту не требует авторизации, просто выбираем тип авторизации Not Auth.

Пример запроса с переменными ‘HOST’ и ‘ACCESS_TOKEN’

Полный код

const value = (key, defaultVlue) => pm.globals.get(key) || defaultVlue;
const setValue = (key, value) => pm.globals.set(key, value);
const getTimeStamp = (expires_in = 0) => {
    const expiryDate = new Date();
    expiryDate.setSeconds(expiryDate.getSeconds() + expires_in);
    return expiryDate.getTime();
}

const sendRequest = (urlencoded) => {
    const request = {
        url: `${pm.environment.get('HOST')}/api/token`,
        method: 'POST',
        header: {
            'content-type': 'application/x-www-form-urlencoded'
        },
        body: { mode: 'urlencoded', urlencoded }
    };
    pm.sendRequest(request, (err, res) => {
        const { access_token, refresh_token, expires_in } = res.json();
        setValue('ACCESS_TOKEN', access_token);
        setValue('REFRESH_TOKEN', refresh_token);
        setValue('ACCESS_TOKEN_EXPIRY', getTimeStamp(expires_in));
    });
}

const sendLoginRequest = () => {
    const username = pm.environment.get('AUTH_USERNAME') || 'admin';
    const password = pm.environment.get('AUTH_PASSWORD') || 'password123';
    console.log('Send login request');
    sendRequest([
        { key: 'grant_type', value: 'password' },
        { key: 'username', value: username },
        { key: 'password', value: password }
    ]);
}

const sendRefreshTokenRequest = () => {
    const refreshToken = value('REFRESH_TOKEN');
    console.log('Send refresh token request');
    sendRequest([
        { key: 'grant_type', value: 'refresh_token' },
        { key: 'refresh_token', value: refreshToken }
    ]);
}

const token = value('ACCESS_TOKEN');
const accessTokenExpiry = value('ACCESS_TOKEN_EXPIRY');

if(!token) {
    sendLoginRequest();
} else if(accessTokenExpiry <= getTimeStamp()) {
    sendRefreshTokenRequest();
}

Ссылки на источники

  1. Execution order of scripts
  2. Write pre-request scripts to add dynamic behavior in Postman
  3. Process data and script workflows using Postman JavaScript objects