Упражнение. Добавление проверки подлинности пользователя

Завершено

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

В этом упражнении вы выполните следующие действия.

  1. Установите интерфейс командной строки для Статических веб-приложений для локальной разработки.
  2. Локально запустите приложение и API с помощью эмуляции локальной проверки подлинности.
  3. Добавите кнопки входа в систему для нескольких поставщиков проверки подлинности.
  4. Добавите кнопку выхода из системы, если пользователь вошел в систему.
  5. Отобразите состояние входа пользователя.
  6. Локально протестируете рабочий процесс проверки подлинности.
  7. Развернете обновленное приложение.

Подготовка к локальной разработке

Интерфейс командной строки для Статических веб-приложений, известный также как SWA CLI, — это локальное средство разработки, которое позволяет запускать веб-приложение и API на локальном компьютере и эмулировать серверы проверки подлинности и авторизации.

  1. Откройте терминал на своем компьютере.

  2. Установите SWA CLI, выполнив приведенную ниже команду.

    npm install -g @azure/static-web-apps-cli
    

Локальный запуск приложения

Теперь запустите приложение и API локально на сервере разработки. Таким образом вы сможете просматривать и тестировать изменения по мере их внесения в код.

  1. Откройте проект в Visual Studio Code.

  2. Откройте палитру команд Visual Studio Code, нажав клавишу F1.

  3. Введите и выберите Терминал: создать встроенный терминал.

  4. Перейдите в папку предпочтительной интерфейсной платформы, как показано ниже.

    cd angular-app
    
    cd react-app
    
    cd svelte-app
    
    cd vue-app
    
  5. Запустите интерфейсное клиентское приложение с помощью сервера разработки.

    npm start
    
    npm start
    
    npm run dev
    
    npm run serve
    

    Оставьте этот сервер работать в фоновом режиме. Теперь запустите API и эмулятор сервера проверки подлинности с помощью SWA CLI.

  6. Откройте палитру команд Visual Studio Code, нажав клавишу F1.

  7. Введите и выберите Терминал: создать встроенный терминал.

  8. Запустите интерфейс командной строки SWA, выполнив следующую команду.

    swa start http://localhost:4200 --api-location ./api
    
    swa start http://localhost:3000 --api-location ./api
    
    swa start http://localhost:5000 --api-location ./api
    
    swa start http://localhost:8080 --api-location ./api
    
  9. Перейдите в http://localhost:4280.

Последний порт, используемый интерфейсом командной строки SWA, отличается от того, который вы видели ранее, так как он использует обратный прокси-сервер для пересылки запросов в три различных приведенных ниже компонента.

  • Сервер разработки платформы.
  • Эмулятор проверки подлинности и авторизации.
  • API, размещенный в среде выполнения функций.

Снимок экрана: архитектура Статические веб-приложения CLI.

Пусть приложение работает, пока вы будете изменять код.

Получение состояния входа пользователя

Сначала вам необходимо получить доступ к состоянию входа пользователя, выполнив запрос /.auth/me в клиенте.

  1. Создайте файл angular-app/src/app/core/models/user-info.ts и добавьте следующий код, который представляет интерфейс для сведений о пользователе.

    export interface UserInfo {
      identityProvider: string;
      userId: string;
      userDetails: string;
      userRoles: string[];
    }
    
  2. Измените файл angular-app/src/app/core/components/nav.component.ts и добавьте следующий метод в класс NavComponent.

    async getUserInfo() {
      try {
        const response = await fetch('/.auth/me');
        const payload = await response.json();
        const { clientPrincipal } = payload;
        return clientPrincipal;
      } catch (error) {
        console.error('No profile could be found');
        return undefined;
      }
    }
    
  3. Создайте свойство класса userInfo и сохраните результат асинхронной функции getUserInfo() при инициализации компонента. Реализуйте интерфейс OnInit и обновите инструкции импорта для импорта OnInit и UserInfo. Этот код получает сведения о пользователе при инициализации компонента.

    import { Component, OnInit } from '@angular/core';
    import { UserInfo } from '../model/user-info';
    
    export class NavComponent implements OnInit {
      userInfo: UserInfo;
    
      async ngOnInit() {
        this.userInfo = await this.getUserInfo();
      }
      // ...
    }
    
  1. Измените файл react-app/src/components/NavBar.js и добавьте следующий код в начало функции. Этот код получает сведения о пользователе при загрузке компонента и сохраняет их в состоянии.

    import React, { useState, useEffect } from 'react';
    import { NavLink } from 'react-router-dom';
    
    const NavBar = (props) => {
      const [userInfo, setUserInfo] = useState();
    
      useEffect(() => {
        (async () => {
          setUserInfo(await getUserInfo());
        })();
      }, []);
    
      async function getUserInfo() {
        try {
          const response = await fetch('/.auth/me');
          const payload = await response.json();
          const { clientPrincipal } = payload;
          return clientPrincipal;
        } catch (error) {
          console.error('No profile could be found');
          return undefined;
        }
      }
    
      return (
      // ...
    
  1. Измените файл svelte-app/src/components/NavBar.svelte и добавьте следующий код в раздел скрипта. Этот код получает сведения о пользователе при загрузке компонента.

    import { onMount } from 'svelte';
    
    let userInfo = undefined;
    
    onMount(async () => (userInfo = await getUserInfo()));
    
    async function getUserInfo() {
      try {
        const response = await fetch('/.auth/me');
        const payload = await response.json();
        const { clientPrincipal } = payload;
        return clientPrincipal;
      } catch (error) {
        console.error('No profile could be found');
        return undefined;
      }
    }
    
  1. Измените файл vue-app/src/components/nav-bar.vue и добавьте userInfo в объект данных.

     data() {
       return {
         userInfo: {
           type: Object,
           default() {},
         },
       };
     },
    
  2. Добавьте метод getUserInfo() в раздел methods.

    methods: {
      async getUserInfo() {
        try {
          const response = await fetch('/.auth/me');
          const payload = await response.json();
          const { clientPrincipal } = payload;
          return clientPrincipal;
        } catch (error) {
          console.error('No profile could be found');
          return undefined;
        }
      },
    },
    
  3. Добавьте в компонент обработчик жизненного цикла created.

    async created() {
      this.userInfo = await this.getUserInfo();
    },
    

    После создания компонента сведения о пользователе извлекаются автоматически.

Добавление кнопок входа и выхода

Сведения о пользователе будут иметь свойство undefined, если вы не вошли в систему, поэтому сейчас изменения не будут видны. Пора добавить кнопки входа для разных поставщиков.

  1. Измените файл angular-app/src/app/core/components/nav.component.ts, добавив список поставщиков в класс NavComponent.

    providers = ['x', 'github', 'aad'];
    
  2. Добавьте следующее свойство redirect, чтобы записать текущий URL-адрес для перенаправления после входа.

    redirect = window.location.pathname;
    
  3. Добавьте следующий код в шаблон после первого элемента </nav>, чтобы отобразить кнопки входа и выхода.

    <nav class="menu auth">
      <p class="menu-label">Auth</p>
      <div class="menu-list auth">
        <ng-container *ngIf="!userInfo; else logout">
          <ng-container *ngFor="let provider of providers">
            <a href="/.auth/login/{{provider}}?post_login_redirect_uri={{redirect}}">{{provider}}</a>
          </ng-container>
        </ng-container>
        <ng-template #logout>
          <a href="/.auth/logout?post_logout_redirect_uri={{redirect}}">Logout</a>
        </ng-template>
      </div>
    </nav>
    

    Если пользователь не вошел в систему, отобразится кнопка входа для каждого поставщика. Каждая кнопка ссылается на /.auth/login/<AUTH_PROVIDER> и задает URL-адрес перенаправления для текущей страницы.

    В противном случае, если пользователь уже вошел в систему, отобразится кнопка выхода, которая ссылается на /.auth/logout, а также URL-адрес перенаправления на текущую страницу.

Вы увидите следующую страницу в браузере.

Снимок экрана: веб-приложение Angular с кнопками входа.

  1. Измените файл react-app/src/components/NavBar.js, добавив список поставщиков в верхней части функции.

    const providers = ['x', 'github', 'aad'];
    
  2. Добавьте следующую переменную redirect ниже первой переменной, чтобы записать текущий URL-адрес для перенаправления после входа.

    const redirect = window.location.pathname;
    
  3. Добавьте следующий код в шаблон JSX после первого элемента </nav>, чтобы отобразить кнопки входа и выхода.

    <nav className="menu auth">
      <p className="menu-label">Auth</p>
      <div className="menu-list auth">
        {!userInfo &&
          providers.map((provider) => (
            <a key={provider} href={`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`}>
              {provider}
            </a>
          ))}
        {userInfo && <a href={`/.auth/logout?post_logout_redirect_uri=${redirect}`}>Logout</a>}
      </div>
    </nav>
    

    Если пользователь не вошел в систему, отобразится кнопка входа для каждого поставщика. Каждая кнопка ссылается на /.auth/login/<AUTH_PROVIDER> и задает URL-адрес перенаправления для текущей страницы.

    В противном случае, если пользователь уже вошел в систему, отобразится кнопка выхода, которая ссылается на /.auth/logout, а также URL-адрес перенаправления на текущую страницу.

Вы увидите следующую страницу в браузере.

Снимок экрана: веб-приложение React с кнопками входа.

  1. Измените файл svelte-app/src/components/NavBar.svelte, добавив список поставщиков в верхней части скрипта.

    const providers = ['x', 'github', 'aad'];
    
  2. Добавьте следующую переменную redirect ниже первой переменной, чтобы записать текущий URL-адрес для перенаправления после входа.

    const redirect = window.location.pathname;
    
  3. Добавьте следующий код в шаблон после первого элемента </nav>, чтобы отобразить кнопки входа и выхода.

     <nav class="menu auth">
       <p class="menu-label">Auth</p>
       <div class="menu-list auth">
         {#if !userInfo}
           {#each providers as provider (provider)}
             <a href={`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`}>
               {provider}
             </a>
           {/each}
         {/if}
         {#if userInfo}
           <a href={`/.auth/logout?post_logout_redirect_uri=${redirect}`}>
             Logout
           </a>
         {/if}
       </div>
     </nav>
    

    Если пользователь не вошел в систему, отобразится кнопка входа для каждого поставщика. Каждая кнопка ссылается на /.auth/login/<AUTH_PROVIDER> и задает URL-адрес перенаправления для текущей страницы.

    В противном случае, если пользователь уже вошел в систему, отобразится кнопка выхода, которая ссылается на /.auth/logout, а также URL-адрес перенаправления на текущую страницу.

Вы увидите следующую страницу в браузере.

Снимок экрана: веб-приложение Svelte с кнопками входа.

  1. Измените файл vue-app/src/components/nav-bar.vue и добавьте список поставщиков в объект данных.

     providers: ['x', 'github', 'aad'],
    
  2. Добавьте следующее свойство redirect, чтобы записать текущий URL-адрес для перенаправления после входа.

     redirect: window.location.pathname,
    
  3. Добавьте следующий код в шаблон после первого элемента </nav>, чтобы отобразить кнопки входа и выхода.

    <nav class="menu auth">
      <p class="menu-label">Auth</p>
      <div class="menu-list auth">
        <template v-if="!userInfo">
          <template v-for="provider in providers">
            <a :key="provider" :href="`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`">
              {{ provider }}
            </a>
          </template>
        </template>
        <a v-if="userInfo" :href="`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`"> Logout </a>
      </div>
    </nav>
    

    Если пользователь не вошел в систему, отобразится кнопка входа для каждого поставщика. Каждая кнопка ссылается на /.auth/login/<AUTH_PROVIDER> и задает URL-адрес перенаправления для текущей страницы.

    В противном случае, если пользователь уже вошел в систему, отобразится кнопка выхода, которая ссылается на /.auth/logout, а также URL-адрес перенаправления на текущую страницу.

Вы увидите следующую страницу в браузере.

Снимок экрана: веб-приложение Vue с кнопками входа.

Отображение состояния входа пользователя

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

Измените файл angular-app/src/app/core/components/nav.component.ts и добавьте следующий код в нижнюю часть шаблона после последнего закрывающего тега </nav>.

<div class="user" *ngIf="userInfo">
  <p>Welcome</p>
  <p>{{ userInfo?.userDetails }}</p>
  <p>{{ userInfo?.identityProvider }}</p>
</div>

Примечание.

Свойство userDetails может быть либо именем пользователя, либо адресом электронной почты в зависимости от удостоверения, используемого для входа.

Готовый файл будет выглядеть следующим образом.

import { Component, OnInit } from '@angular/core';
import { UserInfo } from '../model/user-info';

@Component({
  selector: 'app-nav',
  template: `
    <nav class="menu">
      <p class="menu-label">Menu</p>
      <ul class="menu-list">
        <a routerLink="/products" routerLinkActive="router-link-active">
          <span>Products</span>
        </a>
        <a routerLink="/about" routerLinkActive="router-link-active">
          <span>About</span>
        </a>
      </ul>
    </nav>
    <nav class="menu auth">
      <p class="menu-label">Auth</p>
      <div class="menu-list auth">
        <ng-container *ngIf="!userInfo; else logout">
          <ng-container *ngFor="let provider of providers">
            <a href="/.auth/login/{{ provider }}?post_login_redirect_uri={{ redirect }}">{{ provider }}</a>
          </ng-container>
        </ng-container>
        <ng-template #logout>
          <a href="/.auth/logout?post_logout_redirect_uri={{ redirect }}">Logout</a>
        </ng-template>
      </div>
    </nav>
    <div class="user" *ngIf="userInfo">
      <p>Welcome</p>
      <p>{{ userInfo?.userDetails }}</p>
      <p>{{ userInfo?.identityProvider }}</p>
    </div>
  `,
})
export class NavComponent implements OnInit {
  providers = ['x', 'github', 'aad'];
  redirect = window.location.pathname;
  userInfo: UserInfo;

  async ngOnInit() {
    this.userInfo = await this.getUserInfo();
  }

  async getUserInfo() {
    try {
      const response = await fetch('/.auth/me');
      const payload = await response.json();
      const { clientPrincipal } = payload;
      return clientPrincipal;
    } catch (error) {
      console.error('No profile could be found');
      return undefined;
    }
  }
}

Измените файл react-app/src/components/NavBar.js и добавьте следующий код в нижнюю часть шаблона JSX после последнего закрывающего тега </nav>, чтобы отобразить состояние входа.

{
  userInfo && (
    <div>
      <div className="user">
        <p>Welcome</p>
        <p>{userInfo && userInfo.userDetails}</p>
        <p>{userInfo && userInfo.identityProvider}</p>
      </div>
    </div>
  )
}

Примечание.

Свойство userDetails может быть либо именем пользователя, либо адресом электронной почты в зависимости от удостоверения, используемого для входа.

Готовый файл будет выглядеть следующим образом.

import React, { useState, useEffect } from 'react';
import { NavLink } from 'react-router-dom';

const NavBar = (props) => {
  const providers = ['x', 'github', 'aad'];
  const redirect = window.location.pathname;
  const [userInfo, setUserInfo] = useState();

  useEffect(() => {
    (async () => {
      setUserInfo(await getUserInfo());
    })();
  }, []);

  async function getUserInfo() {
    try {
      const response = await fetch('/.auth/me');
      const payload = await response.json();
      const { clientPrincipal } = payload;
      return clientPrincipal;
    } catch (error) {
      console.error('No profile could be found');
      return undefined;
    }
  }

  return (
    <div className="column is-2">
      <nav className="menu">
        <p className="menu-label">Menu</p>
        <ul className="menu-list">
          <NavLink to="/products" activeClassName="active-link">
            Products
          </NavLink>
          <NavLink to="/about" activeClassName="active-link">
            About
          </NavLink>
        </ul>
        {props.children}
      </nav>
      <nav className="menu auth">
        <p className="menu-label">Auth</p>
        <div className="menu-list auth">
          {!userInfo &&
            providers.map((provider) => (
              <a key={provider} href={`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`}>
                {provider}
              </a>
            ))}
          {userInfo && <a href={`/.auth/logout?post_logout_redirect_uri=${redirect}`}>Logout</a>}
        </div>
      </nav>
      {userInfo && (
        <div>
          <div className="user">
            <p>Welcome</p>
            <p>{userInfo && userInfo.userDetails}</p>
            <p>{userInfo && userInfo.identityProvider}</p>
          </div>
        </div>
      )}
    </div>
  );
};

export default NavBar;

Измените файл svelte-app/src/components/NavBar.svelte и добавьте следующий код в нижнюю часть шаблона после последнего закрывающего тега </nav>, чтобы отобразить состояние входа.

{#if userInfo}
<div class="user">
  <p>Welcome</p>
  <p>{userInfo && userInfo.userDetails}</p>
  <p>{userInfo && userInfo.identityProvider}</p>
</div>
{/if}

Примечание.

Свойство userDetails может быть либо именем пользователя, либо адресом электронной почты в зависимости от удостоверения, используемого для входа.

Готовый файл будет выглядеть следующим образом.

<script>
  import { onMount } from 'svelte';
  import { Link } from 'svelte-routing';

  const providers = ['x', 'github', 'aad'];
  const redirect = window.location.pathname;
  let userInfo = undefined;

  onMount(async () => (userInfo = await getUserInfo()));

  async function getUserInfo() {
    try {
      const response = await fetch('/.auth/me');
      const payload = await response.json();
      const { clientPrincipal } = payload;
      return clientPrincipal;
    } catch (error) {
      console.error('No profile could be found');
      return undefined;
    }
  }

  function getProps({ href, isPartiallyCurrent, isCurrent }) {
    const isActive = href === '/' ? isCurrent : isPartiallyCurrent || isCurrent;

    // The object returned here is spread on the anchor element's attributes
    if (isActive) {
      return { class: 'router-link-active' };
    }
    return {};
  }
</script>

<div class="column is-2">
  <nav class="menu">
    <p class="menu-label">Menu</p>
    <ul class="menu-list">
      <Link to="/products" {getProps}>Products</Link>
      <Link to="/about" {getProps}>About</Link>
    </ul>
  </nav>
  <nav class="menu auth">
    <p class="menu-label">Auth</p>
    <div class="menu-list auth">
      {#if !userInfo}
        {#each providers as provider (provider)}
          <a href={`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`}>
            {provider}
          </a>
        {/each}
      {/if}
      {#if userInfo}
        <a href={`/.auth/logout?post_logout_redirect_uri=${redirect}`}>
          Logout
        </a>
      {/if}
    </div>
  </nav>
  {#if userInfo}
    <div class="user">
      <p>Welcome</p>
      <p>{userInfo && userInfo.userDetails}</p>
      <p>{userInfo && userInfo.identityProvider}</p>
    </div>
  {/if}
</div>

Измените файл vue-app/src/components/nav-bar.vue и добавьте следующий код в нижнюю часть шаблона после последнего закрывающего тега </nav>, чтобы отобразить состояние входа:

<div class="user" v-if="userInfo">
  <p>Welcome</p>
  <p>{{ userInfo.userDetails }}</p>
  <p>{{ userInfo.identityProvider }}</p>
</div>

Примечание.

Свойство userDetails может быть либо именем пользователя, либо адресом электронной почты в зависимости от удостоверения, используемого для входа.

Готовый файл будет выглядеть следующим образом.

<script>
  export default {
    name: 'NavBar',
    data() {
      return {
        userInfo: {
          type: Object,
          default() {},
        },
        providers: ['x', 'github', 'aad'],
        redirect: window.location.pathname,
      };
    },
    methods: {
      async getUserInfo() {
        try {
          const response = await fetch('/.auth/me');
          const payload = await response.json();
          const { clientPrincipal } = payload;
          return clientPrincipal;
        } catch (error) {
          console.error('No profile could be found');
          return undefined;
        }
      },
    },
    async created() {
      this.userInfo = await this.getUserInfo();
    },
  };
</script>

<template>
  <div column is-2>
    <nav class="menu">
      <p class="menu-label">Menu</p>
      <ul class="menu-list">
        <router-link to="/products">Products</router-link>
        <router-link to="/about">About</router-link>
      </ul>
    </nav>
    <nav class="menu auth">
      <p class="menu-label">Auth</p>
      <div class="menu-list auth">
        <template v-if="!userInfo">
          <template v-for="provider in providers">
            <a :key="provider" :href="`/.auth/login/${provider}?post_login_redirect_uri=${redirect}`">{{ provider }}</a>
          </template>
        </template>
        <a v-if="userInfo" :href="`/.auth/logout?post_logout_redirect_uri=${redirect}`">Logout</a>
      </div>
    </nav>
    <div class="user" v-if="userInfo">
      <p>Welcome</p>
      <p>{{ userInfo.userDetails }}</p>
      <p>{{ userInfo.identityProvider }}</p>
    </div>
  </div>
</template>

Тестирование проверки подлинности локально

Теперь все готово. Последний шаг — проверить, все ли работает должным образом.

  1. В веб-приложении выберите одного из поставщиков удостоверений для входа.

  2. Вы будете перенаправлены на эту страницу:

    Снимок экрана: фиктивный экран проверки подлинности SWA CLI.

    Это фиктивный экран проверки подлинности, предоставленный интерфейсом командной строки SWA. Он позволяет протестировать проверку подлинности локально, предоставив свои сведения о пользователе.

  3. Введите mslearn в качестве имени пользователя и 1234 в качестве его идентификатора.

  4. Выберите Войти.

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

    Теперь, когда вы локально проверили, что все работает правильно, пришло время развернуть изменения.

  5. Вы можете отключить работающее приложение и API, нажав клавиши CTRL+C на обоих терминалах.

Развертывание изменений

  1. Откройте палитру команд Visual Studio Code, нажав клавишу F1.

  2. Введите и выберите Git: зафиксировать в.

  3. Введите сообщение фиксации Add authentication и нажмите клавишу ВВОД.

  4. Откройте палитру команд, нажав клавишу F1.

  5. Введите и выберите Git: отправить и нажмите клавишу ВВОД.

После того как изменения отправлены, дождитесь выполнения процесса сборки и развертывания. После этого изменения должны быть видны в развернутом приложении.

Следующие шаги

Теперь приложение поддерживает проверку подлинности пользователей. Следующим шагом является ограничение некоторых частей приложения для пользователей, не прошедших проверку подлинности.