연습 - 사용자 인증 추가
쇼핑 목록 웹앱에는 사용자 인증이 필요합니다. 이 연습에서는 앱에서 로그인 및 로그아웃을 구현하고 현재 사용자 로그인 상태를 표시합니다.
이 연습에서는 다음 단계를 완료합니다.
- 로컬 개발을 위해 Static Web Apps CLI를 설치합니다.
- 로컬 인증 에뮬레이션을 통해 로컬로 앱 및 API를 실행합니다.
- 여러 인증 공급자에 대한 로그인 단추를 추가합니다.
- 사용자가 로그인한 경우 로그아웃 단추를 추가합니다.
- 사용자의 로그인 상태를 표시합니다.
- 로컬로 인증 워크플로를 테스트합니다.
- 업데이트된 앱을 배포합니다.
로컬 개발을 위한 준비
SWA CLI라고도 하는 Static Web Apps CLI는 웹앱 및 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
[https://www.microsoft.com]\(
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() {}, }, }; },
methods 섹션에
getUserInfo()
메서드를 추가합니다.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;
첫 번째
</nav>
요소 뒤에 있는 JSX 템플릿에 다음 코드를 추가하여 로그인 및 로그아웃 단추를 표시합니다.<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
속성은 로그인에 사용된 ID에 따라 사용자 이름 또는 이메일 주소일 수 있습니다.
이제 완료된 파일이 다음과 같이 표시됩니다.
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
파일을 편집하고 마지막으로 닫는 </nav>
태그 뒤에 있는 JSX 템플릿 맨 아래에 해당 코드를 추가하여 로그인 상태를 표시합니다.
{
userInfo && (
<div>
<div className="user">
<p>Welcome</p>
<p>{userInfo && userInfo.userDetails}</p>
<p>{userInfo && userInfo.identityProvider}</p>
</div>
</div>
)
}
참고
userDetails
속성은 로그인에 사용된 ID에 따라 사용자 이름 또는 이메일 주소일 수 있습니다.
이제 완료된 파일이 다음과 같이 표시됩니다.
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
속성은 로그인에 사용된 ID에 따라 사용자 이름 또는 이메일 주소일 수 있습니다.
이제 완료된 파일이 다음과 같이 표시됩니다.
<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
속성은 로그인에 사용된 ID에 따라 사용자 이름 또는 이메일 주소일 수 있습니다.
이제 완료된 파일이 다음과 같이 표시됩니다.
<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>
로컬로 인증 테스트
이제 모든 것이 한 곳에 있습니다. 마지막 단계는 모든 것이 예상대로 작동하는지 테스트하는 것입니다.
웹앱에서 로그인할 ID 공급자 중 하나를 선택합니다.
다음 페이지로 리디렉션됩니다.
이 화면은 SWA CLI에서 제공하는 가짜 인증 화면으로, 사용자 세부 정보를 제공하여 로컬로 인증을 테스트할 수 있습니다.
사용자 이름으로
mslearn
을 입력하고 사용자 ID로1234
를 입력합니다.로그인을 선택합니다.
로그인한 후 이전 페이지로 리디렉션됩니다. 로그인 단추가 로그아웃 단추로 바뀐 것을 볼 수 있습니다. 로그아웃 단추 아래에 사용자 이름 및 선택한 공급자가 나타난 것도 확인할 수 있습니다.
로컬에서 모든 것이 예상대로 작동하는지 확인했으므로 이제 변경 내용을 배포할 차례입니다.
두 터미널에서 Ctrl+C를 눌러 실행 중인 앱과 API를 중지할 수 있습니다.
변경 내용 배포
Visual Studio Code에서 F1 키를 눌러 명령 팔레트를 엽니다.
Git: Commit All을 입력하고 선택합니다.
커밋 메시지로
Add authentication
을 입력하고 Enter 키를 누릅니다.F1 키를 눌러 명령 팔레트를 엽니다.
Git: Push를 입력하고 선택한 다음 Enter 키를 누릅니다.
변경 내용을 푸시한 후 빌드 및 배포 프로세스가 실행될 때까지 기다립니다. 그러면 배포된 앱에 변경 내용이 표시됩니다.
다음 단계
이제 앱에서 사용자 인증을 지원하며 다음 단계는 앱의 일부 부분을 인증되지 않은 사용자로 제한하는 것입니다.