[Перевод] Не создавайте отдельные пути для sign-in

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

  1. Если пользователь не аутентифицирован, перенаправить его по пути /sign-in:
    vfca_dtwrftnv88sogib_kiwo9c.png
  2. Если пользователь не аутентифицирован, показать ему форму входа по URL страницы, которую он пытался открыть, без перенаправления и отдельного пути:
    ujqikm9n1rpbiirmipqbvmiueni.png


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

Для настройки таких перенаправлений создаются страницы без проверки аутентификации, а проверка выполняется какими-нибудь функциями, вызываемыми на этапе маршрутизации. Они гарантируют, что целевые страницы будут открываться, только если пользователь аутентифицирован.


1. Страница без семантики


Как правило, страница с URL нужна тогда, когда пользователи могут захотеть сохранить её в закладки, вернуться к ней или с кем-нибудь ею поделиться. Кто захочет делиться URL с /sign-in?

2. Необходимость обратного перенаправления


После аутентификации пользователя его нужно перенаправить на страницу, которая ему была нужна. Это значит, что такой URL необходимо передать странице входа в виде ?back=/profile.

Это добавляет необходимость дополнительного управления и особые случаи ошибочного или отсутствующего URL возврата.

И это просто выглядит некрасиво в адресной строке.

3. Запись в истории браузера


Если пользователь нажал на ссылку /profile, его перенаправило на /sign-in?…, а потом обратно на /profile, то это создаст три записи в истории браузера.

При нажатии «назад» пользователь ожидает, что он попадёт на страницу, где нажимал ссылку на профиль. Но вместо этого он попадает на /sign-in?…, которая снова перенаправляет его на /profile просто потому, что он аутентифицирован.

Пользователи из начала 2000-х уже привыкли дважды нажимать на кнопку «назад», чтобы избежать мешающих перенаправлений на страницу входа, но это не та полезная привычка, которую мы хотим вырабатывать.

4. По-прежнему существует ситуация анонимного просмотра пути


В современных веб-приложениях в памяти находится стек страниц во фронтенде. Например, стек может быть таким:

//profile/profile/edit

Теперь представьте, что у пользователя автоматически произошёл выход из /profile/edit по таймауту.

Вы можете перенаправить его на /sign-in, однако в стеке под ней всё ещё находится /profile. Эта страница неактивна и не перестраивается, однако всё равно имеет какое-то состояние, может содержать отложенный код для исполнения и отобразится, если из стека будет извлечена верхняя страница. А виджеты всё равно должны проектироваться так, чтобы перестраивание могло происходить в любой момент и с произвольной частотой.

Можно написать охранные функции, извлекающие из стека все страницы, которые требуют аутентификации, но это снижает удобство для пользователя. Если пользователь снова выполнит вход, его стек страниц будет утерян.

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


Итак, у вас не должно быть отдельного URL для входа. Вместо этого нужно отображать форму входа в любом URL, требующем аутентификации.

Её структура зависит от фреймворка.

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

В Flutter-приложениях я обычно делаю следующее:

  1. Создаю блок или любой другой объект бизнес-логики, являющийся источником истины для состояния аутентификации в приложении.
  2. Создаю AuthenticatedOrNotWidget с двумя билдерами: один для случая с аутентификацией, другой для случая без аутентификации. Этот билдер слушает блок аутентификации и выполняет перестройку в случае изменения данных аутентификации. Если был выполнен выход пользователя из системы, то на смену приходят билдеры каждого экрана для случая без аутентификации. Если пользователь снова выполняет вход, то снова вызываются билдеры для случая с аутентификацией. В качестве бонуса виджет автоматически выполняет перестройку, если что-то изменилось в профиле пользователя.
  3. Если экраны имеют блоки или уведомители об изменениях, то сделайте так, чтобы они наследовали от какого-то суперкласса, знающего об аутентификации и вызывающего их бизнес-методы только если с аутентификацией всё в порядке. Эта архитектура зависит от конкретного приложения.


Вот виджет одного из моих проектов:

class AuthenticatedOrNotWidget extends StatefulWidget {
  final ValueWidgetBuilder authenticatedBuilder;
  final ValueWidgetBuilder notAuthenticatedBuilder;
  final ValueWidgetBuilder? progressBuilder;
  final ValueWidgetBuilder? failedBuilder;

  AuthenticatedOrNotWidget({
    required this.authenticatedBuilder,
    required this.notAuthenticatedBuilder,
    this.progressBuilder,
    this.failedBuilder,
  });

  @override
  State createState() =>
      _AuthenticatedOrNotWidgetState();
}

class _AuthenticatedOrNotWidgetState extends State {
  final _authenticationCubit = GetIt.instance.get();

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: _authenticationCubit.outState,
      builder: (context, snapshot) => _buildWithState(
          context, snapshot.data ?? _authenticationCubit.initialState),
    );
  }

  Widget _buildWithState(BuildContext context, AuthenticationState state) {
    switch (state.status) {
      case AuthenticationStatus.authenticated:
        return _buildAuthenticated(context, state);

      case AuthenticationStatus.notAuthenticated:
        return _buildNotAuthenticated(context, state);

      case AuthenticationStatus.unknown:
        return _buildProgress(context, state);

      case AuthenticationStatus.failed:
        return _buildFailed(context, state);
    }
  }

  Widget _buildAuthenticated(BuildContext context, AuthenticationState state) {
    return widget.authenticatedBuilder(context, state, null);
  }

  Widget _buildNotAuthenticated(BuildContext context, AuthenticationState state) {
    return widget.notAuthenticatedBuilder(context, state, null);
  }

  Widget _buildProgress(BuildContext context, AuthenticationState state) {
    return (widget.progressBuilder ?? widget.notAuthenticatedBuilder)(
      context,
      state,
      null,
    );
  }

  Widget _buildFailed(BuildContext context, AuthenticationState state) {
    return (widget.failedBuilder ?? widget.notAuthenticatedBuilder)(
      context,
      state,
      null,
    );
  }
}


А вот ещё более удобный виджет поверх первого, с одним только билдером для целевой страницы:

class SignInIfNotWidget extends StatelessWidget {
  final ValueWidgetBuilder signedInBuilder;

  SignInIfNotWidget({
    required this.signedInBuilder,
  });

  @override
  Widget build(BuildContext context) {
    return AuthenticatedOrNotWidget(
      authenticatedBuilder: signedInBuilder,
      notAuthenticatedBuilder: (_, __, ___) => MySignInWidget(),
      progressBuilder: (_, __, ___) => MyCircularProgressIndicator(),
    );
  }
}

Habrahabr.ru прочитано 18925 раз