다음을 통해 공유


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

Note

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

Warning

이 버전의 ASP.NET Core는 더 이상 지원되지 않습니다. 자세한 내용은 .NET 및 .NET Core 지원 정책을 참조하세요. 현재 릴리스는 이 문서의 .NET 10 버전을 참조하세요.

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

나가는 요청에 토큰을 연결하세요.

AuthorizationMessageHandlerDelegatingHandler 액세스 토큰을 처리하는 데 사용됩니다. 토큰은 프레임워크에 IAccessTokenProvider 의해 등록된 서비스를 사용하여 획득됩니다. 토큰을 가져올 수 없는 경우 AccessTokenNotAvailableException 예외가 발생합니다. AccessTokenNotAvailableException에는 지정된 Redirect를 사용하여 AccessTokenResult.InteractiveRequestUrl로 이동하며 액세스 토큰을 새로 고칠 수 있는 AccessTokenResult.InteractionOptions 메서드가 있습니다.

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

Note

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

다음 예제에서

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

Note

.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솔루션Blazor WebAssembly경우 요청 URI는 앱의 기본 URI 내에 있습니다. 따라서 IWebAssemblyHostEnvironment.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress))는 프로젝트 템플릿에 의해 생성된 앱의 HttpClient.BaseAddress에 할당됩니다.

구성된 HttpClient 패턴은 다음 패턴을 사용하여 try-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 구성 요소 예제에서는 로그인 요청에 추가 매개 변수가 추가됩니다.

  • prompt 가 로 login설정되면 사용자가 해당 요청에 자격 증명을 입력하도록 하여 Single Sign-On을 무효화합니다.
  • 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될 때), 리디렉션 요청에 추가 매개 변수가 추가됩니다.

  • prompt 가 로 login설정되면 사용자가 해당 요청에 자격 증명을 입력하도록 하여 Single Sign-On을 무효화합니다.
  • 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 하는 경우 로그인 요청에 추가 매개 변수가 추가됩니다.

  • prompt 가 로 login설정되면 사용자가 해당 요청에 자격 증명을 입력하도록 하여 Single Sign-On을 무효화합니다.
  • loginHintpeter@contoso.com으로 설정됩니다: 로그인 페이지의 사용자 이름/전자 메일 주소 필드에 peter@contoso.com를 미리 채웁니다. 앱은 이전 로그인에서 preferred_username 클레임을 사용하여 사용자 이름을 이미 추출한 상태에서, 다시 인증할 때 이 매개변수를 자주 사용합니다.
var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = [ ... ]
    });

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

    Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl, 
        accessTokenResult.InteractionOptions);
}
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에 보내는 요청을 수행하는 클라이언트 앱에 권장됩니다.

다음 예제에서는 사용자 지정 클래스가 AuthorizationMessageHandler를 확장하여 DelegatingHandlerHttpClient로 사용됩니다. 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: [ "https://api.contoso.com/v1.0" ],
            scopes: [ "example.read", "example.write" ]);
    }
}

Note

이 섹션에서는 삽입된 HttpClient메시지에서 구성된 IHttpClientFactory 메시지를 만들 때 이전 메시지 처리기를 사용합니다. IHttpClientFactory를 사용하지 않는 경우 HttpClientHandler 인스턴스를 만들고 그것을 AuthorizationMessageHandlerDelegatingHandler.InnerHandler에 할당해야 합니다.

InnerHandler = new HttpClientHandler();

InnerHandler를 사용할 경우, 이 섹션의 뒷부분에서 IHttpClientFactory 호출이 보여주듯이, 앞서 ExampleAPIMethod 할당을 할 필요가 없습니다.

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

Program 파일에서 CustomAuthorizationMessageHandler는 임시 서비스로 등록되며, 특정 이름이 붙은 DelegatingHandler에 의해 생성된 인스턴스를 위한 HttpResponseMessage으로 HttpClient가 구성됩니다.

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

Note

.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>();

Note

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

프로젝트 템플릿 BlazorBlazor WebAssembly을 기반으로 호스팅된 솔루션의 경우, IWebAssemblyHostEnvironment.BaseAddressnew Uri(builder.HostEnvironment.BaseAddress)HttpClient.BaseAddress에 할당됩니다.

구성된 HttpClienttry-catch 패턴을 사용하여 권한 요청을 만드는 데 사용됩니다. 클라이언트가 CreateClient로 생성되었을 때 (Microsoft.Extensions.Http 패키지) 제공된 인스턴스는 서버 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 구성

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

다음 예제에서는 AuthorizationMessageHandler 파일에서 HttpClientProgram로 구성합니다.

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

...

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

앞의 코드에서 다음을 수행합니다.

프로젝트 템플릿 Blazor의 경우, 호스팅된 Blazor WebAssembly 솔루션은 다음에 할당됩니다:

  • HttpClient.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress))입니다.
  • 배열의 URL입니다 authorizedUrls .

타이핑된 HttpClient

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

WeatherForecastClient.cs:

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

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

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

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

앞의 예제 WeatherForecast 에서 형식은 일기 예보 데이터를 보유하는 정적 클래스입니다. The {PACKAGE ID/ASSEMBLY NAME} 자리 표시자는 프로젝트 파일의 프로젝트 패키지 ID(<PackageId>)이며, 이는 앱의 라이브러리 또는 어셈블리 이름을 나타냅니다(예: using static BlazorSample.Data;).

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

Note

.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>();

프로젝트 템플릿 BlazorBlazor WebAssembly을 기반으로 호스팅된 솔루션의 경우, IWebAssemblyHostEnvironment.BaseAddressnew Uri(builder.HostEnvironment.BaseAddress)HttpClient.BaseAddress에 할당됩니다.

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

@inject WeatherForecastClient Client

...

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

처리기 HttpClient 구성

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

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

Note

.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: [ "https://api.contoso.com/v1.0" ],
        scopes: [ "example.read", "example.write" ]));
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의 경우, 호스팅된 Blazor WebAssembly 솔루션은 다음에 할당됩니다:

  • HttpClient.BaseAddress (new Uri(builder.HostEnvironment.BaseAddress))입니다.
  • 배열의 URL입니다 authorizedUrls .

보안 기본 클라이언트가 있는 앱에서 인증되지 않거나 권한이 없는 웹 API 요청

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

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

Note

.NET 앱에 패키지를 추가하는 방법에 대한 지침은 패키지 사용 작업 흐름(NuGet 문서)패키지 설치 및 관리 섹션에서 확인할 수 있습니다. 올바른 패키지 버전을 NuGet.org에서 확인하세요.

Program 파일에서:

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

프로젝트 템플릿 BlazorBlazor WebAssembly을 기반으로 호스팅된 솔루션의 경우, IWebAssemblyHostEnvironment.BaseAddressnew Uri(builder.HostEnvironment.BaseAddress)HttpClient.BaseAddress에 할당됩니다.

이전 등록은 기존 보안 기본 HttpClient 등록에 추가됩니다.

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

@inject IHttpClientFactory ClientFactory

...

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

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

        ...
    }
}

Note

이전 예제의 경우 서버 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} 자리 표시자는 앞의 예시에서 사용자 지정 범위입니다.

Note

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 = [ "{CUSTOM SCOPE 1}", "{CUSTOM SCOPE 2}" ]
    });

if (tokenResult.TryGetToken(out var token))
{
    ...
}
@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은 다음을 반환합니다.

  • true을(를) token에 사용합니다.
  • false 토큰이 검색되지 않은 경우

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

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

다음 정책에는 다음에 대한 구성이 포함됩니다.

  • 요청 원본(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 하는 호스트 Blazor WebAssembly 된 솔루션은 클라이언트 및 서버 앱에 대해 동일한 기본 주소를 사용합니다. 클라이언트 앱의 HttpClient.BaseAddress이(가) builder.HostEnvironment.BaseAddress의 URI로 설정됩니다. 호스트 Blazor 된 솔루션의 기본 구성에는 CORS 구성이 필요하지 않습니다. 서버 프로젝트에서 호스트되지 않고 서버 앱의 기본 주소를 공유 하지 않는 추가 클라이언트 앱에는 서버 프로젝트에서 CORS 구성이 필요합니다.

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

토큰 요청 오류 처리

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

일반적으로 사용자에 대해 IP가 내보내는 토큰은 일반적으로 약 1시간 동안 짧은 시간 동안 유효하므로 클라이언트 앱은 정기적으로 새 토큰을 가져와야 합니다. 그렇지 않으면 부여된 토큰이 만료된 후 사용자가 로그아웃됩니다. 대부분의 경우 OIDC 클라이언트는 사용자가 인증 상태 또는 IP 내에 유지되는 "세션" 덕분에 다시 인증할 필요 없이 새 토큰을 프로비전할 수 있습니다.

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

이러한 시나리오는 토큰 기반 인증과 관련이 없습니다. 이는 SPA의 특성의 일부입니다. 또한 인증 cookie 이 제거되면 쿠키를 사용하는 SPA에서 서버 API를 호출하지 못합니다.

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

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

앱이 토큰을 요청하면 다음과 같은 두 가지 결과가 발생할 수 있습니다.

  • 요청이 성공하고 앱에 유효한 토큰이 있습니다.
  • 요청이 실패하고 앱이 사용자를 다시 인증하여 새 토큰을 가져와야 합니다.

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

  • 세션 스토리지에 현재 페이지 상태를 저장합니다. 라이프사이클 메서드 (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() => 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;
    }
}

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

ApplicationAuthenticationState.cs:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

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

구성 요소(Authentication)는 Authentication.razor 직렬화 및 역직렬화 메서드 StateContainer 와 함께 로컬 세션 스토리지를 GetStateForLocalStorage 사용하여 앱의 상태를 저장하고 복원합니다SetStateFromLocalStorage.

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

builder.Services.AddSingleton<StateContainer>();

앱 경로 사용자 지정

라이브러리는 Microsoft.AspNetCore.Components.WebAssembly.Authentication 다음 표에 표시된 경로를 사용하여 다른 인증 상태를 나타냅니다.

Route Purpose
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 조각 집합이 포함되어 있습니다. 각 상태는 사용자 정의를 통해 맞춤 설정할 수 있습니다. 초기 로그인 프로세스 중에 표시된 텍스트를 사용자 지정하려면 다음과 같이 변경할 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은/는 인증 경로당 하나씩 사용할 수 있는 항목을 다음 표에 나열합니다.

Route Fragment
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 클레임은 Microsoft ID 플랫폼 v1.0 페이로드 클레임토큰의 주체가 인증된 방법을 식별합니다. 이 예제에서는 에 따라 사용자 지정 사용자 계정 클래스를 RemoteUserAccount사용합니다.

RemoteUserAccount 클래스를 상속하는 클래스를 만드세요. 사용자의 JSON 속성 값 배열에 AuthenticationMethod 속성을 설정하는 다음 예제를 보십시오. AuthenticationMethod 는 사용자가 인증될 때 프레임워크에 의해 자동으로 채워집니다.

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(NavigationManager navigation,
    IAccessTokenProviderAccessor accessor)
    : AccountClaimsPrincipalFactory<CustomUserAccount>(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;
    }
}

사용 중인 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 관리자 역할 및 사용자 지정 사용자 계정 클래스와 함께 작동하는 추가 예제는 Microsoft Entra ID 그룹 및 역할이 있는 ASP.NET CoreBlazor WebAssembly를 참조하세요.

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

인증 및 권한 부여가 필요한 콘텐츠를 미리 렌더링하는 것은 현재 지원되지 않습니다. Blazor WebAssembly 보안 앱 주제 중 하나의 지침을 따른 후, 다음 지침을 사용하여 앱을 만드세요.

  • 권한 부여가 필요하지 않은 경로를 미리 렌더링합니다.
  • 권한 부여가 필요한 경로를 미리 렌더링하지 않습니다.

Client 프로젝트의 Program 파일의 경우, 일반적인 서비스 등록을 별도의 메서드로 분리하여 팩터링하십시오. 예를 들어, ConfigureCommonServices 프로젝트에서 Client 메서드를 생성하십시오. 일반적인 서비스는 개발자가 클라이언트 및 서버 프로젝트에서 사용하기 위해 등록하는 서비스입니다.

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);
}

프레임워크 서버 인증 공급자()Blazor에 대한 ServerAuthenticationStateProvider 자세한 내용은 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만 호출하도록 사용자 인증

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

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 옵션을 구성합니다. 다음 예제에서는 v2.0 세그먼트를 Authority 속성에 추가하여 ME-ID이 v2.0으로 구성됩니다.

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 구성 및 사용

Blazor WebAssembly를 사용하도록 앱을 구성 하려면 다음을 수행합니다.

  • 서버에서 gRPC-Web을 사용하도록 설정합니다. 자세한 내용은 ASP.NET Core gRPC 앱의 gRPC-Web을 참조하세요.
  • 앱의 메시지 처리기에 대한 gRPC 서비스를 등록합니다. 다음 예제에서는 gRPC 자습서(GreeterClient파일)의 서비스를 사용하도록 앱의 권한 부여 메시지 처리기를 구성합니다.

Note

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

Note

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

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();
        }
    }
}

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

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

AuthenticationService 구현을 교체하세요

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

  • 다양한 JavaScript AuthenticationService 구현.
  • JavaScript용 Microsoft 인증 라이브러리(MSAL.js).

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> 태그를 폴더에서 CustomAuthenticationService.js 명명 wwwroot/js 된 라이브러리를 로드하는 태그로 바꾸는 방법을 보여 줍니다.

닫는 wwwroot/index.html 태그 내의 Blazor 스크립트(_framework/blazor.webassembly.js) 앞에서 </body> 하기 전에:

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

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

Note

문서 링크는 .NET 참조 소스를 가리키며, 일반적으로 저장소의 기본 브랜치를 로드합니다. 이는 .NET의 다음 릴리스를 위한 현재 개발 상태를 나타냅니다. 특정 릴리스를 위한 태그를 선택하려면 Switch branches or tags 드롭다운 목록을 사용하세요. 자세한 내용은 ASP.NET Core 소스 코드(dotnet/AspNetCore.Docs #26205)의 버전 태그를 선택하는 방법을 참조하세요.

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

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

  1. 시스템에 최신 개발자 .NET SDK가 있는지 확인하거나 .NET SDK: 설치 관리자 및 이진 파일에서 최신 개발자 SDK를 가져오고 설치합니다. 이 시나리오에서는 내부 NuGet 피드 구성이 필요하지 않습니다.
  2. Source의 dotnet/aspnetcoreBuild ASP.NET Core 설명서에 따라 개발을 위해 GitHub 리포지토리를 설정합니다. GitHub 리포지토리의dotnet/aspnetcore ZIP 보관 파일을 포크하고 복제하거나 다운로드합니다.
  3. src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json 파일을 열고 원하는 버전의 @azure/msal-browser.를 설정합니다. 릴리스된 버전 목록은 npm 웹 사이트를 방문하여@azure/msal-browser버전 탭을 선택합니다.
  4. Authentication.Msal 명령 셸의 src/Components/WebAssembly/Authentication.Msal/src 명령을 사용하여 yarn build 폴더에 프로젝트를 빌드합니다.
  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

클래스의 구조는 JSON이 serialize System.Text.Json될 때 라이브러리가 예상하는 것과 일치해야 합니다.

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

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; set; } = [ "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.ProviderOptions.Authority = "...";
        options.ProviderOptions.MetadataUrl = "...";
        options.ProviderOptions.ClientId = "...";
        options.ProviderOptions.DefaultScopes = [ "openid", "profile", "myApi" ];
        options.ProviderOptions.RedirectUri = "https://localhost:5001/authentication/login-callback";
        options.ProviderOptions.PostLogoutRedirectUri = "https://localhost:5001/authentication/logout-callback";
        options.ProviderOptions.ResponseType = "...";
        options.ProviderOptions.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"
    }
    

추가 리소스