Перейти к основному содержимому

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

· 6 мин. чтения

1. Обзор

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

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

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

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

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

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

{
"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 , пользователю больше не нужно вводить свои учетные данные, а только идентификатор клиента, секрет и, конечно же, токен обновления.

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

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

3. Функция «Запомнить меня» с токенами обновления

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

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

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

  • во-первых, путем перехвата любого пользовательского запроса, который возвращает код состояния 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 :

<input type="checkbox"  ng-model="loginData.remember" id="remember"/>
<label for="remember">Remeber me</label>

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

./270fea0e8d44cd170a31ed482008cb91.png

Объект loginData отправляется с запросом на аутентификацию, поэтому он будет включать параметр « запомнить » . Перед отправкой запроса на аутентификацию мы установим файл 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 с новым значением токена доступа, а также новое значение для файла cookie access_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";
}
);

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

@Component
public class CustomPreZuulFilter extends ZuulFilter {

@Override
public Object run() {
//...
String refreshToken = extractRefreshToken(req);
if (refreshToken != null) {
Map<String, String[]> param = new HashMap<String, String[]>();
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 , указывающее количество секунд, в течение которых токен будет действителен.

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

$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 . Вы можете получить доступ к странице входа с функцией «запомнить меня» по URL-адресу /login_remember .