练习 - 添加用户身份验证

已完成

你的购物列表 Web 应用需要用户身份验证。 在此练习中,你将在应用中实现登录和注销,并显示当前用户登录状态。

在本练习中,你将完成以下步骤:

  1. 安装 Static Web Apps CLI 以进行本地开发。
  2. 使用本地身份验证仿真在本地运行应用和 API。
  3. 为多个身份验证提供程序添加登录按钮。
  4. 如果用户已登录,则添加“注销”按钮。
  5. 显示用户的登录状态。
  6. 在本地测试身份验证工作流。
  7. 部署已更新的应用。

准备进行本地开发

Static Web Apps CLI(也称为 SWA CLI)是一个本地开发工具,可用于在本地运行 Web 应用和 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
    

    保持此服务器在后台运行。 现在使用 SWA CLI 运行 API 和身份验证服务器模拟器。

  6. 在 Visual Studio Code 中,按 F1 打开命令面板。

  7. 输入并选择“终端: 创建新的集成终端”。

  8. 运行以下命令,以运行 SWA CLI:

    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 CLI 使用的最后一个端口不同于之前看到的端口,因为它使用反向代理将请求转发到三个不同的组件:

  • 框架开发服务器
  • 身份验证和授权仿真器
  • Functions 运行时托管的 API

Static Web Apps 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 接口并更新导入语句以导入 OnInitUserInfo。 此代码在组件初始化时提取用户信息。

    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 Web 应用的屏幕截图。

  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 Web 应用的屏幕截图。

  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 Web 应用的屏幕截图。

  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 Web 应用的屏幕截图。

显示用户登录状态

在测试身份验证工作流之前,让我们显示有关已登录用户的用户详细信息。

编辑文件 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. 在 Web 应用中,选择标识提供者之一登录。

  2. 你将重定向到以下页面:

    显示 SWA CLI 虚拟身份验证屏幕的屏幕截图。

    这是 SWA CLI 提供的虚拟身份验证屏幕,允许你通过提供用户详细信息在本地测试身份验证。

  3. 输入 mslearn 作为用户名,输入 1234 作为用户 ID。

  4. 选择“登录名”。

    登录后,你将被重定向到上一页。 你可以看到登录按钮已被注销按钮取代。 你还可以在注销按钮下看到你的用户名和所选提供程序。

    检查确认所有项都可按预期方式在本地运行后,就可以部署所做的更改了。

  5. 可以通过在两个终端中按 Ctrl-C 来停止正在运行的应用和 API。

部署所做的更改

  1. 在 Visual Studio Code 中,按 F1 打开命令面板。

  2. 输入并选择“Git: 全部提交”。

  3. 输入 Add authentication 作为提交消息,然后按 Enter

  4. F1 打开命令面板。

  5. 输入并选择“Git: 推送”,然后按 Enter

推送更改后,请等待生成和部署进程运行。 之后,这些更改应该会在部署的应用上可见。

后续步骤

应用现在支持用户身份验证,下一步是将应用的某些部分限制为未经身份验证的用户。