Рубрики
Без рубрики

OAuth2 Запомните меня с помощью токена обновления (с использованием устаревшего стека Spring Security OAuth)

Узнайте, как реализовать функциональность “Запомни меня” с помощью углового интерфейса для приложения, защищенного с помощью Spring Security и OAuth2.

Автор оригинала: baeldung.

1. Обзор

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

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

Примечание : в этой статье используется устаревший проект Spring OAuth .

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

Во-первых, давайте кратко расскажем о токенах OAuth2 и о том, как их можно использовать.

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

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

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

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

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

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

3. Функциональность “Запомни Меня” С Маркерами Обновления

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

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

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

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

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

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);
        //...
    }
}

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


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

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

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/токена, с параметром grant_type=refresh_token .

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

$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));
        }
        //...
    }
}

В дополнение к определению перехватчика, нам необходимо зарегистрировать его у поставщика $http :

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 .

Полный исходный код примеров можно найти на GitHub . Вы можете получить доступ к странице входа с функцией “запомни меня” по адресу /login_remember .