Comments 24
В случае с корпоративными системами, пару лет назад делал вещи просто LDAP с последующим сохранением сессии в куках и с SSL сертификатом от локального ЦС, внутри компании. И на тот момент посчитал достаточным секьюрность, и дальнейшее усугубление уже лишним в корпоративной среде.
Хотя среда среде рознь, может и есть компании где этого мало для отдела ИБ.
Вот как раз применение JWT в браузерах мне кажется неоправданным в большинстве случаев. Плюсы есть, но больше для серверной части и при условии, что авторизационная информация последнее, что осталось у пользователя в состоянии сессии. Для нас основным минусом (кроме усложнения фронта) стала невозможность оперативно изменяь права или вооюще блокировать пользователя.
На фронте — организовывать хранение токена при переходе между страницами (в случае SPA — при обновлении страницы) и добавлять его в каждый запрос. На бэке — оперативно изменять права и отзывать токены в целом ("банить").
Ну и для сложных систем информации в токене о пользователе может быть очень много, многократно превышая размеры всех остальных заголовков и данных запроса — тут не сложность скорее, а эффективность.
А можно поподробнее эту схему описать на уровне фронта и HTTP, асбстрагируясь от каки-то техналогий типа IdentityServer4 и ImplicitFlow ?
Ну IdentityServer — это OAuth2.0/OpenIdConnect сервер, реализованный по спецификациям.
ImplicitFlow — это стандартный flow из спецификации OAuth2.0 и OpenIdConnect.
Работает следующим образом:
- Пользователь открывает https://example.com (FrontEnd)
- Фронт в процессе инициализации залазит в localStorage в поисках токена. Если токен найден, пытается получить информацию о пользователе, обращаясь к UserInfoEndpoint. Если ответ не 200, то стираем всю информацию об авторизации и считаем пользователя неавторизованным, если же 200ка, то сохраняем информацию о пользователе в TypeScript-сервисе (который Angular потом инжектит куда нужно) и работаем как обычно.
- Если пользователь не авторизован, открываем ему страницу входа с кнопкой "Войти". По нажатию на кнопку, юзер отправляется на сервер авторизации (он же 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=чего-то
- Попадая на IdentityServer, пользователь авторизуется через логин-пароль/вк/google/facebook/whatever you want. После этого его редиректит на https://example.com/login-callback, где его ожидает сервис, отвечающий за авторизацию. Т.е. фронт в данном случае у нас перезагружается полностью (ну или нет, если кэширование настроили). На колбэке сидит сервис, который записывает информацию о токенах в localstorage, и, который после этого опять же дёргает метод для получения информации о пользователе (который был в п.2), только после успешного выполнения которого мы продолжаем работу.
- По ходу работы у нас в фоне периодически выполняются обращения к IdentityServer за новыми токенами. И вот тут самое важное. До тех пор, пока сессия на IdentityServer открыта, повторная авторизация в нём не требуется и он, глядя, что с куками всё ок — выдаёт новые токены, попутно обновляя информацию о юзере и устанавливая новые куки, тем самым продлевая время жизни сессии.
Резюмируя вышеописанное. До тех пор пока открыта сессия на IdentityServer, пользователь без проблем будет пользоваться приложением. Может возникнуть вопрос по п.2 — а что если юзер ушёл на 2 часа, закрыв приложение, а сессия валидна 8 часов? Что будет при повторном посещении? А будет вот что — фронт увидит, что токен протух, сотрёт его и редиректнет пользователя на страницу входа. Он нажмёт кнопку "Войти" и дальше просто последует череда редиректов с попутным обновлением сессии на IdentityServer.
А дальше приложение просто делает запросы, цепляя в заголовок access_token, который backend уже сам проверяет, периодически обращаясь к IdentityServer и кэшируя ответы, чтобы сильно его запросами не спамить.
Как-то так)
Все таки не понятно, какие преимущества у session storage перед local?
sessionStorage — хранилище, который живет только в оперативной памяти. Как только завершается работа вкладки/браузера, удаляется все содержимое.
localStorage — хранилище, который пишется на HDD, и актуален даже после закрытия браузера или перезагрузки устройства.
Тут мы видим то самое отличие sessionStorage перед 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 — Имплементация безопасных запросов к GraphQL API посредством JWT-токенов