«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>
Наша форма входа теперь будет отображать дополнительный флажок:
объект отправляется с запросом аутентификации, поэтому он будет включать параметр запоминания. Перед отправкой запроса аутентификации мы установим файл cookie с именем Remember на основе параметра: от того, хочет ли пользователь, чтобы его запомнили, или нет.
function obtainAccessToken(params){
if (params.username != null){
if (params.remember != null){
$cookies.put("remember","yes");
}
else {
$cookies.remove("remember");
}
}
//...
}
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";
}
);
Токен обновления добавляется к запросу с помощью 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, указывающее количество секунд, в течение которых токен будет действителен.
Давайте сохраним это значение в файле 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. Вы можете получить доступ к странице входа с функцией «запомнить меня» по URL-адресу /login_remember.