Blazor 인증과 권한 관리: ASP.NET Identity 핵심 가이드

인증과 권한 관리
📚 핵심 정리

인증과 권한 관리

ASP.NET Core Identity와 AuthorizeView를 사용해 로그인·권한 제어를 구현합니다.

📌 학습 목표

  • ASP.NET Core Identity의 핵심 클래스(UserManager, SignInManager, RoleManager)의 역할을 이해합니다.
  • Program.cs에서 인증·권한 미들웨어를 올바른 순서로 등록하고 설정할 수 있습니다.
  • CascadingAuthenticationStateAuthorizeView로 인증 상태 기반 UI를 선언적으로 구성할 수 있습니다.
  • [Authorize] 속성으로 페이지 전체를 역할(Role) 및 정책(Policy) 기반으로 보호할 수 있습니다.
  • 클레임(Claim) 기반 사용자 정의 정책을 정의하고 세밀한 접근 제어를 구현할 수 있습니다.

📝 개념 설명

1. Blazor 인증 아키텍처 개요

Blazor 앱의 인증 방식은 호스팅 모델에 따라 달라집니다.

  • Blazor Server: 서버가 SignalR 연결을 유지하며 인증 상태를 관리합니다. ASP.NET Core 미들웨어 파이프라인의 쿠키 기반 인증을 그대로 활용합니다.
  • Blazor WebAssembly: 브라우저에서 실행되므로 JWT 토큰 또는 외부 OpenID Connect 공급자를 통해 인증합니다. 서버 API와 별도 인증 흐름이 필요합니다.
💡 이 글은 Blazor Server 환경에서 ASP.NET Core Identity를 이용한 쿠키 기반 인증을 중심으로 설명합니다.

 

 

🔄 Blazor Server 인증 흐름

 

로그인 요청
SignInManager 검증
인증 쿠키 발급
AuthenticationState 갱신
컴포넌트 조건부 렌더링

2. ASP.NET Core Identity 핵심 클래스

ASP.NET Core Identity는 사용자 인증·인가를 위한 종합 멤버십 프레임워크입니다.

  • UserManager<TUser>: 사용자 CRUD, 비밀번호 해싱·검증, 클레임·역할 할당을 담당합니다.
  • SignInManager<TUser>: 로그인·로그아웃, 2단계 인증, 계정 잠금 처리를 담당합니다.
  • RoleManager<TRole>: 역할(Role) 생성·삭제·조회를 담당합니다.

필요한 NuGet 패키지

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

3. Program.cs — 서비스 등록 및 미들웨어 설정

// ApplicationUser.cs
public class ApplicationUser : IdentityUser
{
    public string? DisplayName { get; set; }
}

// ApplicationDbContext.cs
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }
}

// Program.cs
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 8;
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly",   policy => policy.RequireRole("Admin"));
    options.AddPolicy("PremiumUser", policy => policy.RequireClaim("Subscription", "Premium"));
});

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

var app = builder.Build();
app.UseAuthentication();  // 반드시 UseAuthorization() 앞에 위치
app.UseAuthorization();
app.MapRazorPages();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
⚠️ UseAuthentication()UseAuthorization()보다 반드시 먼저 호출해야 합니다. 순서가 바뀌면 인증 상태가 권한 검사에 반영되지 않습니다.

4. CascadingAuthenticationState — 인증 상태 전파

모든 하위 컴포넌트에 인증 상태를 전달하려면 App.razor에서 전체 라우터를 <CascadingAuthenticationState>로 감싸야 합니다.

<!-- App.razor -->
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                                DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <p>이 페이지에 접근할 권한이 없습니다.</p>
                    <a href="/login">로그인하기</a>
                </NotAuthorized>
                <Authorizing>
                    <p>인증 확인 중...</p>
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
        <NotFound><p>페이지를 찾을 수 없습니다.</p></NotFound>
    </Router>
</CascadingAuthenticationState>

5. AuthorizeView 컴포넌트 — 선언적 UI 제어

<AuthorizeView>는 인증 상태에 따라 다른 UI를 렌더링하는 Blazor 전용 컴포넌트입니다. 세 가지 자식 슬롯(Authorized, NotAuthorized, Authorizing)을 제공합니다.

<!-- 로그인 여부에 따른 기본 분기 -->
<AuthorizeView>
    <Authorized>
        <p>환영합니다, @context.User.Identity?.Name 님!</p>
        <a href="/profile">내 프로필</a>
    </Authorized>
    <NotAuthorized>
        <a href="/login">로그인</a> | <a href="/register">회원가입</a>
    </NotAuthorized>
    <Authorizing>
        <span>인증 상태 확인 중...</span>
    </Authorizing>
</AuthorizeView>

<!-- 역할 기반 분기 -->
<AuthorizeView Roles="Admin">
    <Authorized><a href="/admin">관리자 패널</a></Authorized>
</AuthorizeView>

<!-- 정책 기반 분기 -->
<AuthorizeView Policy="PremiumUser">
    <Authorized>
        <p>프리미엄 전용 콘텐츠입니다.</p>
    </Authorized>
    <NotAuthorized>
        <p>프리미엄 회원 전용입니다. 업그레이드하세요.</p>
    </NotAuthorized>
</AuthorizeView>

6. [Authorize] 속성 — 페이지 전체 보호

@attribute [Authorize]를 사용하면 인증되지 않은 사용자가 해당 페이지에 접근할 때 App.razor<NotAuthorized> 슬롯이 렌더링됩니다.

@* 로그인한 모든 사용자 허용 *@
@page "/dashboard"
@attribute [Authorize]

@* Admin 역할 전용 *@
@page "/admin"
@attribute [Authorize(Roles = "Admin")]

@* 정책 기반 접근 제어 *@
@page "/premium"
@attribute [Authorize(Policy = "PremiumUser")]

7. 클레임(Claim) 기반 권한 제어

클레임은 사용자에 대한 키-값 형태의 속성 정보입니다. 역할보다 세밀한 권한 제어가 필요할 때 사용합니다.

// 사용자에게 클레임 추가
var claims = new List<Claim>
{
    new Claim("Subscription", "Premium"),
    new Claim("Department", "Engineering"),
    new Claim("Level", "Senior")
};
await _userManager.AddClaimsAsync(user, claims);

// Program.cs — 복합 클레임 정책 정의
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("SeniorEngineer", policy =>
    {
        policy.RequireClaim("Department", "Engineering");
        policy.RequireClaim("Level", "Senior");
    });
});

 

 

⚖️ AuthorizeView vs [Authorize] 속성 비교

 

항목AuthorizeView[Authorize] 속성
적용 범위컴포넌트 내 일부 UI 영역페이지·컴포넌트 전체
사용 위치Razor 마크업 내부@attribute 지시문
미인증 시 처리NotAuthorized 슬롯 렌더링App.razor NotAuthorized로 전환
역할 지정Roles=”Admin”[Authorize(Roles=”Admin”)]
정책 지정Policy=”PolicyName”[Authorize(Policy=”PolicyName”)]
사용자 정보 접근@context.User 직접 사용코드에서 별도 주입 필요

💡 예제 & 실습

실습 1: 로그인 컴포넌트 구현

@page "/login"
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject NavigationManager NavigationManager

<h2>로그인</h2>
<EditForm Model="@loginModel" OnValidSubmit="HandleLogin">
    <DataAnnotationsValidator />
    <div>
        <label>이메일</label>
        <InputText @bind-Value="loginModel.Email" />
    </div>
    <div>
        <label>비밀번호</label>
        <InputText type="password" @bind-Value="loginModel.Password" />
    </div>
    <button type="submit">로그인</button>
</EditForm>
@if (!string.IsNullOrEmpty(errorMessage))
    { <p style="color:red">@errorMessage</p> }

@code {
    private LoginModel loginModel = new();
    private string errorMessage = string.Empty;

    private async Task HandleLogin()
    {
        var user = await UserManager.FindByEmailAsync(loginModel.Email);
        if (user == null) { errorMessage = "이메일 또는 비밀번호가 올바르지 않습니다."; return; }

        var result = await SignInManager.PasswordSignInAsync(
            user, loginModel.Password, isPersistent: false, lockoutOnFailure: true);

        if (result.Succeeded)
            NavigationManager.NavigateTo("/", forceLoad: true); // 쿠키 반영을 위해 forceLoad 필수
        else if (result.IsLockedOut)
            errorMessage = "계정이 잠겨 있습니다. 잠시 후 다시 시도하세요.";
        else
            errorMessage = "이메일 또는 비밀번호가 올바르지 않습니다.";
    }

    public class LoginModel
    {
        [Required][EmailAddress] public string Email { get; set; } = string.Empty;
        [Required]               public string Password { get; set; } = string.Empty;
    }
}

실습 2: 회원가입 및 역할 부여

@page "/register"
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<IdentityRole> RoleManager
@inject NavigationManager NavigationManager

@code {
    private List<string> errors = new();

    private async Task HandleRegister()
    {
        // 역할이 없으면 먼저 생성
        if (!await RoleManager.RoleExistsAsync("User"))
            await RoleManager.CreateAsync(new IdentityRole("User"));

        var user = new ApplicationUser { UserName = registerModel.Email, Email = registerModel.Email };
        var result = await UserManager.CreateAsync(user, registerModel.Password);

        if (result.Succeeded)
        {
            await UserManager.AddToRoleAsync(user, "User");  // 기본 역할 부여
            NavigationManager.NavigateTo("/login");
        }
        else
            errors = result.Errors.Select(e => e.Description).ToList();
    }
}

실습 3: @code 블록에서 인증 상태 직접 확인

@inject AuthenticationStateProvider AuthStateProvider

@code {
    private string? currentUserName;
    private bool isAdmin;

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity?.IsAuthenticated == true)
        {
            currentUserName = user.Identity.Name;
            isAdmin         = user.IsInRole("Admin");
            string? sub     = user.FindFirst("Subscription")?.Value;
        }
    }
}

실습 4: 로그아웃 처리

@inject SignInManager<ApplicationUser> SignInManager
@inject NavigationManager NavigationManager

<button @onclick="HandleLogout">로그아웃</button>

@code {
    private async Task HandleLogout()
    {
        await SignInManager.SignOutAsync();
        NavigationManager.NavigateTo("/", forceLoad: true);
    }
}

실습 5: 앱 시작 시 역할 시드(Seed)

역할은 데이터베이스에 레코드로 존재해야 합니다. 앱 시작 시 한 번 생성하는 패턴을 사용합니다.

// Program.cs — app.Run() 이전에 삽입
using (var scope = app.Services.CreateScope())
{
    var roleManager = scope.ServiceProvider
        .GetRequiredService<RoleManager<IdentityRole>>();

    foreach (var role in new[] { "Admin", "User", "Premium" })
    {
        if (!await roleManager.RoleExistsAsync(role))
            await roleManager.CreateAsync(new IdentityRole(role));
    }
}

app.Run();

⚠️ 자주 틀리는 것 / 주의사항

① forceLoad 없이 페이지 이동

문제: 로그인 후 메인 페이지로 이동했는데 여전히 비로그인 상태로 보입니다.
원인: Blazor Server는 SignalR 위에서 동작하므로 쿠키 기반 인증은 HTTP 요청 시점에 처리됩니다. NavigateTo(path)만으로는 인증 쿠키가 갱신되지 않습니다.
해결: NavigationManager.NavigateTo("/", forceLoad: true)로 전체 페이지를 새로 로드합니다. 로그아웃 시에도 동일하게 적용합니다.

② UseAuthentication · UseAuthorization 순서 오류

문제: 로그인 상태인데 [Authorize] 페이지에서 권한 없음 오류가 발생합니다.
해결: Program.cs에서 미들웨어 순서를 반드시 지킵니다.
app.UseAuthentication(); → 사용자가 누구인지 먼저 확인
app.UseAuthorization(); → 그 다음 무엇을 할 수 있는지 결정

③ CascadingAuthenticationState 또는 AuthorizeRouteView 누락

문제: <AuthorizeView>가 항상 <NotAuthorized> 슬롯을 렌더링합니다.
원인: App.razor에서 <CascadingAuthenticationState>로 감싸지 않았거나, <RouteView><AuthorizeRouteView>로 교체하지 않았습니다.
해결: App.razor 구조를 이 글의 실습 예시와 동일하게 수정합니다.

④ 클라이언트 측 권한 체크만으로 보안 완성 착각

주의: AuthorizeView[Authorize]UI 렌더링만 제어합니다. Blazor WebAssembly에서는 클라이언트 코드를 조작할 수 있으므로, 민감한 데이터를 다루는 서버 API 엔드포인트에서도 반드시 독립적인 권한 검사를 수행해야 합니다.

⑤ 역할(Role)을 DB에 먼저 생성하지 않음

문제: [Authorize(Roles = "Admin")]을 설정했지만 관리자도 접근이 차단됩니다.
원인: Identity 역할은 데이터베이스 레코드로 존재해야 합니다. 역할 생성 없이 사용자에게 부여할 수 없습니다.
해결: 실습 5의 시드 코드를 참고해 앱 시작 시 기본 역할을 생성합니다.

🎯 마무리

ASP.NET Core Identity와 Blazor의 AuthorizeView·[Authorize]를 조합하면, 로그인부터 역할·클레임 기반의 세밀한 접근 제어까지 선언적이고 안전하게 구현할 수 있습니다. 미들웨어 등록 순서와 forceLoad, 서버 측 검증의 중요성을 반드시 기억하십시오.

✅ 핵심 정리
  • ASP.NET Core Identity: UserManager(사용자 관리), SignInManager(로그인 처리), RoleManager(역할 관리) 세 클래스가 핵심입니다.
  • 미들웨어 순서: UseAuthentication()UseAuthorization() 순서를 반드시 지킵니다.
  • CascadingAuthenticationState: App.razor 최상위에 배치해야 하위 컴포넌트 전체에 인증 상태가 전파됩니다.
  • AuthorizeView: UI 일부를 인증 상태별로 분기합니다. @context.User로 사용자 정보에 직접 접근 가능합니다.
  • [Authorize] 속성: 페이지 전체를 보호하며 Roles·Policy 파라미터로 대상을 세분화합니다.
  • forceLoad: true: 로그인·로그아웃 후 페이지 이동 시 반드시 사용해 인증 쿠키를 즉시 반영합니다.
  • 역할 시드 필수: 역할은 DB에 먼저 존재해야 합니다. 앱 시작 시 RoleManager.CreateAsync()로 기본 역할을 생성합니다.
  • 서버 측 검증 필수: 클라이언트 UI 제어만으로는 보안이 완전하지 않으므로 API 엔드포인트에서도 인증·인가를 수행합니다.

댓글 남기기

Wordpress Social Share Plugin powered by Ultimatelysocial
Copy link
URL has been copied successfully!
THREADS
RSS
error: 저작권 콘텐츠보호를 부탁드립니다.