Pull to refresh

Comments 24

В своих приложениях на Angularе я остановился на OAuth access token. Принципы авторизации примерно похожи с описанным выше. Правда работу по обслуживанию токена обернул в другой класс, чтобы код в гварде был более читаем, без «внутренней кухни».
Поработав с JWT-токенами на фронте корпоративной системы (авторизация сотрудников и т. п.) пришёл к выводу, что они неоправданно усложняют и фронт, и бэк без ощутимых плюсов перед привычными сессиями. Вот хранить токен в сессии на фронт-шлюзе для передачи его бэкенд- сервисам, упрощает многое.
Наверное многое зависит от применимости, оправданности или необходимости технологии в конкретной ситуации.
В случае с корпоративными системами, пару лет назад делал вещи просто LDAP с последующим сохранением сессии в куках и с SSL сертификатом от локального ЦС, внутри компании. И на тот момент посчитал достаточным секьюрность, и дальнейшее усугубление уже лишним в корпоративной среде.
Хотя среда среде рознь, может и есть компании где этого мало для отдела ИБ.

Вот как раз применение JWT в браузерах мне кажется неоправданным в большинстве случаев. Плюсы есть, но больше для серверной части и при условии, что авторизационная информация последнее, что осталось у пользователя в состоянии сессии. Для нас основным минусом (кроме усложнения фронта) стала невозможность оперативно изменяь права или вооюще блокировать пользователя.

В чем сложность?

На фронте — организовывать хранение токена при переходе между страницами (в случае SPA — при обновлении страницы) и добавлять его в каждый запрос. На бэке — оперативно изменять права и отзывать токены в целом ("банить").


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

На фронте — организовывать хранение токена при переходе между страницами (в случае SPA — при обновлении страницы) и добавлять его в каждый запрос. На бэке — оперативно изменять права и отзывать токены в целом («банить»).

Засунуть токен в куку, локалсторадж это разве проблема?

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

С хранением токена в куке вам придется цеплять к запросу csrf токен


хрен редьки не слаще

У нас backend на ASP.NET Core, а вот фронт как раз на Angular 4. На бэке — IdentityServer4, на фронте oidc-client-js. Те же яйца, только в профиль. Тоже JWT юзаем. На бэке включены CORS, а фронт по ImplicitFlow работает с silent renew. Таким образом, отпадает необходимость в refresh-token+можно относительно быстро отреагировать на изменение прав пользователя. Если на IdentityServer'е пользователя заблокируют — его просто выпрет из фронта. Или если новых фич добавят — они у него просто сами в интерфейсе появятся. Всё сертифицировано OpenIdConnect. По мне, так на фронте лучше не хранить refresh_token без возможности инвалидации.
по ImplicitFlow вы как повторно token получаете? Мы перешли на CodeFlow(чтобы не хранить локально логин пароль) и храним только access & refresh, access протухaет через 15 минут, refresh обновляется при каждом запросе access.
Повторно токены получаем через silent renew. Для пользователя всё прозрачно — он авторизуется на IdentityServer, а далее, до тех пор пока активна сессия на стороне IdentityServer, токены скрытно обновляются. Никаких попапов, редиректов или чего-то там ещё. Если в промежутке между обновлениями были внесены изменения в учётную запись пользователя (добавили новую роль, например), то они отражаются в токене при следующем его обновлении, и на основе новых данных реактивно перестраивается фронт. Типа не было кнопочек каких-то, оппа- появились :-) Пользователи довольны, самое главное) Но чтоб такой трюк провернуть, нужно при каждой валидация кук на IdentityServer'е, актуализировать их. Тайм-аут обновления на фронте — 5 минут, срок жизни access_token — 60 минут. Как-то так

А можно поподробнее эту схему описать на уровне фронта и HTTP, асбстрагируясь от каки-то техналогий типа IdentityServer4 и ImplicitFlow ?

Ну IdentityServer — это OAuth2.0/OpenIdConnect сервер, реализованный по спецификациям.
ImplicitFlow — это стандартный flow из спецификации OAuth2.0 и OpenIdConnect.
Работает следующим образом:


  1. Пользователь открывает https://example.com (FrontEnd)
  2. Фронт в процессе инициализации залазит в localStorage в поисках токена. Если токен найден, пытается получить информацию о пользователе, обращаясь к UserInfoEndpoint. Если ответ не 200, то стираем всю информацию об авторизации и считаем пользователя неавторизованным, если же 200ка, то сохраняем информацию о пользователе в TypeScript-сервисе (который Angular потом инжектит куда нужно) и работаем как обычно.
  3. Если пользователь не авторизован, открываем ему страницу входа с кнопкой "Войти". По нажатию на кнопку, юзер отправляется на сервер авторизации (он же IdentityServer) https://identity.example.com через Implicit flow.
    Это будет запрос вида:
    GET https://identity.example.com/connect/authorize?response_type=id_token token&client_id=frontend-app&redirect_uri=https://example.com/login-callback&scope=openid profile api_v1&nonce=чего-то
  4. Попадая на IdentityServer, пользователь авторизуется через логин-пароль/вк/google/facebook/whatever you want. После этого его редиректит на https://example.com/login-callback, где его ожидает сервис, отвечающий за авторизацию. Т.е. фронт в данном случае у нас перезагружается полностью (ну или нет, если кэширование настроили). На колбэке сидит сервис, который записывает информацию о токенах в localstorage, и, который после этого опять же дёргает метод для получения информации о пользователе (который был в п.2), только после успешного выполнения которого мы продолжаем работу.
  5. По ходу работы у нас в фоне периодически выполняются обращения к IdentityServer за новыми токенами. И вот тут самое важное. До тех пор, пока сессия на IdentityServer открыта, повторная авторизация в нём не требуется и он, глядя, что с куками всё ок — выдаёт новые токены, попутно обновляя информацию о юзере и устанавливая новые куки, тем самым продлевая время жизни сессии.

Резюмируя вышеописанное. До тех пор пока открыта сессия на IdentityServer, пользователь без проблем будет пользоваться приложением. Может возникнуть вопрос по п.2 — а что если юзер ушёл на 2 часа, закрыв приложение, а сессия валидна 8 часов? Что будет при повторном посещении? А будет вот что — фронт увидит, что токен протух, сотрёт его и редиректнет пользователя на страницу входа. Он нажмёт кнопку "Войти" и дальше просто последует череда редиректов с попутным обновлением сессии на IdentityServer.


А дальше приложение просто делает запросы, цепляя в заголовок access_token, который backend уже сам проверяет, периодически обращаясь к IdentityServer и кэшируя ответы, чтобы сильно его запросами не спамить.


Как-то так)

Спасибо. Попробую реализовать.

Все таки не понятно, какие преимущества у session storage перед local?

Я бы не назвал это преимуществом, скорее отличием.
sessionStorage — хранилище, который живет только в оперативной памяти. Как только завершается работа вкладки/браузера, удаляется все содержимое.
localStorage — хранилище, который пишется на HDD, и актуален даже после закрытия браузера или перезагрузки устройства.

Тут мы видим то самое отличие sessionStorage перед localStorage — токен необходимо получать заново, если закрыл вкладку. На мой взгляд, это добавляет секьюрности, но создает дополнительные телодвижения для юзера.

И логин каждый раз по новой.


Можно сделать срок жизни токена поменьше. Иначе вы зарастёте в этих токенах.

Есть еще вариант реализации, поместить access токен в sessionStorage и сделать срок жизни покроче. А refresh токен в localStorage, в этом случае, каждый раз логин/пароль вбивать не придется, и секьюрность присутствует.
Заранее прошу прощения, если заметки будут не к теме самой архитектуры, но может Вам будет интересны мысли (исходя из фразы «Поэтому любую критику относительно кода и логики, приму как дружеский совет.»).
this.http.post('http://localhost:8080/login', ({ username: username, password: password }), options)

Думаю здесь (и в других подобных кусочках Вашего кода) стоит использовать новый сахар касающийся свойств с идентичными значениями — ({ username, password}). Значение хоста «localhost:8080» вынести в отдельную константу, если она Вам вообще нужна. Перед map сделать манипуляции .filter(res => res.status === 200).map(res => res.json()).map(data => ...).

Вещи связанные с получением самого токена и проверкой на то, является ли пользователь авторизованным…
sessionStorage.getItem('accessToken')

… стоило бы вынести в Ваш AuthService как отдельные методы isAuthorised и getAccessToken… Это позволяет не привязываться к месту хранения (вдруг Вы заходите переехать на ngrx?..)

Постарайтесь избегать использование сравнений с приведением типов…
if (token != null && refToken != null)

… это не есть хорошей практикой и часто приводит к ошибкам… В данной ситуации у Вас токены undefinedЫ.

Ещё хочу сказать, что возможно Вам стоит взглянуть на практику связанную с созданием класса-обёртки над ангуляровским http. В нём Вы можете единоразово настроить обратку ошибку, парсинг ответа и ту же работу с добавлением хедеров в Ваши запросы (кое что будет неактуально с новыми релизами фреймворка)…
Спасибо большое за труд, потраченный на анализ и советы, обязательно пройдусь по коду.
К счастью, больше не нужно делать такой велосипед) В Angular 4.3 завезли Http клиент новый и HttpInterceptor'ы к нему. По сути middleware. Можно все запросы обрабатывать до, во время отправки, во время и после получения ответа. В частности типичный юзкейс — установка заголовков авторизации.
Как я и сказал — «кое что будет неактуально с новыми релизами фреймворка» :)
Sign up to leave a comment.

Articles