练习 - 添加用户身份验证
你的购物列表 Web 应用需要用户身份验证。 在此练习中,你将在应用中实现登录和注销,并显示当前用户登录状态。
在本练习中,你将完成以下步骤:
- 安装 Static Web Apps CLI 以进行本地开发。
- 使用本地身份验证仿真在本地运行应用和 API。
- 为多个身份验证提供程序添加登录按钮。
- 如果用户已登录,则添加“注销”按钮。
- 显示用户的登录状态。
- 在本地测试身份验证工作流。
- 部署已更新的应用。
准备进行本地开发
Static Web Apps CLI(也称为 SWA CLI)是一个本地开发工具,可用于在本地运行 Web 应用和 API 并模拟身份验证和授权服务器。
在计算机上打开终端。
运行以下命令安装 SWA CLI。
npm install -g @azure/static-web-apps-cli
在本地运行应用
现在使用开发服务器在本地运行应用和 API。 这样,在代码中进行更改时,你就可以查看和测试所做的更改。
在 Visual Studio Code 中打开项目。
在 Visual Studio Code 中,按 F1 打开命令面板。
输入并选择“终端: 创建新的集成终端”。
转到首选前端框架的文件夹,如下所示:
cd angular-app
cd react-app
cd svelte-app
cd vue-app
使用开发服务器运行前端客户端应用程序。
npm start
npm start
npm run dev
npm run serve
保持此服务器在后台运行。 现在使用 SWA CLI 运行 API 和身份验证服务器模拟器。
在 Visual Studio Code 中,按 F1 打开命令面板。
输入并选择“终端: 创建新的集成终端”。
运行以下命令,以运行 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
浏览到
http://localhost:4280
。
SWA CLI 使用的最后一个端口不同于之前看到的端口,因为它使用反向代理将请求转发到三个不同的组件:
- 框架开发服务器
- 身份验证和授权仿真器
- Functions 运行时托管的 API
让应用程序在你修改代码时保持运行状态。
获取用户登录状态
首先,需要通过对客户端中的 /.auth/me
进行查询来访问用户登录状态。
创建文件
angular-app/src/app/core/models/user-info.ts
并添加以下代码来表示用户信息的接口。export interface UserInfo { identityProvider: string; userId: string; userDetails: string; userRoles: string[]; }
编辑文件
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; } }
创建新的类属性
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(); } // ... }
编辑文件
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 ( // ...
编辑文件
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; } }
编辑文件
vue-app/src/components/nav-bar.vue
,并将userInfo
添加到数据对象。data() { return { userInfo: { type: Object, default() {}, }, }; },
将
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; } }, },
将
created
生命周期挂钩添加到组件。async created() { this.userInfo = await this.getUserInfo(); },
创建组件时,会自动提取用户信息。
添加登录和注销按钮
如果用户未登录,这些用户的信息将为 undefined
,在这种情况下所做的更改将暂时不可见。 现在为不同的提供程序添加登录按钮。
编辑文件
angular-app/src/app/core/components/nav.component.ts
,以在NavComponent
类中添加提供程序列表。providers = ['x', 'github', 'aad'];
添加以下
redirect
属性,以捕获登录后重定向的当前 URL。redirect = window.___location.pathname;
将以下代码添加到模板中的第一个
</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 设置为当前页面。
现在应在浏览器中看到此网页。
编辑文件
react-app/src/components/NavBar.js
,以在函数顶部添加提供程序列表。const providers = ['x', 'github', 'aad'];
将以下
redirect
变量添加到第一个变量下面,以捕获登录后重定向的当前 URL。const redirect = window.___location.pathname;
将以下代码添加到 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 设置为当前页面。
现在应在浏览器中看到此网页。
编辑文件
svelte-app/src/components/NavBar.svelte
,以在脚本顶部添加提供程序列表。const providers = ['x', 'github', 'aad'];
将以下
redirect
变量添加到第一个变量下面,以捕获登录后重定向的当前 URL。const redirect = window.___location.pathname;
将以下代码添加到模板中的第一个
</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 设置为当前页面。
现在应在浏览器中看到此网页。
编辑文件
vue-app/src/components/nav-bar.vue
,并将提供程序列表添加到数据对象。providers: ['x', 'github', 'aad'],
添加以下
redirect
属性,以捕获登录后重定向的当前 URL。redirect: window.___location.pathname,
将以下代码添加到模板中的第一个
</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 设置为当前页面。
现在应在浏览器中看到此网页。
显示用户登录状态
在测试身份验证工作流之前,让我们显示有关已登录用户的用户详细信息。
编辑文件 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>
在本地测试身份验证
现在一切都已准备就绪。 最后一步是测试是否一切正常运行。
在 Web 应用中,选择标识提供者之一登录。
你将重定向到以下页面:
这是 SWA CLI 提供的虚拟身份验证屏幕,允许你通过提供用户详细信息在本地测试身份验证。
输入
mslearn
作为用户名,输入1234
作为用户 ID。选择“登录名”。
登录后,你将被重定向到上一页。 你可以看到登录按钮已被注销按钮取代。 你还可以在注销按钮下看到你的用户名和所选提供程序。
检查确认所有项都可按预期方式在本地运行后,就可以部署所做的更改了。
可以通过在两个终端中按 Ctrl-C 来停止正在运行的应用和 API。
部署所做的更改
在 Visual Studio Code 中,按 F1 打开命令面板。
输入并选择“Git: 全部提交”。
输入
Add authentication
作为提交消息,然后按 Enter。按 F1 打开命令面板。
输入并选择“Git: 推送”,然后按 Enter。
推送更改后,请等待生成和部署进程运行。 之后,这些更改应该会在部署的应用上可见。
后续步骤
应用现在支持用户身份验证,下一步是将应用的某些部分限制为未经身份验证的用户。