ASP.NET Core Blazor WebAssembly 추가 보안 시나리오

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

이 문서에서는 Blazor WebAssembly 앱에 대한 추가 보안 시나리오를 설명합니다.

나가는 요청에 토큰 연결

AuthorizationMessageHandler는 액세스 토큰을 처리하는 데 사용하는 DelegatingHandler입니다. 토큰은 프레임워크가 등록한 IAccessTokenProvider 서비스를 사용하여 획득합니다. 토큰을 획득할 수 없는 경우 AccessTokenNotAvailableException이 throw됩니다. AccessTokenNotAvailableException에는 액세스 토큰 새로 고침을 허용하기 위해 지정된 AccessTokenResult.InteractionOptions를 사용하여 AccessTokenResult.InteractiveRequestUrl로 이동하는 Redirect 메서드가 있습니다.

편의상 프레임워크는 앱의 기준 주소를 권한 있는 URL로 사용하여 미리 구성된 BaseAddressAuthorizationMessageHandler를 제공합니다. 액세스 토큰은 요청 URI가 앱의 기본 URI 내에 있는 경우에만 추가됩니다. 나가는 요청 URI가 앱의 기본 URI 내에 없는 경우 사용자 지정 AuthorizationMessageHandler 클래스(권장)를 사용하거나 구성합니다AuthorizationMessageHandler.

참고 항목

서버 API 액세스를 위한 클라이언트 앱 구성 외에도 서버 API는 클라이언트와 서버가 동일한 기준 주소에 있지 않을 때 CORS(원본 간 요청)를 허용해야 합니다. 서버 쪽 CORS 구성에 대한 자세한 내용은 이 문서의 뒷부분에 있는 CORS(원본 간 리소스 공유) 섹션을 참조하세요.

다음 예제에서

다음 예제 HttpClientFactoryServiceCollectionExtensions.AddHttpClient 에서는 .의 확장 Microsoft.Extensions.Http입니다. 패키지를 아직 참조하지 않는 앱에 추가합니다.

참고 항목

.NET 앱에 패키지를 추가하는 방법에 대한 지침은 패키지 사용 워크플로에서 패키지 설치 및 관리의 문서(NuGet 설명서)를 참조하세요. NuGet.org에서 올바른 패키지 버전을 확인합니다.

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient("WebAPI", 
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("WebAPI"));

Blazor WebAssembly프로젝트 템플릿을 기반으로 하는 호스트된 Blazor솔루션의 경우 요청 URI는 기본적으로 앱의 기본 URI 내에 있습니다. 따라서 IWebAssemblyHostEnvironment.BaseAddress(new Uri(builder.HostEnvironment.BaseAddress))은 프로젝트 템플릿에서 생성된 앱의 HttpClient.BaseAddress에 할당됩니다.

구성된 HttpClienttry-catch 패턴을 사용하여 권한이 부여된 요청을 수행하는 데 사용됩니다.

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject HttpClient Http

...

protected override async Task OnInitializedAsync()
{
    try
    {
        var examples = 
            await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

        ...
    }
    catch (AccessTokenNotAvailableException exception)
    {
        exception.Redirect();
    }
}

사용자 지정 인증 요청 시나리오

다음 시나리오에서는 인증 요청을 사용자 지정하는 방법과 인증 옵션에서 로그인 경로를 가져오는 방법을 보여 줍니다.

로그인 프로세스 사용자 지정

InteractiveRequestOptions의 새 인스턴스에서 다음 메서드를 하나 이상 사용하여 로그인 요청에 대한 추가 매개 변수를 관리합니다.

다음 LoginDisplay 구성 요소 예제에서는 로그인 요청에 추가 매개 변수가 추가됩니다.

  • promptlogin로 설정됨: 사용자가 해당 요청에 대한 자격 증명을 강제로 입력하도록 하여 싱글 사인 온을 무효화합니다.
  • loginHintpeter@contoso.com로 설정됨: 사용자가 로그인 페이지의 사용자 이름/전자 메일 주소 필드를 peter@contoso.com로 미리 채웁니다. preferred_username 클레임을 사용하여 이전 로그인 작업에서 사용자 이름이 이미 추출된 경우 앱이 재인증 과정에서 이 매개 변수를 종종 사용합니다.

Shared/LoginDisplay.razor:

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        <button @onclick="BeginLogOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <button @onclick="BeginLogIn">Log in</button>
    </NotAuthorized>
</AuthorizeView>

@code{
    public void BeginLogOut()
    {
        Navigation.NavigateToLogout("authentication/logout");
    }

    public void BeginLogIn()
    {
        InteractiveRequestOptions requestOptions =
            new()
            {
                Interaction = InteractionType.SignIn,
                ReturnUrl = Navigation.Uri,
            };

        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");

        Navigation.NavigateToLogin("authentication/login", requestOptions);
    }
}

자세한 내용은 다음 리소스를 참조하세요.

대화형으로 토큰을 가져오기 전에 옵션 사용자 지정

AccessTokenNotAvailableException 이 발생하는 경우 InteractiveRequestOptions의 새 인스턴스에서 다음 메서드를 하나 이상 사용하여 새 ID 공급자 액세스 토큰 요청에 대한 추가 매개 변수를 관리합니다.

웹 API를 통해 JSON 데이터를 가져오는 다음 예제에서는 액세스 토큰을 사용할 수 없는 경우 리디렉션 요청에 추가 매개 변수가 추가됩니다(AccessTokenNotAvailableException가 throw됨).

  • promptlogin로 설정됨: 사용자가 해당 요청에 대한 자격 증명을 강제로 입력하도록 하여 싱글 사인 온을 무효화합니다.
  • loginHintpeter@contoso.com로 설정됨: 사용자가 로그인 페이지의 사용자 이름/전자 메일 주소 필드를 peter@contoso.com로 미리 채웁니다. preferred_username 클레임을 사용하여 이전 로그인 작업에서 사용자 이름이 이미 추출된 경우 앱이 재인증 과정에서 이 매개 변수를 종종 사용합니다.
try
{
    var examples = await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

    ...
}
catch (AccessTokenNotAvailableException ex)
{
    ex.Redirect(requestOptions => {
        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");
    });
}

앞의 예제에서는 다음을 가정합니다.

자세한 내용은 다음 리소스를 참조하세요.

IAccessTokenProvider를 사용할 때 옵션 사용자 지정

IAccessTokenProvider를 사용할 때 토큰 가져오기가 실패하는 경우, InteractiveRequestOptions의 새 인스턴스에서 다음 메서드를 사용하여 새 ID 공급자 액세스 토큰 요청에 대한 추가 매개 변수를 하나 이상 관리합니다.

사용자에 대한 액세스 토큰을 가져오려고 시도하는 다음 예제에서는 TryGetToken가 호출되면 토큰을 가져오려는 시도가 실패 하는 경우 로그인 요청에 추가 매개 변수가 추가됩니다.

  • promptlogin로 설정됨: 사용자가 해당 요청에 대한 자격 증명을 강제로 입력하도록 하여 싱글 사인 온을 무효화합니다.
  • loginHintpeter@contoso.com로 설정됨: 사용자가 로그인 페이지의 사용자 이름/전자 메일 주소 필드를 peter@contoso.com로 미리 채웁니다. preferred_username 클레임을 사용하여 이전 로그인 작업에서 사용자 이름이 이미 추출된 경우 앱이 재인증 과정에서 이 매개 변수를 종종 사용합니다.
var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { ... }
    });

if (!tokenResult.TryGetToken(out var token))
{
    tokenResult.InteractionOptions.TryAddAdditionalParameter("prompt", "login");
    tokenResult.InteractionOptions.TryAddAdditionalParameter("loginHint", 
        "peter@contoso.com");

    Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl, 
        accessTokenResult.InteractionOptions);
}

앞의 예제에서는 다음을 가정합니다.

자세한 내용은 다음 리소스를 참조하세요.

사용자 지정 반환 URL을 사용한 로그아웃

다음 예제에서는 사용자를 로그아웃시키고 사용자를 /goodbye 엔드포인트로 반환합니다.

Navigation.NavigateToLogout("authentication/logout", "goodbye");

인증 옵션에서 로그인 경로 가져오기

RemoteAuthenticationOptions에서 구성된 로그인 경로를 가져옵니다.

var loginPath = 
    RemoteAuthOptions.Get(Options.DefaultName).AuthenticationPaths.LogInPath;

앞의 예제에서는 다음을 가정합니다.

사용자 지정 AuthorizationMessageHandler 클래스

이 섹션의 이 지침은 앱의 기본 URI 내에 없는 URI에 보내는 요청을 수행하는 클라이언트 앱에 권장됩니다.

다음 예제에서 사용자 지정 클래스는 HttpClientDelegatingHandler로 사용할 AuthorizationMessageHandler를 확장합니다. ConfigureHandler는 액세스 토큰을 사용하여 아웃바운드 HTTP 요청에 권한을 부여하도록 이 처리기를 구성합니다. 액세스 토큰은 권한 있는 URL 중 하나 이상이 요청 URI(HttpRequestMessage.RequestUri)의 기반인 경우에만 연결됩니다.

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, 
        NavigationManager navigation)
        : base(provider, navigation)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
            scopes: new[] { "example.read", "example.write" });
    }
}

앞의 코드에서 범위 example.readexample.write는 특정 공급자에 대한 유효한 범위를 반영하지 않는 일반 예제입니다.

Program 파일 CustomAuthorizationMessageHandler 에서 임시 서비스로 등록되고 명명HttpClient된 인스턴스에 DelegatingHandler 대해 HttpResponseMessage 구성됩니다.

다음 예제 HttpClientFactoryServiceCollectionExtensions.AddHttpClient 에서는 .의 확장 Microsoft.Extensions.Http입니다. 패키지를 아직 참조하지 않는 앱에 추가합니다.

참고 항목

.NET 앱에 패키지를 추가하는 방법에 대한 지침은 패키지 사용 워크플로에서 패키지 설치 및 관리의 문서(NuGet 설명서)를 참조하세요. NuGet.org에서 올바른 패키지 버전을 확인합니다.

builder.Services.AddTransient<CustomAuthorizationMessageHandler>();

builder.Services.AddHttpClient("WebAPI",
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();

참고 항목

앞의 예제에서 CustomAuthorizationMessageHandlerDelegatingHandlerAddHttpMessageHandler에 대한 임시 서비스로 등록됩니다. 임시 등록은 자체 DI 범위를 관리하는 IHttpClientFactory에 대해 권장됩니다. 자세한 내용은 다음 리소스를 참조하세요.

Blazor WebAssembly 프로젝트 템플릿을 기반으로 하는 호스트된 Blazor 솔루션의 경우 기본적으로 IWebAssemblyHostEnvironment.BaseAddress(new Uri(builder.HostEnvironment.BaseAddress))이 HttpClient.BaseAddress에 할당됩니다.

구성된 HttpClienttry-catch 패턴을 사용하여 권한이 부여된 요청을 수행하는 데 사용됩니다. 클라이언트가 CreateClient(Microsoft.Extensions.Http 패키지)를 사용하여 만들어진 경우, HttpClient에는 서버 API에 요청할 때 액세스 토큰을 포함하는 인스턴스가 제공됩니다. 요청 URI가 다음 예제(ExampleAPIMethod)와 같이 상대 URI인 경우 클라이언트 앱이 요청을 할 때 BaseAddress와 결합됩니다.

@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        try
        {
            var client = ClientFactory.CreateClient("WebAPI");

            var examples = 
                await client.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

            ...
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

AuthorizationMessageHandler 구성

AuthorizationMessageHandlerConfigureHandler 메서드를 사용하여 권한 있는 URL, 범위, 반환 URL로 구성될 수 있습니다. ConfigureHandler는 액세스 토큰을 사용하여 아웃바운드 HTTP 요청에 권한을 부여하도록 처리기를 구성합니다. 액세스 토큰은 권한 있는 URL 중 하나 이상이 요청 URI(HttpRequestMessage.RequestUri)의 기반인 경우에만 연결됩니다. 요청 URI가 상대 URI인 경우 BaseAddress와 결합됩니다.

다음 예제 AuthorizationMessageHandler 에서는 파일에서 Program 구성 HttpClient 합니다.

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddScoped(sp => new HttpClient(
    sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }))
    {
        BaseAddress = new Uri("https://api.contoso.com/v1.0")
    });

앞의 코드에서 범위 example.readexample.write는 특정 공급자에 대한 유효한 범위를 반영하지 않는 일반 예제입니다.

Blazor WebAssembly 프로젝트 템플릿을 기반으로 하는 호스트된 Blazor 솔루션의 경우 기본적으로 IWebAssemblyHostEnvironment.BaseAddress이 다음에 할당됩니다.

형식화된 HttpClient

단일 클래스 내에서 모든 HTTP 및 토큰 획득 문제를 처리하는 형식화된 클라이언트를 정의할 수 있습니다.

WeatherForecastClient.cs:

using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient
{
    private readonly HttpClient http;
    private WeatherForecast[]? forecasts;

    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {ASSEMBLY NAME}.Data;

public class WeatherForecastClient
{
    private readonly HttpClient http;
    private WeatherForecast[] forecasts;

    public WeatherForecastClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}

앞의 예제 WeatherForecast 에서 형식은 일기 예보 데이터를 보유하는 정적 클래스입니다. 자리 표시자 {ASSEMBLY NAME}는 앱의 어셈블리 이름입니다(예: using static BlazorSample.Data;).

다음 예제 HttpClientFactoryServiceCollectionExtensions.AddHttpClient 에서는 .의 확장 Microsoft.Extensions.Http입니다. 패키지를 아직 참조하지 않는 앱에 추가합니다.

참고 항목

.NET 앱에 패키지를 추가하는 방법에 대한 지침은 패키지 사용 워크플로에서 패키지 설치 및 관리의 문서(NuGet 설명서)를 참조하세요. NuGet.org에서 올바른 패키지 버전을 확인합니다.

Program 파일에서:

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

Blazor WebAssembly 프로젝트 템플릿을 기반으로 하는 호스트된 Blazor 솔루션의 경우 기본적으로 IWebAssemblyHostEnvironment.BaseAddress(new Uri(builder.HostEnvironment.BaseAddress))이 HttpClient.BaseAddress에 할당됩니다.

날씨 데이터를 가져오는 구성 요소에서 다음을 수행합니다.

@inject WeatherForecastClient Client

...

protected override async Task OnInitializedAsync()
{
    forecasts = await Client.GetForecastAsync();
}

HttpClient 처리기 구성

처리기는 ConfigureHandler를 사용하여 아웃바운드 HTTP 요청에 대해 추가로 구성될 수 있습니다.

다음 예제 HttpClientFactoryServiceCollectionExtensions.AddHttpClient 에서는 .의 확장 Microsoft.Extensions.Http입니다. 패키지를 아직 참조하지 않는 앱에 추가합니다.

참고 항목

.NET 앱에 패키지를 추가하는 방법에 대한 지침은 패키지 사용 워크플로에서 패키지 설치 및 관리의 문서(NuGet 설명서)를 참조하세요. NuGet.org에서 올바른 패키지 버전을 확인합니다.

Program 파일에서:

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new [] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }));

앞의 코드에서 범위 example.readexample.write는 특정 공급자에 대한 유효한 범위를 반영하지 않는 일반 예제입니다.

Blazor WebAssembly 프로젝트 템플릿을 기반으로 하는 호스트된 Blazor 솔루션의 경우 기본적으로 IWebAssemblyHostEnvironment.BaseAddress이 다음에 할당됩니다.

보안 기본 클라이언트가 있는 앱의 인증되지 않거나 권한이 부여되지 않은 웹 API 요청

일반적으로 보안 기본값 HttpClient 을 사용하는 앱은 명명 HttpClient된 웹 API를 구성하여 인증되지 않거나 권한이 없는 웹 API 요청을 만들 수도 있습니다.

다음 예제 HttpClientFactoryServiceCollectionExtensions.AddHttpClient 에서는 .의 확장 Microsoft.Extensions.Http입니다. 패키지를 아직 참조하지 않는 앱에 추가합니다.

참고 항목

.NET 앱에 패키지를 추가하는 방법에 대한 지침은 패키지 사용 워크플로에서 패키지 설치 및 관리의 문서(NuGet 설명서)를 참조하세요. NuGet.org에서 올바른 패키지 버전을 확인합니다.

Program 파일에서:

builder.Services.AddHttpClient("WebAPI.NoAuthenticationClient", 
    client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"));

Blazor WebAssembly 프로젝트 템플릿을 기반으로 하는 호스트된 Blazor 솔루션의 경우 기본적으로 IWebAssemblyHostEnvironment.BaseAddress(new Uri(builder.HostEnvironment.BaseAddress))이 HttpClient.BaseAddress에 할당됩니다.

앞에 나온 등록은 기존의 보안 기본 HttpClient 등록에 더해 이루어집니다.

구성 요소는 IHttpClientFactory(Microsoft.Extensions.Http 패키지)에서 HttpClient를 만들어 인증되지 않거나 권한이 부여되지 않은 요청을 만듭니다.

@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        var client = ClientFactory.CreateClient("WebAPI.NoAuthenticationClient");

        var examples = await client.GetFromJsonAsync<ExampleType[]>(
            "ExampleNoAuthentication");

        ...
    }
}

참고 항목

위 예제에서 서버 API의 컨트롤러인 ExampleNoAuthenticationController[Authorize] 특성으로 표시되지 않습니다.

보안 클라이언트 또는 안전하지 않은 클라이언트를 기본 HttpClient 인스턴스로 사용할지 여부는 개발자가 결정합니다. 이 결정을 내리는 한 가지 방법은 앱이 연결되는 인증된 엔드포인트와 인증되지 않은 엔드포인트의 수를 고려하는 것입니다. 대다수 앱 요청이 API 엔드포인트를 보호하기 위한 것이면 인증된 HttpClient 인스턴스를 기본값으로 사용합니다. 그렇지 않으면 인증되지 않은 HttpClient 인스턴스를 기본값으로 등록합니다.

IHttpClientFactory를 사용하는 대체 방법은 익명 엔드포인트에 대한 인증되지 않은 액세스를 위해 형식화된 클라이언트를 만드는 것입니다.

추가 액세스 토큰 요청

IAccessTokenProvider.RequestAccessToken을 호출하여 액세스 토큰을 수동으로 획득할 수 있습니다. 다음 예제에서는 기본 HttpClient에 대한 추가 범위가 앱에 필요합니다. MSAL(Microsoft 인증 라이브러리) 예제에서는 MsalProviderOptions로 범위를 구성합니다.

Program 파일에서:

builder.Services.AddMsalAuthentication(options =>
{
    ...

    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 1}");
    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 2}");
}

위의 예제에서 {CUSTOM SCOPE 1}{CUSTOM SCOPE 2} 자리 표시자는 사용자 지정 범위입니다.

참고 항목

AdditionalScopesToConsent 사용자가 Microsoft Azure에 등록된 앱을 처음 사용하는 경우 Microsoft Entra ID 동의 UI를 통해 Microsoft Graph에 대한 위임된 사용자 권한을 프로비전할 수 없습니다. 자세한 내용은 ASP.NET Core에서 Graph API 사용을 참조 하세요 Blazor WebAssembly.

IAccessTokenProvider.RequestAccessToken 메서드는 앱이 지정된 범위 세트를 사용하여 액세스 토큰을 프로비저닝할 수 있도록 지원하는 오버로드를 제공합니다.

Razor 구성 요소에서:

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider

...

var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { "{CUSTOM SCOPE 1}", "{CUSTOM SCOPE 2}" }
    });

if (tokenResult.TryGetToken(out var token))
{
    ...
}

위의 예제에서 {CUSTOM SCOPE 1}{CUSTOM SCOPE 2} 자리 표시자는 사용자 지정 범위입니다.

AccessTokenResult.TryGetToken은 다음을 반환합니다.

  • token이 사용할 수 있도록 true.
  • 토큰이 검색되지 않은 경우 false.

교차 출처 리소스 공유(CORS)

CORS 요청에 대한 자격 증명(권한 부여 cookie/헤더)을 보낼 때 CORS 정책에서 Authorization 헤더를 허용해야 합니다.

다음 정책에는 해당 구성이 포함되어 있습니다.

  • 요청 원본(http://localhost:5000, https://localhost:5001).
  • 임의 메서드(동사)
  • Content-TypeAuthorization 헤더. 사용자 지정 헤더(예: x-custom-header)를 허용하려면 WithHeaders를 호출할 때 헤더를 나열합니다.
  • 클라이언트 쪽 JavaScript 코드에 의해 설정된 자격 증명(credentials 속성이 include로 설정됨).
app.UseCors(policy => 
    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
        .AllowAnyMethod()
        .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, 
            "x-custom-header")
        .AllowCredentials());

Blazor WebAssembly 프로젝트 템플릿을 기반으로 하는 호스트된 Blazor 솔루션은 클라이언트 앱과 서버 앱에 동일한 기준 주소를 사용합니다. 클라이언트 앱의 HttpClient.BaseAddress은 기본적으로 builder.HostEnvironment.BaseAddress의 URI로 설정됩니다. 호스트된 Blazor 솔루션의 기본 구성에서는 CORS 구성이 필요하지 않습니다. 서버 프로젝트가 호스트하지 않으며 서버 앱의 기준 주소를 공유하지 않는 추가 클라이언트 앱은 서버 프로젝트에 CORS 구성이 필요합니다.

자세한 내용은 ASP.NET Core에서 CORS(원본 간 요청) 사용 및 샘플 앱의 HTTP 요청 테스터 구성 요소(Components/HTTPRequestTester.razor)를 참조하세요.

토큰 요청 오류 처리

SPA(단일 페이지 애플리케이션)가 OIDC(OpenID Connect)를 사용하여 사용자를 인증하는 경우 인증 상태는 SPA 내에서 로컬로 유지 관리되는 동시에 IP(Identity 공급자)에서 사용자가 자격 증명을 제공한 결과 설정된 세션 cookie 형식으로 유지 관리됩니다.

일반적으로 사용자에 대해 IP가 내보내는 토큰은 짧은 시간 동안(보통 1시간 동안) 유효하므로 클라이언트 앱이 정기적으로 새 토큰을 페치해야 합니다. 그러지 않으면 부여된 토큰이 만료된 후에 사용자가 로그아웃됩니다. 대부분의 경우 OIDC 클라이언트는 IP 내에 유지되는 인증 상태 또는 “세션” 덕분에 사용자에게 다시 인증하도록 요구하지 않고 새 토큰을 프로비저닝할 수 있습니다.

클라이언트가 사용자 조작 없이는 토큰을 가져올 수 없는 경우도 있습니다. 사용자가 어떤 이유로든 IP에서 명시적으로 로그아웃하는 경우가 그 예입니다. 이 시나리오는 사용자가 방문하여 https://login.microsoftonline.com 로그아웃하는 경우에 발생합니다. 이러한 시나리오에서 앱은 사용자가 로그아웃했다는 사실을 즉시 알지 못합니다. 클라이언트가 보유하는 모든 토큰은 더 이상 유효하지 않을 수 있습니다. 또한 클라이언트는 현재 토큰이 만료된 후 사용자 조작 없이는 새 토큰을 프로비저닝할 수 없습니다.

이러한 시나리오는 토큰 기반 인증에만 국한되지 않으며, SPA의 특성에 해당합니다. cookie를 사용하는 SPA도 인증 cookie가 제거된 경우 서버 API를 호출하지 못합니다.

앱에서 보호된 리소스에 대해 API 호출을 수행하는 경우에는 다음 사항에 유의해야 합니다.

  • 새 액세스 토큰을 프로비저닝하여 API를 호출하려면 사용자가 다시 인증해야 할 수 있습니다.
  • 클라이언트에 유효한 것으로 보이는 토큰이 있는 경우에도 사용자가 토큰을 해지했기 때문에 서버에 대한 호출이 실패할 수 있습니다.

앱에서 토큰을 요청하는 경우 다음과 같은 두 가지 가능한 결과가 발생합니다.

  • 요청이 성공하고 앱이 유효한 토큰을 보유합니다.
  • 요청이 실패하고 앱이 사용자를 다시 인증하여 새 토큰을 획득해야 합니다.

토큰 요청이 실패할 경우 리디렉션을 수행하기 전에 현재 상태를 저장할 것인지 여부를 결정해야 합니다. 복잡성 수준이 증가하는 상태를 저장하는 몇 가지 방법이 있습니다.

  • 세션 스토리지에 현재 페이지 상태를 저장합니다. OnInitializedAsync 수명 주기 메서드(OnInitializedAsync) 중에, 계속하기 전에 상태를 복원할 수 있는지 확인합니다.
  • 쿼리 문자열 매개 변수를 추가하고 이 매개 변수를 이전에 저장된 상태를 다시 하이드레이션해야 한다는 사실을 앱에 알리는 용도로 사용합니다.
  • 고유 식별자를 갖는 쿼리 문자열 매개 변수를 추가하여 다른 항목과의 충돌이 발생할 가능성 없이 세션 스토리지에 데이터를 저장합니다.

세션 스토리지를 사용하여 인증 작업 전에 앱 상태 저장

아래 예제는 다음과 같은 작업의 방법을 보여 줍니다.

  • 로그인 페이지로 리디렉션하기 전에 상태를 유지합니다.
  • 쿼리 문자열 매개 변수를 사용하여 인증 후 이전 상태를 복구합니다.
...
@using System.Text.Json
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
@inject IJSRuntime JS
@inject NavigationManager Navigation

<EditForm Model="User" OnSubmit="OnSaveAsync">
    <label>
        First Name: 
        <InputText @bind-Value="User!.Name" />
    </label>
    <label>
        Last Name: 
        <InputText @bind-Value="User!.LastName" />
    </label>
    <button type="submit">Save User</button>
</EditForm>

@code {
    public Profile User { get; set; } = new Profile();

    protected override async Task OnInitializedAsync()
    {
        var currentQuery = new Uri(Navigation.Uri).Query;

        if (currentQuery.Contains("state=resumeSavingProfile"))
        {
            var user = await JS.InvokeAsync<string>("sessionStorage.getItem",
                "resumeSavingProfile");

            if (!string.IsNullOrEmpty(user))
            {
                User = JsonSerializer.Deserialize<Profile>(user);
            }
        }
    }

    public async Task OnSaveAsync()
    {
        var http = new HttpClient();
        http.BaseAddress = new Uri(Navigation.BaseUri);

        var resumeUri = Navigation.Uri + $"?state=resumeSavingProfile";

        var tokenResult = await TokenProvider.RequestAccessToken(
            new AccessTokenRequestOptions
            {
                ReturnUrl = resumeUri
            });

        if (tokenResult.TryGetToken(out var token))
        {
            http.DefaultRequestHeaders.Add("Authorization", 
                $"Bearer {token.Value}");
            await http.PostAsJsonAsync("Save", User);
        }
        else
        {
            await JS.InvokeVoidAsync("sessionStorage.setItem", 
                "resumeSavingProfile", JsonSerializer.Serialize(User));
            Navigation.NavigateTo(tokenResult.InteractiveRequestUrl);
        }
    }

    public class Profile
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }
}

세션 스토리지 및 상태 컨테이너를 사용하여 인증 작업 전에 앱 상태 저장

인증 작업을 수행하는 동안 브라우저가 IP로 리디렉션되기 전에 앱 상태를 저장해야 하는 경우가 있습니다. 상태 컨테이너를 사용하고 있으며 인증에 성공한 후에 상태를 복원하려는 경우가 여기에 해당할 수 있습니다. 이때 사용자 지정 인증 상태 개체를 사용하여 앱의 상태를 보존하거나 참조하고 인증 작업이 성공적으로 완료된 후에 해당 상태를 복원할 수 있습니다. 다음 예제에서는 이 접근 방식을 보여 줍니다.

앱의 상태 값을 저장하는 속성이 있는 상태 컨테이너 클래스가 앱 안에 만들어집니다. 다음 예제에서는 기본 Blazor 프로젝트 템플릿의 Counter 구성 요소(Counter.razor)의 카운터 값을 유지 관리하기 위해 컨테이너가 사용됩니다. 컨테이너를 직렬화 및 역직렬화하는 메서드는 System.Text.Json을 기반으로 합니다.

using System.Text.Json;

public class StateContainer
{
    public int CounterValue { get; set; }

    public string GetStateForLocalStorage()
    {
        return JsonSerializer.Serialize(this);
    }

    public void SetStateFromLocalStorage(string locallyStoredState)
    {
        var deserializedState = 
            JsonSerializer.Deserialize<StateContainer>(locallyStoredState);

        CounterValue = deserializedState.CounterValue;
    }
}

Counter 구성 요소는 상태 컨테이너를 사용하여 구성 요소 밖에서 currentCount 값을 유지합니다.

@page "/counter"
@inject StateContainer State

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    protected override void OnInitialized()
    {
        if (State.CounterValue > 0)
        {
            currentCount = State.CounterValue;
        }
    }

    private void IncrementCount()
    {
        currentCount++;
        State.CounterValue = currentCount;
    }
}

RemoteAuthenticationState에서 ApplicationAuthenticationState를 만듭니다. 로컬로 저장된 상태의 식별자로 기능하는 Id 속성을 제공합니다.

ApplicationAuthenticationState.cs:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string? Id { get; set; }
}
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string Id { get; set; }
}

Authentication 구성 요소(Authentication.razor)는 StateContainer serialization 및 deserialization 메서드인 GetStateForLocalStorageSetStateFromLocalStorage로 로컬 세션 스토리지를 사용하여 앱의 상태를 저장하고 복원합니다.

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

@code {
    [Parameter]
    public string? Action { get; set; }

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

@code {
    [Parameter]
    public string Action { get; set; }

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}

이 예제에서는 인증에 MICROSOFT Entra(ME-ID)를 사용합니다. Program 파일에서:

  • ApplicationAuthenticationState는 MSAL(Microsoft 인증 라이브러리) RemoteAuthenticationState 형식으로 구성됩니다.
  • 상태 컨테이너는 서비스 컨테이너에 등록됩니다.
builder.Services.AddMsalAuthentication<ApplicationAuthenticationState>(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});

builder.Services.AddSingleton<StateContainer>();

앱 경로 사용자 지정

기본적으로 Microsoft.AspNetCore.Components.WebAssembly.Authentication 라이브러리는 다음 표에 나와 있는 경로를 사용하여 다양한 인증 상태를 나타냅니다.

경로 목적
authentication/login 로그인 작업을 트리거합니다.
authentication/login-callback 로그인 작업의 결과를 처리합니다.
authentication/login-failed 어떤 이유로든 로그인 작업이 실패한 경우 오류 메시지를 표시합니다.
authentication/logout 로그아웃 작업을 트리거합니다.
authentication/logout-callback 로그아웃 작업의 결과를 처리합니다.
authentication/logout-failed 어떤 이유로든 로그아웃 작업이 실패한 경우 오류 메시지를 표시합니다.
authentication/logged-out 사용자가 성공적으로 로그아웃했음을 나타냅니다.
authentication/profile 사용자 프로필을 편집하는 작업을 트리거합니다.
authentication/register 새 사용자를 등록하는 작업을 트리거합니다.

앞의 표에 나와 있는 경로는 RemoteAuthenticationOptions<TRemoteAuthenticationProviderOptions>.AuthenticationPaths를 통해 구성할 수 있습니다. 사용자 지정 경로를 제공하는 옵션을 설정할 때는 앱에 각 경로를 처리하는 경로가 있는지 확인해야 합니다.

다음 예제에서는 모든 경로에 접두사를 갖 /security습니다.

Authentication 구성 요소(Authentication.razor):

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

@code{
    [Parameter]
    public string? Action { get; set; }
}
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action" />

@code{
    [Parameter]
    public string Action { get; set; }
}

Program 파일에서:

builder.Services.AddApiAuthorization(options => { 
    options.AuthenticationPaths.LogInPath = "security/login";
    options.AuthenticationPaths.LogInCallbackPath = "security/login-callback";
    options.AuthenticationPaths.LogInFailedPath = "security/login-failed";
    options.AuthenticationPaths.LogOutPath = "security/logout";
    options.AuthenticationPaths.LogOutCallbackPath = "security/logout-callback";
    options.AuthenticationPaths.LogOutFailedPath = "security/logout-failed";
    options.AuthenticationPaths.LogOutSucceededPath = "security/logged-out";
    options.AuthenticationPaths.ProfilePath = "security/profile";
    options.AuthenticationPaths.RegisterPath = "security/register";
});

서로 완전히 다른 경로가 필요한 경우, 이전에 설명한 대로 경로를 설정하고 명시적 작업 매개 변수를 사용하여 RemoteAuthenticatorView를 렌더링합니다.

@page "/register"

<RemoteAuthenticatorView Action="RemoteAuthenticationActions.Register" />

원하는 경우 UI를 여러 페이지로 나눌 수 있습니다.

인증 사용자 인터페이스 사용자 지정

RemoteAuthenticatorView 에는 각 인증 상태에 대한 기본 UI 조각 집합이 포함되어 있습니다. 사용자 지정 RenderFragment를 전달하여 각 상태를 사용자 지정할 수 있습니다. 초기 로그인 프로세스 중에 표시되는 텍스트를 사용자 지정하려면 다음과 같이 RemoteAuthenticatorView를 변경할 수 있습니다.

Authentication 구성 요소(Authentication.razor):

@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string? Action { get; set; }
}
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string Action { get; set; }
}

RemoteAuthenticatorView에는 다음 표에 나와 있는 인증 경로에 따라 사용할 수 있는 하나의 조각이 있습니다.

경로 조각
authentication/login <LoggingIn>
authentication/login-callback <CompletingLoggingIn>
authentication/login-failed <LogInFailed>
authentication/logout <LogOut>
authentication/logout-callback <CompletingLogOut>
authentication/logout-failed <LogOutFailed>
authentication/logged-out <LogOutSucceeded>
authentication/profile <UserProfile>
authentication/register <Registering>

사용자의 사용자 지정

앱에 바인딩된 사용자는 사용자 지정할 수 있습니다.

페이로드 클레임으로 사용자의 사용자 지정

다음 예제에서는 앱의 인증된 사용자가 각 사용자 인증 방법에 대해 amr 클레임을 받습니다. amr 클레임은 Microsoft Identity Platform v1.0 페이로드 클레임에서 토큰의 주체가 인증된 방법을 식별합니다. 이 예제에서는 RemoteUserAccount에 따라 사용자 지정 사용자 계정 클래스를 사용합니다.

RemoteUserAccount 클래스를 확장하는 클래스를 만듭니다. 다음 예제에서는 AuthenticationMethod 속성을 amrJSON 속성 값의 사용자 배열로 설정합니다. AuthenticationMethod는 사용자가 인증될 때 프레임워크에서 자동으로 채워집니다.

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[]? AuthenticationMethod { get; set; }
}
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[] AuthenticationMethod { get; set; }
}

AccountClaimsPrincipalFactory<TAccount>를 확장하여 CustomUserAccount.AuthenticationMethod에 저장된 사용자의 인증 방법에서 클레임을 만드는 팩터리를 만듭니다.

using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory 
    : AccountClaimsPrincipalFactory<CustomUserAccount>
{
    public CustomAccountFactory(NavigationManager navigation, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            if (account.AuthenticationMethod is not null)
            {
                foreach (var value in account.AuthenticationMethod)
                {
                    userIdentity.AddClaim(new Claim("amr", value));
                }
            }
        }

        return initialUser;
    }
}
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory 
    : AccountClaimsPrincipalFactory<CustomUserAccount>
{
    public CustomAccountFactory(NavigationManager navigation, 
        IAccessTokenProviderAccessor accessor) : base(accessor)
    {
    }

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            foreach (var value in account.AuthenticationMethod)
            {
                userIdentity.AddClaim(new Claim("amr", value));
            }
        }

        return initialUser;
    }
}

사용 중인 인증 공급자에 대해 CustomAccountFactory를 등록합니다. 다음 등록이 모두 유효합니다.

  • AddOidcAuthentication:

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddOidcAuthentication<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    
  • AddMsalAuthentication:

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddMsalAuthentication<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    
  • AddApiAuthorization:

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    ...
    
    builder.Services.AddApiAuthorization<RemoteAuthenticationState, 
        CustomUserAccount>(options =>
        {
            ...
        })
        .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
            CustomUserAccount, CustomAccountFactory>();
    

사용자 지정 사용자 계정 클래스를 사용하는 ME-ID 보안 그룹 및 역할

ME-ID 보안 그룹 및 ME-ID 관리istrator 역할 및 사용자 지정 사용자 계정 클래스와 함께 작동하는 추가 예제는 Microsoft Entra ID 그룹 및 역할이 있는 ASP.NET Core Blazor WebAssembly 를 참조하세요.

인증을 사용하여 미리 렌더링

인증 및 권한 부여가 필요한 콘텐츠 사전 렌더링은 현재 지원되지 않습니다. Blazor WebAssembly 보안 앱 항목 중 하나의 지침을 따른 후에는 다음 지침에 따라 다음을 수행하는 앱을 만듭니다.

  • 인증이 필요하지 않은 경로를 미리 렌더링합니다.
  • 인증이 필요한 경로를 미리 렌더링하지 않습니다.

Client 프로젝트 파일의 Program 경우 일반적인 서비스 등록을 별도의 메서드(예: 프로젝트에서 메서드 Client 만들기ConfigureCommonServices)로 계산합니다. 공통 서비스는 개발자가 클라이언트 및 서버 프로젝트에서 사용하기 위해 등록하는 서비스입니다.

public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}

Program 파일에서:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...

builder.Services.AddScoped( ... );

ConfigureCommonServices(builder.Services);

await builder.Build().RunAsync();

Server 프로젝트의 Program 파일에서 다음 추가 서비스를 등록하고 다음을 호출ConfigureCommonServices합니다.

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddRazorPages();
builder.Services.TryAddScoped<AuthenticationStateProvider, 
    ServerAuthenticationStateProvider>();

Client.Program.ConfigureCommonServices(services);

Server 프로젝트의 Startup.ConfigureServices 메소드에서, 다음 추가 서비스를 등록하고 ConfigureCommonServices을(를) 호출합니다.

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddRazorPages();
    services.AddScoped<AuthenticationStateProvider, 
        ServerAuthenticationStateProvider>();
    services.AddScoped<SignOutSessionStateManager>();

    Client.Program.ConfigureCommonServices(services);
}

프레임워크 서버 인증 공급자()ServerAuthenticationStateProvider에 대한 Blazor 자세한 내용은 ASP.NET Core Blazor 인증 및 권한 부여를 참조하세요.

Server프로젝트의 Pages/_Host.cshtml 파일에서 Component 태그 도우미(<component ... />)를 다음으로 바꿉니다.

<div id="app">
    @if (HttpContext.Request.Path.StartsWithSegments("/authentication"))
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssembly" />
    }
    else
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssemblyPrerendered" />
    }
</div>

앞의 예에서:

  • 자리 표시자 {CLIENT APP ASSEMBLY NAME}는 클라이언트 앱의 어셈블리 이름입니다(예: BlazorSample.Client).
  • 경로 세그먼트에 대한 /authentication 조건부 검사.
    • 인증 경로에 대한 미리 렌더링(render-mode="WebAssembly")을 방지합니다.
    • 비 인증 경로에 대한 미리 렌더링(render-mode="WebAssemblyPrerendered")을 수행합니다.

호스트된 앱 및 타사 로그인 공급자에 대한 옵션

타사 공급자를 사용하여 호스트된 Blazor WebAssembly 앱을 인증하고 권한을 부여하는 경우 사용자를 인증하는 데 사용할 수 있는 몇 가지 옵션이 있습니다. 시나리오에 따라 선택하는 옵션이 달라집니다.

자세한 내용은 ASP.NET Core에서 외부 공급자의 추가 클레임 및 토큰 유지를 참조하세요.

보호된 타사 API만 호출하도록 사용자 인증

클라이언트 쪽 OAuth 흐름을 사용하여 사용자를 타사 API 공급자에 대해 인증합니다.

builder.services.AddOidcAuthentication(options => { ... });

이 시나리오에서는

  • 앱을 호스트하는 서버에서 역할을 재생하지 않습니다.
  • 서버의 API는 보호할 수 없습니다.
  • 앱은 보호된 타사 API만 호출할 수 있습니다.

타사 공급자를 사용하여 사용자를 인증하고 호스트 서버 및 타사에서 보호된 API 호출

타사 로그인 공급자를 사용하여 Identity를 구성합니다. 타사 API 액세스에 필요한 토큰을 가져와 저장합니다.

사용자가 로그인하면 Identity는 인증 프로세스의 일부로 액세스 및 새로 고침 토큰을 수집합니다. 이때 타사 API에 대한 API 호출을 수행하는 데 사용할 수 있는 몇 가지 방법이 있습니다.

서버 액세스 토큰을 사용하여 타사 액세스 토큰 검색

서버에서 생성된 액세스 토큰을 사용하여 서버 API 엔드포인트에서 타사 액세스 토큰을 검색합니다. 여기에서 타사 액세스 토큰을 사용하여 클라이언트의 Identity에서 직접 타사 API 리소스를 호출합니다.

이 방법은 권장하지 않습니다. 이 방법을 사용하려면 타사 액세스 토큰을 공용 클라이언트에 대해 생성된 것처럼 처리해야 합니다. OAuth 맥락에서, 퍼블릭 앱은 암호를 안전하게 저장하는 데 신뢰할 수 없으므로 클라이언트 암호를 갖지 않으며, 액세스 토큰은 기밀 클라이언트에 대해 생성됩니다. 기밀 클라이언트는 클라이언트 암호를 포함하는 클라이언트이며 비밀을 안전하게 저장할 수 있는 것으로 간주됩니다.

  • 타사 액세스 토큰에는 타사에서 더 신뢰할 수 있는 클라이언트에 대한 토큰을 내보낸 사실을 기반으로 중요한 작업을 수행하기 위한 추가 범위가 부여될 수 있습니다.
  • 마찬가지로, 신뢰할 수 없는 클라이언트에 대해서는 새로 고침 토큰을 발급하지 않아야 합니다. 발급할 경우, 다른 제한이 적용되지 않는 한 클라이언트에 무제한 액세스 권한이 제공됩니다.

타사 API를 호출하기 위해 클라이언트에서 서버 API로 API 호출 수행

클라이언트에서 서버 API로 API 호출을 수행합니다. 서버에서 타사 API 리소스에 대한 액세스 토큰을 검색하고 필요한 호출이 무엇이든 해당 호출을 실행합니다.

이 방법을 사용하는 것이 좋습니다. 이 방법은 타사 API를 호출하기 위해 서버를 통한 추가 네트워크 홉이 필요하지만 궁극적으로 더 안전한 환경을 제공합니다.

  • 서버는 새로 고침 토큰을 저장하고 앱이 타사 리소스에 대한 액세스 권한을 잃지 않도록 보장할 수 있습니다.
  • 앱은 더 중요한 권한을 포함할 수 있는 서버의 액세스 토큰을 누출할 수 없습니다.

OIDC(OpenID Connect) v2.0 엔드포인트 사용

인증 라이브러리 및 Blazor 프로젝트 템플릿은 OIDC(OpenID Connect) v1.0 엔드포인트를 사용합니다. v2.0 엔드포인트를 사용하려면 JWT 전달자 JwtBearerOptions.Authority 옵션을 구성해야 합니다. 다음 예제에서 ME-ID는 속성에 세그먼트를 v2.0 추가하여 v2.0에 Authority 대해 구성됩니다.

using Microsoft.AspNetCore.Authentication.JwtBearer;

...

builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    });

또는 앱 설정(appsettings.json) 파일에서 설정을 수행할 수 있습니다.

{
  "Local": {
    "Authority": "https://login.microsoftonline.com/common/oauth2/v2.0/",
    ...
  }
}

ME ID가 아닌 공급자와 같이 앱의 OIDC 공급자에 대해 기관에 세그먼트를 태킹하는 것이 적절하지 않은 경우 속성을 직접 설정합니다 Authority . JwtBearerOptions 또는 앱 설정 파일(appsettings.json)에서 Authority 키를 사용하여 속성을 설정합니다.

ID 토큰의 클레임 목록은 v2.0 엔드포인트의 경우 변경됩니다. 변경 내용에 대한 Microsoft 설명서는 사용 중지되었지만 ID 토큰의 클레임에 대한 지침은 ID 토큰 클레임 참조에서 사용할 수 있습니다.

구성 요소에서 gRPC 구성 및 사용

ASP.NET Core gRPC 프레임워크를 사용하도록 Blazor WebAssembly 앱을 구성하려면 다음을 수행합니다.

참고 항목

미리 렌더링은 Web Apps에서 Blazor 기본적으로 사용하도록 설정되므로 먼저 서버에서 렌더링한 다음 클라이언트에서 렌더링하는 구성 요소를 고려해야 합니다. 미리 렌더링된 상태는 다시 사용할 수 있도록 클라이언트로 전달되어야 합니다. 자세한 내용은 Prerender ASP.NET Core Razor 구성 요소를 참조하세요.

참고 항목

미리 렌더링은 호스트 Blazor WebAssembly 된 앱에서 기본적으로 사용하도록 설정되므로 먼저 서버에서 렌더링한 다음 클라이언트에서 렌더링하는 구성 요소를 고려해야 합니다. 미리 렌더링된 상태는 다시 사용할 수 있도록 클라이언트로 전달되어야 합니다. 자세한 내용은 ASP.NET Core Razor 구성 요소 미리 렌더링 및 통합을 참조하세요.

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;

...

builder.Services.AddScoped(sp =>
{
    var baseAddressMessageHandler = 
        sp.GetRequiredService<BaseAddressAuthorizationMessageHandler>();
    baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
    var grpcWebHandler = 
        new GrpcWebHandler(GrpcWebMode.GrpcWeb, baseAddressMessageHandler);
    var channel = GrpcChannel.ForAddress(builder.HostEnvironment.BaseAddress, 
        new GrpcChannelOptions { HttpHandler = grpcWebHandler });

    return new Greeter.GreeterClient(channel);
});

클라이언트 앱의 구성 요소는 gRPC 클라이언트(Grpc.razor)를 사용하여 gRPC 호출을 수행할 수 있습니다.

@page "/grpc"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject Greeter.GreeterClient GreeterClient

<h1>Invoke gRPC service</h1>

<p>
    <input @bind="name" placeholder="Type your name" />
    <button @onclick="GetGreeting" class="btn btn-primary">Call gRPC service</button>
</p>

Server response: <strong>@serverResponse</strong>

@code {
    private string name = "Bert";
    private string? serverResponse;

    private async Task GetGreeting()
    {
        try
        {
            var request = new HelloRequest { Name = name };
            var reply = await GreeterClient.SayHelloAsync(request);
            serverResponse = reply.Message;
        }
        catch (Grpc.Core.RpcException ex)
            when (ex.Status.DebugException is 
                AccessTokenNotAvailableException tokenEx)
        {
            tokenEx.Redirect();
        }
    }
}

Status.DebugException 속성을 사용하려면 Grpc.Net.Client 버전 2.30.0 이상을 사용합니다.

자세한 내용은 ASP.NET Core gRPC 앱의 gRPC-Web을 참조하세요.

AuthenticationService 구현을 대체합니다.

다음 하위 섹션에서는 교체 방법을 설명합니다.

  • 모든 JavaScript AuthenticationService 구현.
  • JavaScript(MSAL.js)용 Microsoft 인증 라이브러리.

모든 JavaScript AuthenticationService 구현을 대체합니다.

사용자 지정 인증 세부 정보를 처리하는 JavaScript 라이브러리를 만듭니다.

Warning

이 섹션의 참고 자료는 기본 RemoteAuthenticationService<TRemoteAuthenticationState,TAccount,TProviderOptions>의 구현 세부 정보입니다. 이 섹션의 TypeScript 코드는 .NET 7의 ASP.NET Core에 특별히 적용되며 ASP.NET Core의 향후 릴리스에서 예고 없이 변경될 수 있습니다.

// .NET makes calls to an AuthenticationService object in the Window.
declare global {
  interface Window { AuthenticationService: AuthenticationService }
}

export interface AuthenticationService {
  // Init is called to initialize the AuthenticationService.
  public static init(settings: UserManagerSettings & AuthorizeServiceSettings, logger: any) : Promise<void>;

  // Gets the currently authenticated user.
  public static getUser() : Promise<{[key: string] : string }>;

  // Tries to get an access token silently.
  public static getAccessToken(options: AccessTokenRequestOptions) : Promise<AccessTokenResult>;

  // Tries to sign in the user or get an access token interactively.
  public static signIn(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the sign-in process when a redirect is used.
  public static async completeSignIn(url: string) : Promise<AuthenticationResult>;

  // Signs the user out.
  public static signOut(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the signout callback when a redirect is used.
  public static async completeSignOut(url: string) : Promise<AuthenticationResult>;
}

// The rest of these interfaces match their C# definitions.

export interface AccessTokenRequestOptions {
  scopes: string[];
  returnUrl: string;
}

export interface AccessTokenResult {
  status: AccessTokenResultStatus;
  token?: AccessToken;
}

export interface AccessToken {
  value: string;
  expires: Date;
  grantedScopes: string[];
}

export enum AccessTokenResultStatus {
  Success = 'Success',
  RequiresRedirect = 'RequiresRedirect'
}

export enum AuthenticationResultStatus {
  Redirect = 'Redirect',
  Success = 'Success',
  Failure = 'Failure',
  OperationCompleted = 'OperationCompleted'
};

export interface AuthenticationResult {
  status: AuthenticationResultStatus;
  state?: unknown;
  message?: string;
}

export interface AuthenticationContext {
  state?: unknown;
  interactiveRequest: InteractiveAuthenticationRequest;
}

export interface InteractiveAuthenticationRequest {
  scopes?: string[];
  additionalRequestParameters?: { [key: string]: any };
};

원래 <script> 태그를 제거하고 사용자 지정 라이브러리를 로드하는 <script> 태그를 추가하여 라이브러리를 가져올 수 있습니다. 다음 예제에서는 기본 <script> 태그를 wwwroot/js 폴더에서 명명된 라이브러리 CustomAuthenticationService.js를 로드하는 태그로 바꾸는 방법을 보여 줍니다.

wwwroot/index.html</body> 는 태그 내의 Blazor 스크립트(_framework/blazor.webassembly.js) 앞에서 다음을 수행합니다.

- <script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
+ <script src="js/CustomAuthenticationService.js"></script>

자세한 내용은 dotnet/aspnetcore GitHub 리포지토리에서 AuthenticationService.ts를 참조하세요.

참고 항목

.NET 참조 원본의 설명서 링크는 일반적으로 다음 릴리스의 .NET을 위한 현재 개발을 나타내는 리포지토리의 기본 분기를 로드합니다. 특정 릴리스를 위한 태그를 선택하려면 Switch branches or tags(분기 또는 태그 전환) 드롭다운 목록을 사용합니다. 자세한 내용은 ASP.NET Core 소스 코드(dotnet/AspNetCore.Docs #26205)의 버전 태그를 선택하는 방법을 참조하세요.

JavaScript(MSAL.js)용 Microsoft 인증 라이브러리 바꾸기

앱에 JavaScript(MSAL.js)용 Microsoft 인증 라이브러리의 사용자 지정 버전이 필요한 경우 다음 단계를 수행합니다.

  1. 시스템에 최신 개발자 .NET SDK가 있는지 확인하거나 .NET Core SDK: 설치 프로그램과 이진 파일에서 최신 개발자 SDK를 다운로드하고 설치합니다. 이 시나리오에서는 내부 NuGet 피드의 구성이 필요하지 않습니다.
  2. Sourcedotnet/aspnetcore Build ASP.NET Core 설명서에 따라 개발을 위해 GitHub 리포지토리를 설정합니다. GitHub 리포지토리의 dotnet/aspnetcore ZIP 보관 파일을 포크하고 복제하거나 다운로드합니다.
  3. src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json 파일을 열고 원하는 버전의 @azure/msal-browser를 설정합니다. 릴리스된 버전 목록을 보려면 @azure/msal-browser npm 웹 사이트를 방문하여 버전 탭을 선택합니다.
  4. 명령 셸에서 yarn build 명령을 사용하여 src/Components/WebAssembly/Authentication.Msal/src 폴더에 Authentication.Msal 프로젝트를 빌드합니다.
  5. 앱이 압축된 자산(Brotli/Gzip)을 사용하는 경우 Interop/dist/Release/AuthenticationService.js 파일을 압축합니다.
  6. AuthenticationService.js 파일과 해당 파일의 압축된 버전(.br/.gz)(생성된 경우)을 Interop/dist/Release 폴더에서 앱의 게시된 자산에 있는 앱의 publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal 폴더에 복사합니다.

사용자 지정 공급자 옵션 전달

기본 JavaScript 라이브러리에 데이터를 전달하기 위한 클래스를 정의합니다.

Important

클래스의 구조는 System.Text.Json을 사용하여 JSON을 직렬화했을 때 라이브러리가 예상하는 것과 일치해야 합니다.

다음 예제에서는 가상 사용자 지정 공급자 라이브러리의 예상과 일치하는 JsonPropertyName 특성이 있는 ProviderOptions 클래스를 보여 줍니다.

public class ProviderOptions
{
    public string? Authority { get; set; }
    public string? MetadataUrl { get; set; }

    [JsonPropertyName("client_id")]
    public string? ClientId { get; set; }

    public IList<string> DefaultScopes { get; } = 
        new List<string> { "openid", "profile" };

    [JsonPropertyName("redirect_uri")]
    public string? RedirectUri { get; set; }

    [JsonPropertyName("post_logout_redirect_uri")]
    public string? PostLogoutRedirectUri { get; set; }

    [JsonPropertyName("response_type")]
    public string? ResponseType { get; set; }

    [JsonPropertyName("response_mode")]
    public string? ResponseMode { get; set; }
}
public class ProviderOptions
{
    public string Authority { get; set; }
    public string MetadataUrl { get; set; }

    [JsonPropertyName("client_id")]
    public string ClientId { get; set; }

    public IList<string> DefaultScopes { get; } = 
        new List<string> { "openid", "profile" };

    [JsonPropertyName("redirect_uri")]
    public string RedirectUri { get; set; }

    [JsonPropertyName("post_logout_redirect_uri")]
    public string PostLogoutRedirectUri { get; set; }

    [JsonPropertyName("response_type")]
    public string ResponseType { get; set; }

    [JsonPropertyName("response_mode")]
    public string ResponseMode { get; set; }
}

DI 시스템 내에서 공급자 옵션을 등록하고 적절한 값을 구성합니다.

builder.Services.AddRemoteAuthentication<RemoteAuthenticationState, RemoteUserAccount,
    ProviderOptions>(options => {
        options.Authority = "...";
        options.MetadataUrl = "...";
        options.ClientId = "...";
        options.DefaultScopes = new List<string> { "openid", "profile", "myApi" };
        options.RedirectUri = "https://localhost:5001/authentication/login-callback";
        options.PostLogoutRedirectUri = "https://localhost:5001/authentication/logout-callback";
        options.ResponseType = "...";
        options.ResponseMode = "...";
    });

앞의 예제에서는 일반 문자열 리터럴을 사용하여 리디렉션 URI를 설정합니다. 사용할 수 있는 대안은 다음과 같습니다.

  • TryCreate 사용 IWebAssemblyHostEnvironment.BaseAddress:

    Uri.TryCreate(
        $"{builder.HostEnvironment.BaseAddress}authentication/login-callback", 
        UriKind.Absolute, out var redirectUri);
    options.RedirectUri = redirectUri;
    
  • 호스트 빌더 구성:

    options.RedirectUri = builder.Configuration["RedirectUri"];
    

    wwwroot/appsettings.json:

    {
      "RedirectUri": "https://localhost:5001/authentication/login-callback"
    }
    

추가 리소스