OAuth2 Запомнить меня с токеном обновления

OAuth2 Запомнить меня с токеном обновления

1. обзор

В этой статье мы добавим функцию «Запомнить меня» в защищенное приложениеOAuth 2, используя токен обновленияOAuth 2.

Эта статья является продолжением нашей серии статей об использованииOAuth 2 для защиты Spring REST API, доступ к которому осуществляется через клиентAngularJS. Для настройки сервера авторизации, сервера ресурсов и клиентского интерфейса вы можете следоватьthe introductory article.

Затем вы можете продолжить нашу статью оhandling the refresh token, используя проксиZuul.

2. Токен доступа OAuth 2 и токен обновления

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

При первой попытке аутентификации с использованием типа предоставленияpassword пользователю необходимо отправить действительное имя пользователя и пароль, а также идентификатор и секрет клиента. Если запрос аутентификации успешен, сервер отправляет ответ в форме:

{
    "access_token": "2e17505e-1c34-4ea6-a901-40e49ba786fa",
    "token_type": "bearer",
    "refresh_token": "e5f19364-862d-4212-ad14-9d6275ab1a62",
    "expires_in": 59,
    "scope": "read write",
}

Мы видим, что ответ сервера содержит как токен доступа, так и токен обновления. Маркер доступа будет использоваться для последующих вызовов API, требующих аутентификации, покаthe purpose of the refresh token is to obtain a new valid access token или просто отзывается предыдущий.

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

The goal of using two types of tokens is to enhance user security. Обычно токен доступа имеет более короткий период действия, поэтому, если злоумышленник получит токен доступа, у него будет ограниченное время, в течение которого он может его использовать. С другой стороны, если токен обновления скомпрометирован, это бесполезно, так как также необходимы идентификатор клиента и секрет.

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

3. Функция Remember-Me с токенами обновления

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

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

В следующих разделах мы обсудим два способа реализации этой функции:

  • во-первых, путем перехвата любого пользовательского запроса, который возвращает код состояния 401, что означает, что токен доступа недействителен. В этом случае, если пользователь установил флажок «запомнить меня», мы автоматически выдадим запрос на новый токен доступа с использованием типа предоставленияrefresh_token, а затем снова выполним первоначальный запрос.

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

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

4. Хранение токена обновления

В предыдущей статье мы добавилиCustomPostZuulFilter, который перехватывает запросы к серверуOAuth, извлекает токен обновления, отправленный обратно при аутентификации, и сохраняет его в cookie на стороне сервера:

@Component
public class CustomPostZuulFilter extends ZuulFilter {

    @Override
    public Object run() {
        //...
        Cookie cookie = new Cookie("refreshToken", refreshToken);
        cookie.setHttpOnly(true);
        cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
        cookie.setMaxAge(2592000); // 30 days
        ctx.getResponse().addCookie(cookie);
        //...
    }
}

Затем давайте добавим в нашу форму входа флажок, который имеет привязку данных к переменнойloginData.remember:


Наша форма входа теперь будет отображать дополнительный флажок:

image

ОбъектloginData отправляется с запросом аутентификации, поэтому он будет включать параметрremember. Перед отправкой запроса аутентификации мы установим файл cookie с именемremember на основе параметра:

function obtainAccessToken(params){
    if (params.username != null){
        if (params.remember != null){
            $cookies.put("remember","yes");
        }
        else {
            $cookies.remove("remember");
        }
    }
    //...
}

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

5. Обновление токенов путем перехвата ответов 401

Чтобы перехватывать запросы, которые возвращаются с ответом 401, давайте изменим наше приложениеAngularJS, добавив перехватчик с функциейresponseError:

app.factory('rememberMeInterceptor', ['$q', '$injector', '$httpParamSerializer',
  function($q, $injector, $httpParamSerializer) {
    var interceptor = {
        responseError: function(response) {
            if (response.status == 401){

                // refresh access token

                // make the backend call again and chain the request
                return deferred.promise.then(function() {
                    return $http(response.config);
                });
            }
            return $q.reject(response);
        }
    };
    return interceptor;
}]);

Наша функция проверяет, имеет ли статус 401 - что означает, что токен доступа недействителен, и, если это так, пытается использовать токен обновления, чтобы получить новый действительный токен доступа.

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

Давайте подробнее рассмотрим процесс обновления токена доступа. Сначала мы инициализируем необходимые переменные:

var $http = $injector.get('$http');
var $cookies = $injector.get('$cookies');
var deferred = $q.defer();

var refreshData = {grant_type:"refresh_token"};

var req = {
    method: 'POST',
    url: "oauth/token",
    headers: {"Content-type": "application/x-www-form-urlencoded; charset=utf-8"},
    data: $httpParamSerializer(refreshData)
}

Вы можете увидеть переменнуюreq, которую мы будем использовать для отправки запроса POST в конечную точку / oauth / token, с параметромgrant_type=refresh_token.

Затем давайте воспользуемся внедренным нами модулем$http для отправки запроса. Если запрос будет успешным, мы установим новый заголовокAuthentication с новым значением токена доступа, а также новое значение для файла cookieaccess_token. Если запрос не выполняется, что может произойти, если токен обновления также в конце концов истекает, то пользователь перенаправляется на страницу входа:

$http(req).then(
    function(data){
        $http.defaults.headers.common.Authorization= 'Bearer ' + data.data.access_token;
        var expireDate = new Date (new Date().getTime() + (1000 * data.data.expires_in));
        $cookies.put("access_token", data.data.access_token, {'expires': expireDate});
        window.location.href="index";
    },function(){
        console.log("error");
        $cookies.remove("access_token");
        window.location.href = "login";
    }
);

Токен обновления добавляется к запросуCustomPreZuulFilter, который мы реализовали в предыдущей статье:

@Component
public class CustomPreZuulFilter extends ZuulFilter {

    @Override
    public Object run() {
        //...
        String refreshToken = extractRefreshToken(req);
        if (refreshToken != null) {
            Map param = new HashMap();
            param.put("refresh_token", new String[] { refreshToken });
            param.put("grant_type", new String[] { "refresh_token" });

            ctx.setRequest(new CustomHttpServletRequest(req, param));
        }
        //...
    }
}

Помимо определения перехватчика, нам нужно зарегистрировать его с помощью$httpProvider:

app.config(['$httpProvider', function($httpProvider) {
    $httpProvider.interceptors.push('rememberMeInterceptor');
}]);

6. Активное обновление токенов

Еще один способ реализовать функцию «запомнить меня» - запросить новый токен доступа до истечения срока действия текущего.

При получении токена доступа ответ JSON содержит значениеexpires_in, которое указывает количество секунд, в течение которых токен будет действителен.

Давайте сохраним это значение в файле cookie для каждой аутентификации:

$cookies.put("validity", data.data.expires_in);

Затем, чтобы отправить запрос на обновление, давайте воспользуемся службойAngularJS $timeout, чтобы запланировать вызов обновления за 10 секунд до истечения срока действия токена:

if ($cookies.get("remember") == "yes"){
    var validity = $cookies.get("validity");
    if (validity >10) validity -= 10;
    $timeout( function(){ $scope.refreshAccessToken(); }, validity * 1000);
}

7. Заключение

В этом руководстве мы рассмотрели два способа реализации функции «Запомнить меня» с помощью приложения OAuth2 и внешнего интерфейсаAngularJS.

Полный исходный код примеров можно найтиover on GitHub. Вы можете получить доступ к странице входа с функцией «запомнить меня» по URL-адресу/login_remember.