Identity 서버를 사용하여 호스트된 ASP.NET Core Blazor WebAssembly 앱 보호

이 문서에서는 Duende Identity Server를 사용하여 사용자와 API 호출을 인증하는 호스트형 Blazor WebAssembly 솔루션을 만드는 방법을 설명합니다.

Important

Duende Software에서 Duende Identity 서버의 프로덕션 사용에 대한 라이선스 요금 지불을 요구할 수 있습니다. 자세한 내용은 ASP.NET Core 5.0에서 6.0으로 마이그레이션을 참조하세요.

참고 항목

기존, 외부 Identity 서버를 사용하도록 독립 실행형 또는 호스팅된 Blazor WebAssembly 앱을 구성하려면 인증 라이브러리를 사용한 ASP.NET Core Blazor WebAssembly 독립 실행형 앱 보안의 지침을 따릅니다.

이 문서를 읽은 후 추가 보안 시나리오에 대한 자세한 내용은 ASP.NET Core Blazor WebAssembly 추가 보안 시나리오를 참조하세요.

연습

연습의 하위 섹션에서는 다음 방법을 설명합니다.

  • Blazor 앱 만들기
  • 앱 실행

Blazor 앱 만들기

인증 메커니즘을 사용하여 새 Blazor WebAssembly 프로젝트를 만들려면

  1. 새 프로젝트를 만듭니다.

  2. Blazor WebAssembly 앱 템플릿을 선택합니다. 다음을 선택합니다.

  3. 대시를 사용하지 않고 프로젝트 이름을 제공합니다. 위치가 올바른지 확인합니다. 다음을 선택합니다.

    OIDC 앱 식별자의 형성을 중단하는 프로젝트 이름에 대시(-)를 사용하지 마세요. 프로젝트 템플릿의 Blazor WebAssembly 논리는 솔루션 구성에서 OIDC 앱 식별자의 프로젝트 이름을 사용하며 OIDC 앱 식별자에서는 대시가 허용되지 않습니다. 파스칼식 대/소문자(BlazorSample) 또는 밑줄(Blazor_Sample)은 사용 가능한 대안입니다.

  4. 추가 정보 대화 상자에서 인증 유형으로 개별 계정을 선택하여 ASP.NET Core의 Identity 시스템을 사용하여 앱 내에 사용자를 저장합니다.

  5. ASP.NET Core 호스트됨 확인란을 선택합니다.

  6. 만들기 단추를 선택하여 앱을 만듭니다.

앱 실행

프로젝트에서 앱을 실행합니다 Server . Visual Studio를 사용하는 경우 다음 중 하나를 수행합니다.

  • 실행 단추 옆에 있는 드롭다운 화살표를 선택합니다. 드롭다운 목록에서 시작 프로젝트 구성을 엽니다. 단일 시작 프로젝트 옵션을 선택합니다. 시작 프로젝트의 프로젝트를 프로젝트로 확인하거나 변경합니다 Server .

  • Server 다음 방법 중 한 가지 방법으로 앱을 시작하기 전에 솔루션 탐색기 프로젝트가 강조 표시되어 있는지 확인합니다.

    • 실행 단추를 선택합니다.
    • 메뉴에서 디버그>디버깅 시작을 사용합니다.
    • F5키를 누릅니다.
  • 명령 셸에서 솔루션의 Server 프로젝트 폴더로 이동합니다. dotnet run 명령을 실행합니다.

솔루션의 일부

이 섹션에서는 프로젝트 템플릿에서 Blazor WebAssembly 생성된 솔루션의 부분에 대해 설명하고 솔루션 ClientServer 프로젝트가 참조하도록 구성된 방법을 설명합니다. 연습 섹션의 지침을 사용하여 앱을 만든 경우 기본 작업 애플리케이션에 대해 이 섹션에서 따라야 할 구체적인 지침은 없습니다. 이 섹션의 지침은 사용자를 인증하고 권한을 부여하도록 앱을 업데이트하는 데 유용합니다. 그러나 앱을 업데이트하는 다른 방법은 연습 섹션의 지침에서 새 앱을 만들고 앱의 구성 요소, 클래스 및 리소스를 새 앱으로 이동하는 것입니다.

Server app services

‘이 섹션은 솔루션의 Server 앱에 적용됩니다.’

다음 서비스가 등록됩니다.

  • Program 파일에서:

    • Entity Framework Core 및 ASP.NET Core Identity:

      builder.Services.AddDbContext<ApplicationDbContext>(options =>
          options.UseSqlite( ... ));
      builder.Services.AddDatabaseDeveloperPageExceptionFilter();
      
      builder.Services.AddDefaultIdentity<ApplicationUser>(options => 
              options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<ApplicationDbContext>();
      
    • Identity서버 맨 위에 Identity 기본 ASP.NET Core 규칙을 설정하는 추가 AddApiAuthorization 도우미 메서드가 있는 서버:

      builder.Services.AddIdentityServer()
          .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
      
    • 서버에서 생성된 Identity JWT 토큰의 유효성을 검사하도록 앱을 구성하는 추가 AddIdentityServerJwt 도우미 메서드로 인증:

      builder.Services.AddAuthentication()
          .AddIdentityServerJwt();
      
  • Startup.csStartup.ConfigureServices에서 다음을 수행합니다.

    • Entity Framework Core 및 ASP.NET Core Identity:

      services.AddDbContext<ApplicationDbContext>(options =>
          options.UseSqlite(
              Configuration.GetConnectionString("DefaultConnection")));
      
      services.AddDefaultIdentity<ApplicationUser>(options => 
              options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<ApplicationDbContext>();
      
    • Identity서버 맨 위에 Identity 기본 ASP.NET Core 규칙을 설정하는 추가 AddApiAuthorization 도우미 메서드가 있는 서버:

      services.AddIdentityServer()
          .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
      
    • 서버에서 생성된 Identity JWT 토큰의 유효성을 검사하도록 앱을 구성하는 추가 AddIdentityServerJwt 도우미 메서드로 인증:

      services.AddAuthentication()
          .AddIdentityServerJwt();
      

참고 항목

단일 인증 체계가 등록되면 인증 체계가 앱의 기본 체계로 자동으로 사용되며 구성표를 AddAuthentication 또는 AuthenticationOptions을 통해 지정할 필요가 없습니다. 자세한 내용은 ASP.NET Core 인증 개요ASP.NET Core 알림(aspnet/Announcements #490)을 참조하세요.

  • Program 파일에서:
  • Startup.csStartup.Configure에서 다음을 수행합니다.
  • 서버 미들웨어는 Identity OIDC(OpenID 커넥트) 엔드포인트를 노출합니다.

    app.UseIdentityServer();
    
  • 인증 미들웨어는 요청 자격 증명의 유효성을 검사하고 요청 컨텍스트에서 사용자를 설정해야 합니다.

    app.UseAuthentication();
    
  • 인증 미들웨어가 인증 기능을 사용하도록 설정합니다.

    app.UseAuthorization();
    

API 권한 부여

‘이 섹션은 솔루션의 Server 앱에 적용됩니다.’

AddApiAuthorization 도우미 메서드는 ASP.NET Core 시나리오에 대해 Identity Server를 구성합니다. Identity Server는 앱 보안 문제를 처리하는 강력하고 확장성 있는 프레임워크입니다. Identity 서버는 가장 일반적인 시나리오에 불필요한 복잡성을 노출합니다. 따라서 좋은 출발점이라고 생각되는 몇 가지 규칙과 구성 옵션 세트가 제공됩니다. 인증을 변경해야 하면 앱의 요구 사항에 맞게 인증을 사용자 지정할 수 있는 서버의 Identity 모든 기능을 사용할 수 있습니다.

서버와 Identity 공존하는 API에 대한 인증 처리기 추가

‘이 섹션은 솔루션의 Server 앱에 적용됩니다.’

AddIdentityServerJwt 도우미 메서드는 앱의 정책 체계를 기본 인증 처리기로서 구성합니다. 정책은 URL 공간의 모든 하위 경로 Identity 로 라우팅되는 모든 요청을 처리할 수 있도록 Identity 구성됩니다/Identity. JwtBearerHandler는 다른 모든 요청을 처리합니다. 이 메서드는 또한

  • API 리소스를 서버의 Identity 기본 범위 {PROJECT NAME}API로 등록합니다. 여기서 {PROJECT NAME} 자리 표시자는 앱을 만들 때 프로젝트의 이름입니다.
  • 앱에 대해 서버에서 발급 Identity 한 토큰의 유효성을 검사하도록 JWT 전달자 토큰 미들웨어를 구성합니다.

일기 예보 컨트롤러

‘이 섹션은 솔루션의 Server 앱에 적용됩니다.’

WeatherForecastController(Controllers/WeatherForecastController.cs)에서는 [Authorize] 특성이 클래스에 적용됩니다. 이 특성은 사용자가 리소스에 액세스하려면 기본 정책을 기준으로 인증되어야 함을 나타냅니다. 기본 인증 정책은 기본 인증 체계를 사용하도록 구성되었으며, 기본 인증 체계는 AddIdentityServerJwt에 의해 설정됩니다. 도우미 메서드는 JwtBearerHandler를 앱에 대한 요청의 기본 처리기로 구성합니다.

애플리케이션 데이터베이스 컨텍스트

‘이 섹션은 솔루션의 Server 앱에 적용됩니다.’

(ApplicationDbContextData/ApplicationDbContext.csDbContext)에서 서버에 대한 Identity 스키마를 포함하도록 확장됩니다.ApiAuthorizationDbContext<TUser> ApiAuthorizationDbContext<TUser>IdentityDbContext에서 파생됩니다.

데이터베이스 스키마에 대한 모든 권한을 얻으려면 사용 가능한 IdentityDbContext 클래스 중 하나에서 상속하고 OnModelCreating 메서드에서 builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value)를 호출하여 Identity 스키마를 포함하도록 컨텍스트를 구성합니다.

OIDC 구성 컨트롤러

‘이 섹션은 솔루션의 Server 앱에 적용됩니다.’

OidcConfigurationController(Controllers/OidcConfigurationController.cs)에서 클라이언트 엔드포인트가 OIDC 매개 변수를 제공하도록 프로비저닝됩니다.

앱 설정

‘이 섹션은 솔루션의 Server 앱에 적용됩니다.’

프로젝트 루트에 있는 앱 설정 파일(appsettings.json)의 IdentityServer 섹션은 구성된 클라이언트 목록을 설명합니다. 다음 예제에는 단일 클라이언트가 있습니다. 클라이언트 이름은 앱의 어셈블리 이름에 Client 해당하며 규칙에 따라 OAuth ClientId 매개 변수에 매핑됩니다. 프로필은 구성되는 앱 유형을 나타냅니다. 프로필은 서버에 대한 구성 프로세스를 간소화하는 규칙을 구현하기 위해 내부적으로 사용됩니다.

"IdentityServer": {
  "Clients": {
    "{ASSEMBLY NAME}": {
      "Profile": "IdentityServerSPA"
    }
  }
}

자리 표시자는 {ASSEMBLY NAME} 앱의 어셈블리 이름(예: BlazorSample.Client)입니다Client.

인증 패키지

‘이 섹션은 솔루션의 Client 앱에 적용됩니다.’

앱이 개별 사용자 계정(Individual)을 사용하도록 만들어진 경우 해당 앱은 자동으로 Microsoft.AspNetCore.Components.WebAssembly.Authentication 패키지에 대한 패키지 참조를 받습니다. 패키지는 앱이 사용자를 인증하고 보호된 API를 호출하는 데 사용할 토큰을 가져올 수 있도록 지원하는 기본 형식 세트를 제공합니다.

앱에 인증을 추가하는 경우에는 Microsoft.AspNetCore.Components.WebAssembly.Authentication 패키지를 앱에 수동으로 추가합니다.

참고 항목

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

HttpClient 구성

‘이 섹션은 솔루션의 Client 앱에 적용됩니다.’

Program 파일에서 명명된 이름은 HttpClient 서버 API에 요청할 때 액세스 토큰을 포함하는 인스턴스를 제공 HttpClient 하도록 구성됩니다. 기본적으로 솔루션 생성 시 이름은 자리 HttpClient{PROJECT NAME}.ServerAPI{PROJECT NAME} 표시자가 프로젝트의 이름입니다.

builder.Services.AddHttpClient("{PROJECT NAME}.ServerAPI", 
        client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("{PROJECT NAME}.ServerAPI"));

자리 표시자는 {PROJECT NAME} 솔루션 생성 시 프로젝트 이름입니다. 예를 들어 프로젝트 이름을 BlazorSample 제공하면 이름이 HttpClientBlazorSample.ServerAPI입니다.

참고 항목

호스트형 Blazor 솔루션의 일부가 아닌 기존 Identity 서버 인스턴스를 사용하도록 Blazor WebAssembly 앱을 구성하는 경우, HttpClient 기준 주소 등록을 IWebAssemblyHostEnvironment.BaseAddress(builder.HostEnvironment.BaseAddress)에서 서버 앱의 API 권한 부여 엔드포인트 URL로 변경합니다.

API 권한 부여 지원

‘이 섹션은 솔루션의 Client 앱에 적용됩니다.’

사용자 인증에 대한 지원은 Microsoft.AspNetCore.Components.WebAssembly.Authentication 패키지 내에서 제공하는 확장 메서드를 통해 서비스 컨테이너에 연결되어 있습니다. 이 메서드는 앱이 기존 인증 시스템과 상호 작용하는 데 필요한 서비스를 설정합니다.

builder.Services.AddApiAuthorization();

기본적으로 앱에 대한 구성은 규칙에 따라 _configuration/{client-id}에서 로드됩니다. 규칙에 따라 클라이언트 ID는 앱의 어셈블리 이름으로 설정됩니다. 이 URL은 옵션을 사용하여 오버로드를 호출함으로써 별도의 엔드포인트를 가리키도록 변경될 수 있습니다.

Imports 파일

‘이 섹션은 솔루션의 Client 앱에 적용됩니다.’

Microsoft.AspNetCore.Components.Authorization 네임스페이스는 _Imports.razor 파일을 통해 앱 전체에서 사용할 수 있게 됩니다.

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

Index 페이지

‘이 섹션은 솔루션의 Client 앱에 적용됩니다.’

인덱스 페이지(wwwroot/index.html)는 JavaScript로 AuthenticationService를 정의하는 스크립트를 포함합니다. AuthenticationService는 OIDC 프로토콜의 하위 수준 세부 정보를 처리합니다. 앱은 내부적으로 스크립트에 정의된 메서드를 호출하여 인증 작업을 수행합니다.

<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>

App 구성 요소

‘이 섹션은 솔루션의 Client 앱에 적용됩니다.’

App 구성 요소(App.razor)는 Blazor Server 앱에 있는 App 구성 요소와 비슷합니다.

  • CascadingAuthenticationState 구성 요소는 앱의 나머지 부분에 AuthenticationState를 노출하는 것을 관리합니다.
  • AuthorizeRouteView 구성 요소는 현재 사용자가 지정된 페이지에 액세스할 수 있는 권한이 부여받았는지 확인하고 그러지 않은 경우 RedirectToLogin 구성 요소를 렌더링합니다.
  • RedirectToLogin 구성 요소는 권한 없는 사용자를 로그인 페이지로 리디렉션하는 기능을 관리합니다.

ASP.NET Core 릴리스 간 프레임워크 변경으로 인해 App 구성 요소의 Razor 변경 내용(App.razor)은 이 섹션에 표시되지 않습니다. 지정된 릴리스의 구성 요소의 변경 내용을 검사하려면 다음 방법 중 하나를 사용합니다.

  • 사용하려는 ASP.NET Core 버전의 기본 Blazor WebAssembly 프로젝트 템플릿에서 인증을 위해 프로비전된 앱을 만듭니다. 생성된 앱에서 App 구성 요소(App.razor)를 검사합니다.

  • 참조 소스에서 App 구성 요소(App.razor)를 검사합니다. 분기 선택기에서 버전을 선택하고 수년에 걸쳐 이동했기 때문에 리포지토리 폴더에서 구성 요소를 ProjectTemplates 검색합니다.

    참고 항목

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

RedirectToLogin 구성 요소

‘이 섹션은 솔루션의 Client 앱에 적용됩니다.’

RedirectToLogin 구성 요소(RedirectToLogin.razor)는 다음을 수행합니다.

  • 로그인 페이지로의 권한 없는 사용자 리디렉션을 관리합니다.
  • 사용자가 액세스를 시도하는 현재 URL은 다음을 사용하여 인증에 성공한 경우 해당 페이지로 반환될 수 있도록 기본.

참조 소스에서 RedirectToLogin 구성 요소를 검사합니다. 구성 요소의 위치는 시간이 지남에 따라 변경되므로 GitHub 검색 도구를 사용하여 구성 요소를 찾습니다.

참고 항목

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

LoginDisplay 구성 요소

‘이 섹션은 솔루션의 Client 앱에 적용됩니다.’

LoginDisplay 구성 요소(LoginDisplay.razor)는 MainLayout 구성 요소(MainLayout.razor)에서 렌더링되고 다음 동작을 관리합니다.

  • 인증된 사용자의 경우:
    • 현재 사용자 이름을 표시합니다.
    • ASP.NET Core Identity에 있는 사용자 프로필 페이지로 연결되는 링크를 제공합니다.
    • 앱에서 로그아웃하는 단추를 제공합니다.
  • 익명 사용자의 경우:
    • 등록 옵션을 제공합니다.
    • 로그인 옵션을 제공합니다.

ASP.NET Core 릴리스 간 프레임워크 변경으로 인해 LoginDisplay 구성 요소의 Razor 변경 내용은 이 섹션에 표시되지 않습니다. 지정된 릴리스의 구성 요소의 변경 내용을 검사하려면 다음 방법 중 하나를 사용합니다.

  • 사용하려는 ASP.NET Core 버전의 기본 Blazor WebAssembly 프로젝트 템플릿에서 인증을 위해 프로비전된 앱을 만듭니다. 생성된 앱에서 LoginDisplay 구성 요소를 검사합니다.

  • 참조 소스에서 LoginDisplay 구성 요소를 검사합니다. 구성 요소의 위치는 시간이 지남에 따라 변경되므로 GitHub 검색 도구를 사용하여 구성 요소를 찾습니다. true와 같은 Hosted에 대한 템플릿 콘텐츠가 사용됩니다.

    참고 항목

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

Authentication 구성 요소

‘이 섹션은 솔루션의 Client 앱에 적용됩니다.’

Authentication 구성 요소에 의해 생성된 페이지(Pages/Authentication.razor)는 다른 인증 단계를 처리하는 데 필요한 경로를 정의합니다.

RemoteAuthenticatorView 구성 요소는 다음과 같습니다.

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

<RemoteAuthenticatorView Action="Action" />

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

참고 항목

NRT(Nullable 참조 형식) 및 .NET 컴파일러 null 상태 정적 분석은 .NET 6 이상의 ASP.NET Core에서 지원됩니다. .NET 6에서 ASP.NET Core가 릴리스되기 전에 형식은 string null 형식 지정(?)없이 표시됩니다.

FetchData 구성 요소

‘이 섹션은 솔루션의 Client 앱에 적용됩니다.’

FetchData 구성 요소는 다음을 수행하는 방법을 보여 줍니다.

  • 액세스 토큰을 프로비저닝합니다.
  • 액세스 토큰을 사용하여 ‘서버’ 앱에서 보호된 리소스 API를 호출합니다.

@attribute [Authorize] 지시문은 사용자가 이 구성 요소에 액세스하기 위해 권한을 부여받아야 한다는 것을 Blazor WebAssembly 권한 부여 시스템에 나타냅니다. Client 앱에 특성이 있으면 서버에 있는 API가 적절한 자격 증명 없이 호출되는 것을 방지하지 않습니다. 또한 Server 앱은 적절한 엔드포인트에서 [Authorize]를 사용하여 올바로 보호해야 합니다.

IAccessTokenProvider.RequestAccessToken은 API를 호출하는 요청에 추가할 수 있는 액세스 토큰을 요청하는 작업을 처리합니다. 토큰이 캐시되었거나 서비스에서 사용자 개입 없이 새 액세스 토큰을 프로비저닝할 수 있으면 토큰 요청이 성공합니다. 그러지 않으면 토큰 요청이 실패하고 AccessTokenNotAvailableExceptiontry-catch 문에 catch됩니다.

요청에 포함할 실제 토큰을 가져오려면 앱에서 tokenResult.TryGetToken(out var token)을 호출하여 요청이 성공했는지 확인해야 합니다.

요청이 성공하면 토큰 변수가 액세스 토큰으로 채워집니다. 토큰의 AccessToken.Value 속성은 Authorization 요청 헤더에 포함할 리터럴 문자열을 노출합니다.

사용자 개입 없이 토큰을 프로비저닝할 수 없어 요청이 실패한 경우:

  • .NET 7 이상의 ASP.NET Core: 앱은 지정된 AccessTokenResult.InteractionOptions 토큰을 사용하여 액세스 토큰을 새로 고치도록 탐색 AccessTokenResult.InteractiveRequestUrl 합니다.
  • .NET 6 이하의 ASP.NET Core: 토큰 결과에 리디렉션 URL이 포함됩니다. 이 URL로 이동하면 사용자가 로그인 페이지로 이동하고 인증 성공 후 현재 페이지로 돌아옵니다.
@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using {APP NAMESPACE}.Shared
@attribute [Authorize]
@inject HttpClient Http

...

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

Linux의 Azure App Service

Azure App Service on Linux에 배포할 때 발급자를 명시적으로 지정합니다. 자세한 내용은 SPA에 대한 Web API 백 엔드 보안을 위한 사용을 Identity 참조하세요.

API 권한 부여를 사용한 이름 및 역할 클레임

사용자 지정 사용자 팩터리

Client 앱에서 사용자 지정 사용자 팩터리를 만듭니다. Identity 서버가 단일 role 클레임에서 여러 역할을 JSON 배열로 보냅니다. 클레임에서 단일 역할이 문자열 값으로 전송됩니다. 팩터리가 각 사용자 역할에 대해 개별 role 클레임을 만듭니다.

CustomUserFactory.cs:

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

public class CustomUserFactory
    : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
    public CustomUserFactory(IAccessTokenProviderAccessor accessor)
        : base(accessor)
    {
    }

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

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            var identity = (ClaimsIdentity)user.Identity;
            var roleClaims = identity.FindAll(identity.RoleClaimType).ToArray();

            if (roleClaims.Any())
            {
                foreach (var existingClaim in roleClaims)
                {
                    identity.RemoveClaim(existingClaim);
                }

                var rolesElem = 
                    account.AdditionalProperties[identity.RoleClaimType];

                if (options.RoleClaim is not null && rolesElem is JsonElement roles)
                {
                    if (roles.ValueKind == JsonValueKind.Array)
                    {
                        foreach (var role in roles.EnumerateArray())
                        {
                            var roleValue = role.GetString();

                            if (!string.IsNullOrEmpty(roleValue))
                            {
                                identity.AddClaim(
                                  new Claim(options.RoleClaim, roleValue));
                            }

                        }
                    }
                    else
                    {
                        var roleValue = roles.GetString();

                        if (!string.IsNullOrEmpty(roleValue))
                        {
                            identity.AddClaim(
                              new Claim(options.RoleClaim, roleValue));
                        }
                    }
                }
            }
        }

        return user;
    }
}
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomUserFactory
    : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
    public CustomUserFactory(IAccessTokenProviderAccessor accessor)
        : base(accessor)
    {
    }

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

        if (user.Identity.IsAuthenticated)
        {
            var identity = (ClaimsIdentity)user.Identity;
            var roleClaims = identity.FindAll(identity.RoleClaimType).ToArray();

            if (roleClaims.Any())
            {
                foreach (var existingClaim in roleClaims)
                {
                    identity.RemoveClaim(existingClaim);
                }

                var rolesElem = account.AdditionalProperties[identity.RoleClaimType];

                if (rolesElem is JsonElement roles)
                {
                    if (roles.ValueKind == JsonValueKind.Array)
                    {
                        foreach (var role in roles.EnumerateArray())
                        {
                            identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
                        }
                    }
                    else
                    {
                        identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
                    }
                }
            }
        }

        return user;
    }
}

앱에서 Client 파일에 팩터리를 등록합니다.Program

builder.Services.AddApiAuthorization()
    .AddAccountClaimsPrincipalFactory<CustomUserFactory>();

Server 앱에서 역할 관련 서비스를 추가하는 작성기를 호출 AddRolesIdentity 합니다.

Program 파일에서:

using Microsoft.AspNetCore.Identity;

...

builder.Services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

Startup.cs의 경우

using Microsoft.AspNetCore.Identity;

...

services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

Identity Server 구성

다음 방법 중 하나를 사용합니다.

API 권한 부여 옵션

Server 앱에서:

  • Identity 서버가 namerole 클레임을 ID 토큰 및 액세스 토큰에 배치하도록 구성합니다.
  • JWT 토큰 처리기에서 역할의 기본 매핑을 방지합니다.

Program 파일에서:

using System.IdentityModel.Tokens.Jwt;

...

builder.Services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
        options.IdentityResources["openid"].UserClaims.Add("name");
        options.ApiResources.Single().UserClaims.Add("name");
        options.IdentityResources["openid"].UserClaims.Add("role");
        options.ApiResources.Single().UserClaims.Add("role");
    });

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

Startup.cs의 경우

using System.IdentityModel.Tokens.Jwt;
using System.Linq;

...

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
        options.IdentityResources["openid"].UserClaims.Add("name");
        options.ApiResources.Single().UserClaims.Add("name");
        options.IdentityResources["openid"].UserClaims.Add("role");
        options.ApiResources.Single().UserClaims.Add("role");
    });

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

프로필 서비스

Server 앱에서 ProfileService 구현을 만듭니다.

ProfileService.cs:

using IdentityModel;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;

public class ProfileService : IProfileService
{
    public ProfileService()
    {
    }

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var nameClaim = context.Subject.FindAll(JwtClaimTypes.Name);
        context.IssuedClaims.AddRange(nameClaim);

        var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
        context.IssuedClaims.AddRange(roleClaims);

        await Task.CompletedTask;
    }

    public async Task IsActiveAsync(IsActiveContext context)
    {
        await Task.CompletedTask;
    }
}
using IdentityModel;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using System.Threading.Tasks;

public class ProfileService : IProfileService
{
    public ProfileService()
    {
    }

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var nameClaim = context.Subject.FindAll(JwtClaimTypes.Name);
        context.IssuedClaims.AddRange(nameClaim);

        var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
        context.IssuedClaims.AddRange(roleClaims);

        await Task.CompletedTask;
    }

    public async Task IsActiveAsync(IsActiveContext context)
    {
        await Task.CompletedTask;
    }
}

앱에서 Server 프로필 서비스를 파일에 등록합니다.Program

using Duende.IdentityServer.Services;

...

builder.Services.AddTransient<IProfileService, ProfileService>();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

앱에서 Server 다음의 Profile Service를 등록합니다Startup.cs.Startup.ConfigureServices

using IdentityServer4.Services;

...

services.AddTransient<IProfileService, ProfileService>();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

권한 부여 메커니즘 사용

이 시점에서는 Client 앱의 구성 요소 권한 부여 방식이 작동합니다. 구성 요소의 모든 권한 부여 메커니즘에서 역할을 사용하여 사용자에게 권한을 부여할 수 있습니다.

Client 앱에서 User.Identity.Name에 사용자의 사용자 이름이 채워집니다. 이는 보통 사용자의 로그인 전자 메일 주소입니다.

UserManagerSignInManager

서버 앱에 다음이 필요한 경우 사용자 ID 클레임 유형을 설정합니다.

Program.cs.NET 6 이상에서 ASP.NET Core의 경우:

using System.Security.Claims;

...

builder.Services.Configure<IdentityOptions>(options => 
    options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

ASP.NET Core 6.0 이전 버전의 Startup.ConfigureServices에서:

using System.Security.Claims;

...

services.Configure<IdentityOptions>(options => 
    options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

다음은 WeatherForecastController 메서드가 호출되는 UserName 시기를 Get 기록합니다.

참고 항목

다음 예제에서는 C# 10 이상(.NET 6 이상) 기능인 파일 범위 네임스페이스를 사용합니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using BlazorSample.Server.Models;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers;

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly UserManager<ApplicationUser> userManager;

    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", 
        "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger, 
        UserManager<ApplicationUser> userManager)
    {
        this.logger = logger;
        this.userManager = userManager;
    }

    [HttpGet]
    public async Task<IEnumerable<WeatherForecast>> Get()
    {
        var rng = new Random();

        var user = await userManager.GetUserAsync(User);

        if (user != null)
        {
            logger.LogInformation("User.Identity.Name: {UserIdentityName}", user.UserName);
        }

        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

앞의 예에서:

  • Server 프로젝트의 네임스페이스는 BlazorSample.Server입니다.
  • Shared 프로젝트의 네임스페이스는 BlazorSample.Shared입니다.

사용자 지정 도메인과 인증서를 사용하여 Azure App Service에서 호스트

다음 지침에서 설명하는 것은 아래와 같습니다.

  • Identity 서버와 함께 호스트형 Blazor WebAssembly 앱을 사용자 지정 도메인을 사용하는 Azure App Service에 배포하는 방법.
  • 브라우저와의 HTTPS 프로토콜 통신을 위해 TLS 인증서를 만들고 사용하는 방법. 이 지침은 사용자 지정 도메인에 이 인증서를 사용하는 방법을 중점적으로 설명하지만 contoso.azurewebsites.net와 같은 기본 Azure 앱 도메인을 사용하는 경우에도 똑같이 적용할 수 있습니다.

이 호스팅 시나리오에서는 Identity 서버의 토큰 서명 키 및 브라우저에 대한 사이트의 HTTPS 보안 통신에 동일한 인증서를 사용하지 않습니다.

  • 두 가지 해당 요구 사항에 서로 다른 인증서를 사용하는 것은 각 목적에 맞게 프라이빗 키를 격리하기 때문에 좋은 보안 사례입니다.
  • 브라우저와 통신하기 위한 TLS 인증서는 Identity 서버의 토큰 서명에 영향을 주지 않고 독립적으로 관리됩니다.
  • Azure Key Vault가 사용자 지정 도메인 바인딩을 위해 App Service 앱에 인증서를 제공하는 경우 Identity 서버는 토큰 서명을 위해 Azure Key Vault에서 동일한 인증서를 가져올 수 없습니다. 실제 경로에서 동일한 TLS 인증서를 사용하도록 Identity 서버를 구성할 수 있지만 보안 인증서를 소스 제어에 배치하는 것은 좋지 않은 사례이며 대부분의 시나리오에서 피해야 합니다.

다음 지침에서 자체 서명된 인증서는 Identity 서버 토큰 서명을 위해서만 Azure Key Vault에 생성됩니다. Identity 서버 구성은 앱의 CurrentUser>My 인증서 저장소를 통해 키 자격 증명 모음 인증서를 사용합니다. 사용자 지정 도메인을 사용하는 HTTPS 트래픽에 사용되는 기타 인증서는 Identity 서버 서명 인증서와 별도로 생성 및 구성됩니다.

사용자 지정 도메인 및 HTTPS를 사용하여 호스트하도록 앱, Azure App Service 및 Azure Key Vault를 구성하려면:

  1. Basic B1 이상의 계획 수준을 사용하여 App Service 요금제를 만듭니다. App Service에서는 사용자 지정 도메인을 사용하려면 Basic B1 이상 서비스 계층이 필요합니다.

  2. 조직이 제어하는 사이트 FQDN(정규화된 도메인 이름)의 일반 이름(예: www.contoso.com)을 사용하여 사이트 보안 브라우저 통신(HTTPS 프로토콜)용 PFX 인증서를 만듭니다. 다음을 사용하여 인증서를 만듭니다.

    • 키 사용
      • 디지털 시그니처 유효성 검사(digitalSignature)
      • 키 암호화(keyEncipherment)
    • 고급/확장 키 사용
      • 클라이언트 인증(1.3.6.1.5.5.7.3.2)
      • 서버 인증(1.3.6.1.5.5.7.3.1)

    인증서를 만들려면 다음 접근 방식 중 하나를 사용하거나 다른 적합한 도구나 온라인 서비스를 사용합니다.

    나중에 Azure Key Vault로 인증서를 가져오는 데 사용되는 암호를 기록해 둡니다.

    Azure Key Vault 인증서에 대한 자세한 내용은 Azure Key Vault: 인증서를 참조 하세요.

  3. 새 Azure Key Vault를 만들거나 Azure 구독에서 기존 키 자격 증명 모음을 사용합니다.

  4. 키 자격 증명 모음의 인증서 영역에서 PFX 사이트 인증서를 가져옵니다. 나중에 앱 구성에서 사용되는 인증서 지문을 기록합니다.

  5. Azure Key Vault에서 Identity 서버 토큰 서명을 위한 새로운 자체 서명된 인증서를 생성합니다. 인증서에 인증서 이름주체를 지정합니다. 주체CN={COMMON NAME}으로 지정됩니다. 여기서 {COMMON NAME} 자리 표시자는 인증서의 일반 이름입니다. 일반 이름은 영숫자 문자열일 수 있습니다. 예를 들어 CN=IdentityServerSigning은 유효한 인증서 주체입니다. 발급 정책>고급 정책 구성에서 기본 설정을 사용합니다. 나중에 앱 구성에서 사용되는 인증서 지문을 기록합니다.

  6. Azure Portal에서 Azure App Service로 이동하고 다음 구성을 사용하여 새 App Service를 만듭니다.

    • 게시Code로 설정합니다.
    • 런타임 스택을 앱 런타임으로 설정합니다.
    • Sku 및 크기의 경우 App Service 계층이 Basic B1 이상인지 확인합니다. App Service에서는 사용자 지정 도메인을 사용하려면 Basic B1 이상 서비스 계층이 필요합니다.
  7. Azure가 App Service를 만든 후 앱 구성을 열고 이전에 기록된 인증서 지문을 지정하는 새 애플리케이션 설정을 추가합니다. 앱 설정 키는 WEBSITE_LOAD_CERTIFICATES입니다. 다음 예제와 같이 앱 설정 값의 인증서 지문을 쉼표로 구분합니다.

    • 키: WEBSITE_LOAD_CERTIFICATES
    • 값: 57443A552A46DB...D55E28D412B943565,29F43A772CB6AF...1D04F0C67F85FB0B1

    Azure Portal에서 앱 설정을 저장하는 작업은 2단계 프로세스입니다. WEBSITE_LOAD_CERTIFICATES 키-값 설정을 저장한 다음, 블레이드 위쪽에서 저장 단추를 선택합니다.

  8. 앱의 TLS/SSL 설정을 선택합니다. 프라이빗 키 인증서(.pfx)를 선택합니다. Key Vault 인증서 가져오기 프로세스를 사용합니다. 이 프로세스를 두 번 사용하여 HTTPS 통신용 사이트 인증서 및 사이트의 자체 서명된 Identity 서버 토큰 서명 인증서를 둘 다 가져옵니다.

  9. 사용자 지정 도메인 블레이드로 이동합니다. 도메인 등록 기관의 웹 사이트에서 IP 주소사용자 지정 도메인 확인 ID를 사용하여 도메인을 구성합니다. 일반적인 도메인 구성에는 다음이 포함됩니다.

    • @호스트 및 Azure Portal의 IP 주소 값이 포함된 A 레코드.
    • asuid호스트 및 Azure에서 생성하고 Azure Portal에서 제공하는 확인 ID 값이 포함된 TXT 레코드.

    도메인 등록 기관의 웹 사이트에서 변경 내용을 올바르게 저장해야 합니다. 일부 등록 기관 웹 사이트에는 do기본 레코드를 저장하려면 2단계 프로세스가 필요합니다. 하나 이상의 레코드가 개별적으로 저장되고, 그 다음에는 do기본 등록을 별도의 단추로 업데이트합니다.

  10. Azure Portal에서 사용자 지정 도메인 블레이드로 돌아갑니다. 사용자 지정 도메인 추가를 선택합니다. A 레코드 옵션을 선택합니다. 도메인을 제공하고 유효성 검사를 선택합니다. 도메인 레코드가 올바르고 인터넷에서 전파되는 경우 포털에서 사용자 지정 도메인 추가 단추를 선택할 수 있습니다.

    도메인 등록 변경이 도메인 등록 기관에서 처리된 후 인터넷 DNS(도메인 이름 서버)에서 전파되는 데 며칠이 걸릴 수 있습니다. 도메인 레코드가 영업일 기준 3일 이내에 업데이트되지 않는 경우 도메인 등록 기관을 통해 레코드가 올바르게 설정되었는지 확인하고 고객 지원에 문의하세요.

  11. 사용자 지정 도메인 블레이드에서 도메인의 SSL 상태Not Secure로 표시됩니다. 바인딩 추가 링크를 선택합니다. 사용자 지정 도메인 바인딩을 위해 키 자격 증명 모음에서 사이트 HTTPS 인증서를 선택합니다.

  12. Visual Studio에서 ‘서버’ 프로젝트의 앱 설정 파일(appsettings.json 또는 appsettings.Production.json)을 엽니다. Identity 서버 구성에서 다음 Key 섹션을 추가합니다. Name 키의 자체 서명된 인증서 주체를 지정합니다. 다음 예제에서는 키 자격 증명 모음에 할당된 인증서의 일반 이름이 IdentityServerSigning이며 이 이름에서 CN=IdentityServerSigning주체가 생성됩니다.

    "IdentityServer": {
    
      ...
    
      "Key": {
        "Type": "Store",
        "StoreName": "My",
        "StoreLocation": "CurrentUser",
        "Name": "CN=IdentityServerSigning"
      }
    },
    
  13. Visual Studio에서 ‘서버’ 프로젝트의 Azure App Service 게시 프로필을 만듭니다. 메뉴 모음에서 빌드>게시>새로 만들기>Azure>Azure App Service(Windows 또는 Linux)를 선택합니다. Visual Studio가 Azure 구독에 연결된 경우 리소스 종류별로 Azure 리소스의 를 설정할 수 있습니다. 웹앱 목록 내에서 탐색하여 앱의 App Service를 찾고 선택합니다. 마침을 선택합니다.

  14. Visual Studio가 게시 창으로 돌아가면 키 자격 증명 모음 및 SQL Server 데이터베이스 서비스 종속성이 자동으로 검색됩니다.

    키 자격 증명 모음 서비스에는 기본 설정에 대한 구성 변경이 필요하지 않습니다.

    테스트를 위해 기본적으로 Blazor 템플릿을 통해 구성되는 앱의 로컬 SQLite 데이터베이스는 추가 구성 없이 앱과 함께 배포할 수 있습니다. 프로덕션 환경에서 Identity 서버에 대해 다른 데이터베이스를 구성하는 작업은 이 문서의 범위를 벗어납니다. 자세한 내용은 다음 설명서 세트의 데이터베이스 리소스를 참조하세요.

  15. 창 위쪽에 있는 배포 프로필 이름에서 편집 링크를 선택합니다. 대상 URL을 사이트의 사용자 지정 도메인 URL(예: https://www.contoso.com)로 변경합니다. 설정을 저장합니다.

  16. 앱을 게시합니다. Visual Studio가 브라우저 창을 열고 해당 사용자 지정 도메인에서 사이트를 요청합니다.

Azure 설명서에는 A 레코드 대신 CNAME 레코드를 사용하는 방법에 관한 정보를 포함하여 App Service에서 TLS 바인딩과 함께 Azure 서비스 및 사용자 지정 도메인을 사용하는 방법에 관한 추가적인 세부 정보가 있습니다. 자세한 내용은 다음 리소스를 참조하세요.

Azure Portal에서 앱, 앱 구성 또는 Azure 서비스를 변경한 후 각 앱 테스트 실행에 대해 새 프라이빗 모드 브라우저 창(예: Microsoft Edge InPrivate 모드 또는 Google Chrome Incognito 모드)을 사용하는 것이 좋습니다. 이전 테스트 실행에서 느린 cookie로 인해 사이트의 구성이 올바르더라도 사이트를 테스트할 때 인증 또는 권한 부여에 실패할 수 있습니다. 각 테스트 실행에 대한 새 프라이빗 브라우저 창을 열도록 Visual Studio를 구성하는 방법에 대한 자세한 내용은 s 및 사이트 데이터 섹션을 Cookie참조하세요.

Azure Portal에서 App Service 구성이 변경되면 업데이트는 일반적으로 신속하게 적용되지만 즉시 적용되지는 않습니다. 경우에 따라 구성 변경 내용을 적용하려면 App Service가 다시 시작할 때까지 잠시 기다려야 합니다.

Identity 서버 키 서명 인증서 로드 문제를 해결하는 경우 Azure Portal Kudu PowerShell 명령 셸에서 다음 명령을 실행합니다. 명령은 앱이 CurrentUser>My 인증서 저장소에서 액세스할 수 있는 인증서 목록을 제공합니다. 출력에는 앱을 디버그할 때 유용한 인증서 주체 및 지문이 포함됩니다.

Get-ChildItem -path Cert:\CurrentUser\My -Recurse | Format-List DnsNameList, Subject, Thumbprint, EnhancedKeyUsageList

문제 해결

로깅

인증에 Blazor WebAssembly 디버그 또는 추적 로깅을 사용하도록 설정하려면 문서 버전 선택기가 ASP.NET Core Blazor 7.0 이상으로 설정된 ASP.NET Core 로깅의 클라이언트 쪽 인증 로깅 섹션을 참조하세요.

일반 오류

  • 앱 또는 IP(Identity 공급자)의 잘못된 구성

    가장 일반적인 오류는 잘못된 구성으로 인해 발생합니다. 다음은 몇 가지 예입니다.

    • 시나리오의 요구 사항에 따라, 누락되거나 잘못된 권한, 인스턴스, 테넌트 ID, 테넌트 도메인, 클라이언트 ID 또는 리디렉션 URI 때문에 앱에서 클라이언트를 인증하지 못합니다.
    • 잘못된 요청 범위는 클라이언트가 서버 웹 API 엔드포인트에 액세스하지 못하게 합니다.
    • 서버 API 권한이 잘못되거나 누락되어 클라이언트가 서버 웹 API 엔드포인트에 액세스할 수 없습니다.
    • IP 앱 등록의 리디렉션 URI에 구성된 것과 다른 포트에서 앱을 실행합니다. Microsoft Entra ID 및 개발 테스트 주소에서 localhost 실행되는 앱에는 포트가 필요하지 않지만 앱의 포트 구성 및 앱이 실행되는 포트는 주소localhost 가 아닌 경우 일치해야 합니다.

    이 문서 지침의 구성 섹션에서는 올바른 구성의 예를 보여 줍니다. 앱 및 IP 구성을 찾는 문서의 각 섹션을 주의 깊게 확인하세요.

    구성이 올바르게 표시되면 다음을 수행합니다.

    • 애플리케이션 로그를 분석합니다.

    • 브라우저의 개발자 도구를 사용하여 클라이언트 앱과 IP 또는 서버 앱 간의 네트워크 트래픽을 검사합니다. 종종 정확한 오류 메시지 또는 문제의 원인에 대한 단서가 있는 메시지가 요청을 수행한 후 IP 또는 서버 앱에 의해 클라이언트로 반환됩니다. 개발자 도구 지침은 다음 문서에서 확인할 수 있습니다.

    • JWT(ON Web Token)가 사용되는 릴리스JS의 Blazor 경우 문제가 발생하는 위치에 따라 클라이언트를 인증하거나 서버 웹 API에 액세스하는 데 사용되는 토큰의 콘텐츠를 디코딩합니다. 자세한 내용은 JWT(JSON Web Token)의 콘텐츠 검사를 참조하세요.

    문서 작업 팀은 설명서 피드백 및 문서의 버그에 응답하지만(이 페이지의 피드백 섹션에서 문제 열기) 제품 지원을 제공할 수 없습니다. 앱 문제 해결을 지원하기 위해 몇 가지 퍼블릭 지원 포럼을 사용할 수 있습니다. 다음을 권장합니다.

    이전 포럼은 Microsoft에서 소유하거나 제어하지 않습니다.

    중요하지 않고 보안이나 기밀이 아니며 재현 가능한 프레임워크 버그 보고서의 경우 ASP.NET Core 제품 단위에서 문제를 여세요. 문제의 원인을 철저하게 조사했으며 자신의 노력과 퍼블릭 지원 포럼에서 커뮤니티의 도움을 받아도 해결할 수 없는 경우에만 제품 단위에서 문제를 열고 그전에는 열지 마세요. 제품 단위는 단순한 구성 오류 또는 타사 서비스와 관련된 사용 사례로 인해 손상된 개별 앱의 문제를 해결할 수 없습니다. 보고서가 본질적으로 민감하거나 기밀이거나 공격자가 악용할 수 있는 제품의 잠재적인 보안 결함을 설명하는 경우 보안 문제 및 버그 보고(dotnet/aspnetcoreGitHub 리포지토리)를 참조하세요.

  • ME-ID에 대한 권한 없는 클라이언트

    info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] 권한 부여에 실패했습니다. 이러한 요구 사항이 충족되지 않았습니다. DenyAnonymousAuthorizationRequirement: 인증된 사용자가 필요합니다.

    ME-ID의 로그인 콜백 오류:

    • 오류: unauthorized_client
    • 설명: AADB2C90058: The provided application is not configured to allow public clients.

    오류를 해결하려면:

    1. Azure Portal에서 앱의 매니페스트에 액세스합니다.
    2. allowPublicClient 특성null 또는 true로 설정합니다.

Cookie 및 사이트 데이터

Cookie 및 사이트 데이터가 앱을 업데이트할 때에도 유지되어 테스트 및 문제 해결에 방해가 될 수 있습니다. 앱 코드를 변경하거나 공급자를 사용하여 사용자 계정을 변경하거나 공급자 앱 구성을 변경하는 경우 다음을 지우세요.

  • 사용자 로그인 cookie
  • 앱 cookie
  • 캐시되고 저장된 사이트 데이터

남겨진 cookie 및 사이트 데이터로 인해 테스트 및 문제 해결이 지장을 받지 않도록 하려면 다음을 수행합니다.

  • 브라우저 구성
    • 브라우저에서 브라우저를 닫을 때마다 모든 cookie 및 사이트 데이터를 삭제하도록 구성할 수 있는지 테스트합니다.
    • 앱, 테스트 사용자 또는 공급자 구성을 변경할 때 수동으로 또는 IDE를 통해 브라우저를 닫습니다.
  • 사용자 지정 명령을 사용하여 Visual Studio에서 InPrivate 또는 Incognito 모드로 브라우저를 엽니다.
    • Visual Studio의 실행 단추를 통해 브라우저 선택 대화 상자를 엽니다.
    • 추가 단추를 선택합니다.
    • 프로그램 필드에서 브라우저의 경로를 제공합니다. 다음 실행 파일 경로는 Windows 10에서 일반적인 설치 위치입니다. 브라우저가 다른 위치에 설치되어 있거나 Windows 10을 사용하지 않는 경우 브라우저 실행 파일의 경로를 제공합니다.
      • Microsoft Edge: C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
      • Google Chrome: C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
      • Mozilla Firefox: C:\Program Files\Mozilla Firefox\firefox.exe
    • 인수 필드에서 브라우저가 InPrivate 또는 Incognito 모드에서 여는 데 사용하는 명령줄 옵션을 제공합니다. 일부 브라우저에는 앱의 URL이 필요합니다.
      • Microsoft Edge: 사용 -inprivate.
      • Google Chrome: 자리 표시자가 {URL} 열 URL인 경우(예https://localhost:5001: )를 사용합니다--incognito --new-window {URL}.
      • Mozilla Firefox: 자리 표시자가 {URL} 열 URL인 경우(예https://localhost:5001: )를 사용합니다-private -url {URL}.
    • 이름 필드에 이름을 입력합니다. 예들 들어 Firefox Auth Testing입니다.
    • 확인 단추를 선택합니다.
    • 앱 테스트를 반복할 때마다 브라우저 프로필을 선택할 필요가 없도록 하려면 기본값으로 설정 단추를 사용하여 프로필을 기본값으로 설정합니다.
    • 앱, 테스트 사용자 또는 공급자 구성을 변경할 때 IDE를 통해 브라우저를 닫습니다.

앱 업그레이드

개발 컴퓨터의 .NET Core SDK 또는 앱 내의 패키지 버전을 업그레이드하거나 앱 내 패키지 버전을 변경한 후 즉시 작동 중인 앱에서 오류가 발생할 수 있습니다. 경우에 따라 중요한 업그레이드를 수행할 때 일관되지 않은 패키지로 인해 응용 프로그램이 중단될 수 있습니다. 이러한 대부분의 문제는 다음 지침에 따라 수정할 수 있습니다.

  1. 명령 셸에서 dotnet nuget locals all --clear를 실행하여 로컬 시스템의 NuGet 패키지 캐시를 지웁니다.
  2. 프로젝트의 binobj 폴더를 삭제합니다.
  3. 프로젝트를 복원하고 다시 빌드합니다.
  4. 앱을 다시 배포하기 전에 서버의 배포 폴더에 있는 모든 파일을 삭제합니다.

참고 항목

앱의 대상 프레임워크와 호환되지 않는 패키지 버전의 사용은 지원되지 않습니다. 패키지에 대한 자세한 내용은 NuGet 갤러리 또는 FuGet 패키지 탐색기를 사용하세요.

Server 실행

호스트된 Blazor WebAssembly솔루션을 테스트하고 문제를 해결할 때 Server 프로젝트에서 앱을 실행하고 있는지 확인합니다.

사용자 검사

다음 User 구성 요소는 앱에서 직접 사용하거나 추가 사용자 지정의 기준으로 사용할 수 있습니다.

User.razor:

@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService

<h1>@AuthenticatedUser?.Identity?.Name</h1>

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

<h2>Access token</h2>

<p id="access-token">@AccessToken?.Value</p>

<h2>Access token claims</h2>

@foreach (var claim in GetAccessTokenClaims())
{
    <p>@(claim.Key): @claim.Value.ToString()</p>
}

@if (AccessToken != null)
{
    <h2>Access token expires</h2>

    <p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
    <p id="access-token-expires">@AccessToken.Expires</p>

    <h2>Access token granted scopes (as reported by the API)</h2>

    @foreach (var scope in AccessToken.GrantedScopes)
    {
        <p>Scope: @scope</p>
    }
}

@code {
    [CascadingParameter]
    private Task<AuthenticationState> AuthenticationState { get; set; }

    public ClaimsPrincipal AuthenticatedUser { get; set; }
    public AccessToken AccessToken { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        var state = await AuthenticationState;
        var accessTokenResult = await AuthorizationService.RequestAccessToken();

        if (!accessTokenResult.TryGetToken(out var token))
        {
            throw new InvalidOperationException(
                "Failed to provision the access token.");
        }

        AccessToken = token;

        AuthenticatedUser = state.User;
    }

    protected IDictionary<string, object> GetAccessTokenClaims()
    {
        if (AccessToken == null)
        {
            return new Dictionary<string, object>();
        }

        // header.payload.signature
        var payload = AccessToken.Value.Split(".")[1];
        var base64Payload = payload.Replace('-', '+').Replace('_', '/')
            .PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');

        return JsonSerializer.Deserialize<IDictionary<string, object>>(
            Convert.FromBase64String(base64Payload));
    }
}

JWT(JSON Web Token)의 콘텐츠 검사

JWT(JSON Web Token)를 디코딩하려면 Microsoft의 jwt.ms 도구를 사용합니다. UI의 값은 브라우저에 남겨지지 않습니다.

인코딩된 JWT 예제(표시를 위해 축약됨):

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V_bU55Ug_PW4pLPr20tTS8Ct7_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2_nXA25Q

Azure AAD B2C에 대해 인증하는 앱용 도구에 의해 디코딩된 JWT 예제:

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1610059429,
  "nbf": 1610055829,
  "ver": "1.0",
  "iss": "https://mysiteb2c.b2clogin.com/5cc15ea8-a296-4aa3-97e4-226dcc9ad298/v2.0/",
  "sub": "5ee963fb-24d6-4d72-a1b6-889c6e2c7438",
  "aud": "70bde375-fce3-4b82-984a-b247d823a03f",
  "nonce": "b2641f54-8dc4-42ca-97ea-7f12ff4af871",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]

추가 리소스