1. Обзор
В этом уроке мы продолжим изучение потока паролей OAuth, который мы начали собирать в нашей предыдущей статье, и сосредоточимся на том, как обрабатывать токен обновления в приложении AngularJS.
Примечание : эта статья использует Spring OAuth legacy project . Для версии этой статьи, использующей новый стек Spring Security 5, ознакомьтесь с нашей статьей OAuth2 для Spring REST API – Обработайте токен обновления в Angular .
2. Истечение срока действия Токена Доступа
Во-первых, помните, что клиент получал маркер доступа, когда пользователь входил в приложение:
function obtainAccessToken(params) { var req = { method: 'POST', url: "oauth/token", headers: {"Content-type": "application/x-www-form-urlencoded; charset=utf-8"}, data: $httpParamSerializer(params) } $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"); window.location.href = "login"; }); }
Обратите внимание, как наш токен доступа хранится в файле cookie, срок действия которого истекает в зависимости от того, когда истекает сам токен.
Важно понимать, что сам файл cookie используется только для хранения и он не управляет ничем другим в потоке OAuth. Например, браузер никогда не будет автоматически отправлять файлы cookie на сервер с запросами.
Также обратите внимание, как мы на самом деле вызываем эту функцию get Access Token() :
$scope.loginData = { grant_type:"password", username: "", password: "", client_id: "fooClientIdPassword" }; $scope.login = function() { obtainAccessToken($scope.loginData); }
3. Доверенное лицо
Теперь у нас будет прокси-сервер Zuul, работающий во фронтальном приложении и в основном сидящий между фронтальным клиентом и сервером авторизации.
Давайте настроим маршруты прокси:
zuul: routes: oauth: path: /oauth/** url: http://localhost:8081/spring-security-oauth-server/oauth
Интересно то, что мы только проксируем трафик на сервер авторизации и ничего больше. Нам действительно нужен прокси-сервер только тогда, когда клиент получает новые токены.
Если вы хотите ознакомиться с основами Зуула, быстро прочтите основную статью о Зууле .
4. Фильтр Zuul, Который Выполняет Базовую Аутентификацию
Первое использование прокси – сервера очень просто-вместо того, чтобы раскрывать наше приложение ” client secret ” в javascript, мы будем использовать предварительный фильтр Zuul для добавления заголовка авторизации к запросам токенов доступа:
@Component public class CustomPreZuulFilter extends ZuulFilter { @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); if (ctx.getRequest().getRequestURI().contains("oauth/token")) { byte[] encoded; try { encoded = Base64.encode("fooClientIdPassword:secret".getBytes("UTF-8")); ctx.addZuulRequestHeader("Authorization", "Basic " + new String(encoded)); } catch (UnsupportedEncodingException e) { logger.error("Error occured in pre filter", e); } } return null; } @Override public boolean shouldFilter() { return true; } @Override public int filterOrder() { return -2; } @Override public String filterType() { return "pre"; } }
Теперь имейте в виду, что это не добавляет никакой дополнительной безопасности, и единственная причина, по которой мы это делаем, заключается в том, что конечная точка токена защищена базовой аутентификацией с использованием учетных данных клиента.
С точки зрения реализации, тип фильтра особенно стоит отметить. Мы используем фильтр типа “pre” для обработки запроса перед его передачей.
5. Поместите маркер обновления в файл cookie
Перейдем к забавным вещам.
То, что мы планируем сделать здесь, – это заставить клиента получить маркер обновления в виде файла cookie. Не просто обычный файл cookie, а защищенный файл cookie HttpOnly с очень ограниченным путем ( /oauth/token ).
Мы настроим постфильтр Zuul для извлечения токена обновления из тела JSON ответа и установим его в файле cookie:
@Component public class CustomPostZuulFilter extends ZuulFilter { private ObjectMapper mapper = new ObjectMapper(); @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); try { InputStream is = ctx.getResponseDataStream(); String responseBody = IOUtils.toString(is, "UTF-8"); if (responseBody.contains("refresh_token")) { MapresponseMap = mapper.readValue( responseBody, new TypeReference
Здесь нужно понять несколько интересных вещей:
- Мы использовали постфильтр Zuul для чтения ответа и извлечения токена обновления
- Мы удалили значение refresh_token из ответа JSON, чтобы убедиться, что оно никогда не будет доступно переднему концу вне файла cookie
- Мы устанавливаем максимальный возраст файла cookie на 30 дней -так как это соответствует времени истечения срока действия токена
Чтобы добавить дополнительный уровень защиты от CSRF-атак, мы добавим заголовок файла cookie того же сайта ко всем нашим файлам cookie .
Для этого мы создадим класс конфигурации:
@Configuration public class SameSiteConfig implements WebMvcConfigurer { @Bean public TomcatContextCustomizer sameSiteCookiesConfig() { return context -> { final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor(); cookieProcessor.setSameSiteCookies(SameSiteCookies.STRICT.getValue()); context.setCookieProcessor(cookieProcessor); }; } }
Здесь мы устанавливаем атрибут strict , так что любая межсайтовая передача файлов cookie строго запрещена.
6. Получите и используйте токен обновления из файла Cookie
Теперь, когда у нас есть токен обновления в файле cookie, когда интерфейсное приложение AngularJS пытается вызвать обновление токена, оно отправит запрос по адресу /oauth/token , и поэтому браузер, конечно же, отправит этот файл cookie.
Итак, теперь у нас будет еще один фильтр в прокси – сервере, который извлечет токен обновления из файла cookie и отправит его вперед в качестве параметра HTTP-так что запрос будет действителен:
public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); ... HttpServletRequest req = ctx.getRequest(); String refreshToken = extractRefreshToken(req); if (refreshToken != null) { Mapparam = new HashMap (); param.put("refresh_token", new String[] { refreshToken }); param.put("grant_type", new String[] { "refresh_token" }); ctx.setRequest(new CustomHttpServletRequest(req, param)); } ... } private String extractRefreshToken(HttpServletRequest req) { Cookie[] cookies = req.getCookies(); if (cookies != null) { for (int i = 0; i < cookies.length; i++) { if (cookies[i].getName().equalsIgnoreCase("refreshToken")) { return cookies[i].getValue(); } } } return null; }
А вот наш CustomHttpServletRequest – используется для инъекции наших параметров токена обновления :
public class CustomHttpServletRequest extends HttpServletRequestWrapper { private MapadditionalParams; private HttpServletRequest request; public CustomHttpServletRequest( HttpServletRequest request, Map additionalParams) { super(request); this.request = request; this.additionalParams = additionalParams; } @Override public Map getParameterMap() { Map map = request.getParameterMap(); Map param = new HashMap (); param.putAll(map); param.putAll(additionalParams); return param; } }
Опять же, здесь много важных замечаний по реализации:
- Прокси-сервер извлекает токен обновления из файла cookie
- Затем он устанавливает его в параметр refresh_token
- Он также устанавливает grant_type в refresh_token
- Если нет refresh Token cookie (либо истекший срок действия, либо первый логин) – то запрос токена доступа будет перенаправлен без каких-либо изменений
7. Обновление маркера доступа из AngularJS
Наконец, давайте изменим наше простое интерфейсное приложение и фактически воспользуемся обновлением токена:
Вот наша функция refresh Access Token() :
$scope.refreshAccessToken = function() { obtainAccessToken($scope.refreshData); }
А вот наш $scope.refresh Data :
$scope.refreshData = {grant_type:"refresh_token"};
Обратите внимание, как мы просто используем существующую функцию obtainAccessToken – и просто передаем ей различные входные данные.
Также обратите внимание, что мы не добавляем refresh_token сами – так как об этом позаботится фильтр Zuul.
8. Заключение
В этом уроке OAuth мы узнали, как хранить токен обновления в клиентском приложении AngularJS, как обновить токен доступа с истекшим сроком действия и как использовать прокси-сервер Zuul для всего этого.
Полную реализацию этого учебника можно найти в проекте github .