Blazor의 인증 및 권한 부여: 실용 가이드
ASP.NET MVC 또는 Razor Pages를 사용하여 작업한 경우 인증 작동 방식에 대한 정신적 모델이 있을 것입니다. 미들웨어가 요청을 가로채고, 쿠키 또는 토큰을 확인하고, HttpContext.User를 채우면 경쟁이 시작됩니다. Blazor는 미묘하지만 중요한 방식으로 정신 모델을 변경합니다. 이러한 차이점을 조기에 이해하지 못하면 불가능하다고 느껴지는 인증 문제를 디버깅하게 됩니다.
이 게시물에서는 서버 및 WebAssembly 호스팅 모델을 모두 다루면서 Blazor에서 인증 및 권한 부여가 실제로 어떻게 작동하는지 살펴보고 싶습니다. 우리는 사용자 지정 공급자, 외부 OAuth 및 숙련된 .NET 개발자라도 겪었던 함정을 통해 기본부터 끝까지 진행할 것입니다.
Blazor의 인증이 다른 이유
기존 ASP.NET에서는 모든 사용자 상호 작용이 HTTP 요청입니다. 서버는 자격 증명을 확인하고 쿠키를 설정하며 모든 후속 요청에는 해당 쿠키가 전달됩니다. 인증 파이프라인은 선형적이고 예측 가능합니다.
Blazor Server는 지속적인 SignalR 연결을 통해 작동합니다. 초기 HTTP 요청이 페이지를 로드한 후 모든 후속 상호 작용은 WebSocket을 통해 발생합니다. 각 버튼 클릭에 대한 새로운 HTTP 요청이 없으므로 미들웨어는 모든 상호 작용에서 다시 실행되지 않습니다. HttpContext은 초기 연결 중에 사용할 수 있지만 회로 수명 내내 이에 의존하면 버그가 발생할 수 있습니다.
Blazor WebAssembly는 전적으로 브라우저에서 실행됩니다. 서버 측 HttpContext가 전혀 없습니다. 인증 상태는 API에서 가져와서 클라이언트측에 저장하고 토큰(일반적으로 JWT)을 통해 관리해야 합니다. 서버는 토큰 유효성 검사가 진행되는 동안에만 클라이언트를 신뢰합니다.
이는 Blazor가 호스팅 모델에 관계없이 작동하는 인증 상태에 대한 자체 추상화가 필요함을 의미합니다. 그 추상화는 AuthenticationStateProvider입니다.
인증 상태: 재단
Blazor 인증 시스템의 핵심은 AuthenticationStateProvider입니다. 이는 단일 중요한 메서드를 노출하는 추상 클래스입니다.
public abstract Task<AuthenticationState> GetAuthenticationStateAsync();
AuthenticationState 개체는 .NET 전체에서 사용되는 것과 동일한 ID 모델인 ClaimsPrincipal를 래핑합니다. 구성 요소는 쿠키나 토큰과 직접 대화하지 않습니다. 그들은 현재 상태에 대해 AuthenticationStateProvider에게 묻습니다.
전체 구성 요소 트리에서 이 상태를 사용할 수 있도록 Blazor는 CascadingAuthenticationState를 제공합니다. 일반적으로 라우터를 App.razor 또는 레이아웃으로 래핑합니다.
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<p>You're not authorized to view this page.</p>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Page not found.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
AuthorizeRouteView는 여기서 이중 임무를 수행합니다. 즉, 일치하는 페이지 구성 요소를 렌더링하기 전에 사용자가 인증 및 승인되었는지 확인하고 그렇지 않은 경우 대체 UI를 제공합니다.
통합 Blazor 모델이 포함된 .NET 8 이상에서는 App.razor에서 이를 구성하고 서비스 등록에서 AddCascadingAuthenticationState()를 사용할 때 프레임워크가 계단식 매개 변수를 자동으로 처리합니다.
ASP.NET ID 통합
대부분의 프로젝트에서는 처음부터 인증을 구축할 필요가 없습니다. ASP.NET ID는 사용자 관리, 암호 해싱, 2단계 인증 및 계정 확인 기능을 즉시 제공합니다.Blazor를 사용한 설정은 Program.cs에서 시작됩니다.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});
.NET 8+의 Blazor 웹앱 템플릿을 사용하면 스캐폴드된 ID UI가 Razor 구성 요소를 직접 사용합니다. 나머지 Blazor 애플리케이션과 자연스럽게 통합되는 로그인, 등록 및 계정 관리 페이지가 제공됩니다. 더 이상 Razor Pages와 Blazor 구성 요소를 어색하게 혼합할 필요가 없습니다.
ApplicationDbContext는 IdentityDbContext에서 상속되며 ID 테이블을 생성하려면 마이그레이션을 실행해야 합니다.
public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
}
dotnet ef migrations add InitialIdentity
dotnet ef database update
AuthorizeView 구성 요소
인증 상태가 구성 요소 트리를 통해 흐르면 AuthorizeView를 사용하면 조건부로 UI를 렌더링할 수 있습니다.
<AuthorizeView>
<Authorized>
<p>Welcome, @context.User.Identity?.Name!</p>
<a href="/account/manage">Manage Account</a>
<form method="post" action="/account/logout">
<button type="submit">Log Out</button>
</form>
</Authorized>
<NotAuthorized>
<a href="/account/login">Log In</a>
<a href="/account/register">Register</a>
</NotAuthorized>
</AuthorizeView>
<Authorized> 내부의 context 매개변수는 AuthenticationState에 대한 액세스를 제공하므로 마크업에서 직접 클레임, 역할 및 사용자 ID를 검사할 수 있습니다.
역할 및 정책과 함께 AuthorizeView를 사용할 수도 있습니다.
<AuthorizeView Roles="Admin,Moderator">
<Authorized>
<button @onclick="DeletePost">Delete Post</button>
</Authorized>
</AuthorizeView>
<AuthorizeView Policy="CanEditArticles">
<Authorized>
<button @onclick="EditArticle">Edit</button>
</Authorized>
</AuthorizeView>
명심해야 할 한 가지: AuthorizeView는 UI 문제입니다. 요소를 숨기거나 표시하지만 기본 논리를 보호하지는 않습니다. 누군가가 API 엔드포인트를 호출하거나 메소드를 직접 호출할 수 있으면 AuthorizeView를 완전히 우회합니다. 항상 서버 측에서도 인증을 시행하십시오.
[Authorize] 속성
전체 페이지를 보호하려면 [Authorize] 속성을 적용하세요.
@page "/admin/dashboard"
@attribute [Authorize(Roles = "Admin")]
<h1>Admin Dashboard</h1>
<p>Only administrators can see this page.</p>
인증되지 않은 사용자가 이 페이지를 탐색하면 AuthorizeRouteView가 시작되어 이전에 정의한 <NotAuthorized> 템플릿을 렌더링합니다. 대신 탐색을 통해 NotAuthorized 사례를 처리하여 로그인 페이지로 리디렉션할 수 있습니다.
<NotAuthorized>
@if (!context.User.Identity?.IsAuthenticated ?? true)
{
<RedirectToLogin />
}
else
{
<p>You don't have permission to access this page.</p>
}
</NotAuthorized>
간단한 RedirectToLogin 구성 요소는 다음과 같습니다.
@inject NavigationManager Navigation
@code {
protected override void OnInitialized()
{
var returnUrl = Uri.EscapeDataString(Navigation.Uri);
Navigation.NavigateTo($"/account/login?returnUrl={returnUrl}", forceLoad: true);
}
}
여기서는 forceLoad: true가 중요합니다. 서버 측 인증 미들웨어가 로그인 흐름을 적절하게 처리할 수 있도록 실제 HTTP 탐색이 필요합니다.
역할 기반 및 정책 기반 인증
역할은 가장 간단한 모델입니다. 사용자를 “관리자” 또는 “편집자"와 같은 그룹에 할당한 다음 멤버십을 확인하세요. 그러나 정책은 훨씬 더 많은 유연성을 제공합니다.
Program.cs에 정책을 등록합니다.
builder.Services.AddAuthorizationCore(options =>
{
options.AddPolicy("CanPublish", policy =>
policy.RequireClaim("Permission", "Publish"));
options.AddPolicy("MinimumAge", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
options.AddPolicy("PremiumUser", policy =>
policy.RequireRole("Premium")
.RequireClaim("Subscription", "Active"));
});
사용자 정의 요구 사항에는 처리기가 필요합니다.
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
}
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst("DateOfBirth");
if (dateOfBirthClaim is null)
return Task.CompletedTask;
var dateOfBirth = DateOnly.Parse(dateOfBirthClaim.Value);
var age = DateOnly.FromDateTime(DateTime.Today).Year - dateOfBirth.Year;
if (age >= requirement.MinimumAge)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
핸들러를 등록합니다.
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
구성 요소에서 동적 논리가 필요할 때 프로그래밍 방식으로 인증을 확인할 수도 있습니다.
@inject IAuthorizationService AuthorizationService
@inject AuthenticationStateProvider AuthStateProvider
@code {
private bool canPublish;
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var result = await AuthorizationService.AuthorizeAsync(
authState.User, "CanPublish");
canPublish = result.Succeeded;
}
}
외부 OAuth 제공자
ASP.NET 인증 미들웨어를 사용하면 “Google로 로그인” 또는 “GitHub으로 로그인” 지원이 간단해집니다. OAuth 흐름에는 HTTP 리디렉션이 필요하므로 서버 측에서 구성됩니다.
builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["Auth:Google:ClientId"]!;
options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]!;
options.Scope.Add("profile");
})
.AddMicrosoftAccount(options =>
{
options.ClientId = builder.Configuration["Auth:Microsoft:ClientId"]!;
options.ClientSecret = builder.Configuration["Auth:Microsoft:ClientSecret"]!;
})
.AddGitHub(options =>
{
options.ClientId = builder.Configuration["Auth:GitHub:ClientId"]!;
options.ClientSecret = builder.Configuration["Auth:GitHub:ClientSecret"]!;
});
GitHub의 경우 기본 ASP.NET 라이브러리에 포함되어 있지 않으므로 AspNet.Security.OAuth.GitHub NuGet 패키지가 필요합니다.
그런 다음 로그인 UI는 외부 인증 확인을 트리거하는 링크를 제공합니다.
@page "/account/external-login"
<h2>Sign in with an external provider</h2>
<form method="post" action="/api/auth/external-login">
<button type="submit" name="provider" value="Google">
Sign in with Google
</button>
<button type="submit" name="provider" value="Microsoft">
Sign in with Microsoft
</button>
<button type="submit" name="provider" value="GitHub">
Sign in with GitHub
</button>
</form>
API 엔드포인트는 챌린지를 트리거하고 콜백을 처리합니다.
app.MapPost("/api/auth/external-login", (string provider, HttpContext context) =>
{
var properties = new AuthenticationProperties
{
RedirectUri = "/api/auth/external-callback"
};
return Results.Challenge(properties, [provider]);
});
Blazor의 외부 인증에는 항상 전체 페이지 탐색이 필요합니다. 서버를 통하지 않고는 SignalR 회로 또는 WebAssembly 앱 내에서 OAuth 리디렉션을 완료할 수 없습니다.
Blazor WebAssembly의 토큰 기반 인증Blazor WebAssembly는 클라이언트에서 실행되므로 쿠키 기반 인증은 동일한 방식으로 적용되지 않습니다. 대신 일반적으로 메모리에 저장되어 나가는 HTTP 요청에 연결된 JWT를 사용합니다.
프레임워크는 토큰을 자동으로 연결하기 위해 AuthorizationMessageHandler를 제공합니다.
builder.Services.AddHttpClient("API",
client => client.BaseAddress = new Uri("https://api.example.com"))
.AddHttpMessageHandler<AuthorizationMessageHandler>();
builder.Services.AddScoped(sp =>
sp.GetRequiredService<IHttpClientFactory>().CreateClient("API"));
자체 API에 대해 인증하는 독립 실행형 Blazor WASM 앱의 경우 JWT를 구문 분석하는 사용자 지정 AuthenticationStateProvider을 구현합니다.
public class JwtAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly ILocalStorageService _localStorage;
private readonly HttpClient _httpClient;
public JwtAuthenticationStateProvider(
ILocalStorageService localStorage,
HttpClient httpClient)
{
_localStorage = localStorage;
_httpClient = httpClient;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await _localStorage.GetItemAsync<string>("authToken");
if (string.IsNullOrWhiteSpace(token))
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var claims = ParseClaimsFromJwt(token);
var identity = new ClaimsIdentity(claims, "jwt");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public void NotifyAuthStateChanged()
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var payload = jwt.Split('.')[1];
var padded = payload.Length % 4 switch
{
2 => payload + "==",
3 => payload + "=",
_ => payload
};
var bytes = Convert.FromBase64String(padded);
var json = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(bytes);
return json?.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()))
?? Enumerable.Empty<Claim>();
}
}
로그인에 성공하면 토큰을 저장하고 인증 상태를 알립니다.
public class AuthService
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
private readonly JwtAuthenticationStateProvider _authStateProvider;
public AuthService(
HttpClient httpClient,
ILocalStorageService localStorage,
AuthenticationStateProvider authStateProvider)
{
_httpClient = httpClient;
_localStorage = localStorage;
_authStateProvider = (JwtAuthenticationStateProvider)authStateProvider;
}
public async Task<bool> LoginAsync(string email, string password)
{
var response = await _httpClient.PostAsJsonAsync("/api/auth/login",
new { Email = email, Password = password });
if (!response.IsSuccessStatusCode)
return false;
var result = await response.Content
.ReadFromJsonAsync<LoginResponse>();
await _localStorage.SetItemAsync("authToken", result!.Token);
_authStateProvider.NotifyAuthStateChanged();
return true;
}
public async Task LogoutAsync()
{
await _localStorage.RemoveItemAsync("authToken");
_authStateProvider.NotifyAuthStateChanged();
}
}
주의 사항: JWT를 localStorage에 저장하면 XSS 공격에 노출됩니다. 보안 수준이 높은 애플리케이션의 경우 토큰을 메모리에만 유지하고 새로 고침 토큰을 사용하거나 서버가 토큰을 관리하고 클라이언트가 HTTP 전용 쿠키를 사용하는 BFF(Backend-for-Frontend) 패턴을 채택하는 것이 좋습니다.
Blazor 서버와 WebAssembly: 보안 고려 사항
호스팅 모델은 보안 상태를 근본적으로 변화시킵니다.
Blazor Server는 모든 구성 요소 논리를 서버에 유지합니다. 클라이언트는 SignalR을 통해 렌더링된 HTML diff만 볼 수 있습니다. 이는 다음을 의미합니다.
- 민감한 로직은 절대 서버를 떠나지 않습니다.
- 구성 요소에서 직접 데이터베이스 및 내부 서비스에 액세스할 수 있습니다.
- 인증 상태는 초기 연결 시 서버의
HttpContext에서 가져옵니다. - 회로는 인증 쿠키보다 오래 지속될 수 있습니다. 사용자의 쿠키가 만료되면 회로는 연결이 끊어질 때까지 활성 상태로 유지됩니다.
- 회로 연결 해제를 적절하게 처리하고 다시 연결 시 인증 상태를 다시 검증해야 합니다.
Blazor WebAssembly는 전적으로 브라우저에서 실행됩니다. 이는 다음을 의미합니다.
- 모든 구성 요소 코드를 다운로드하고 검사할 수 있습니다.
- WASM 구성 요소에 비밀, 연결 문자열 또는 민감한 비즈니스 논리를 넣지 마십시오.
- 인증은 UX용 클라이언트에만 적용됩니다. 실제 시행은 API 계층에서 이루어져야 합니다.
- 토큰 관리는 귀하의 책임입니다
- 서버 프로젝트가 인증을 처리하고 WASM 앱을 제공하는 호스팅 모델 사용을 고려하세요.
WebAssembly 앱에 대해 제가 권장하는 패턴은 모든 구성 요소를 “신뢰할 수 없는 UI"인 것처럼 처리하고 모든 API 엔드포인트를 알 수 없는 클라이언트에 의해 호출되는 것처럼 처리하는 것입니다. 클라이언트가 확인하는 내용에 관계없이 서버 측의 모든 것을 검증합니다.
사용자 정의 AuthenticationStateProvider 구축
때로는 기본 제공 공급자가 아키텍처에 맞지 않는 경우가 있습니다. 레거시 인증 시스템과 통합하거나 인증 상태 변경을 폴링해야 할 수도 있습니다. Blazor Server에 대한 보다 완전한 사용자 지정 공급자는 다음과 같습니다.
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IUserService _userService;
public CustomAuthStateProvider(
IHttpContextAccessor httpContextAccessor,
IUserService userService)
{
_httpContextAccessor = httpContextAccessor;
_userService = userService;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext?.User?.Identity?.IsAuthenticated != true)
return new AuthenticationState(
new ClaimsPrincipal(new ClaimsIdentity()));
var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId is null)
return new AuthenticationState(
new ClaimsPrincipal(new ClaimsIdentity()));
var user = await _userService.GetUserWithClaimsAsync(userId);
if (user is null)
return new AuthenticationState(
new ClaimsPrincipal(new ClaimsIdentity()));
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Name, user.DisplayName),
new(ClaimTypes.Email, user.Email)
};
claims.AddRange(user.Roles.Select(r => new Claim(ClaimTypes.Role, r)));
claims.AddRange(user.Permissions.Select(p => new Claim("Permission", p)));
var identity = new ClaimsIdentity(claims, "Custom");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public void MarkUserAsAuthenticated(string userId)
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public void MarkUserAsLoggedOut()
{
var anonymous = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymous));
NotifyAuthenticationStateChanged(authState);
}
}
Program.cs에 등록하세요.
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddScoped<CustomAuthStateProvider>();
여기서 핵심 통찰력은 NotifyAuthenticationStateChanged입니다. 이를 호출하면 계단식 매개변수가 업데이트되어 구성 요소 트리의 모든 AuthorizeView 및 AuthorizeRouteView가 다시 평가됩니다. 이는 전체 페이지 새로 고침 없이 UI가 로그인/로그아웃 이벤트에 반응하도록 만드는 방법입니다.
일반적인 함정 및 솔루션
여러 프로젝트에서 Blazor 인증을 사용한 후 가장 자주 보게 되는 문제는 다음과 같습니다.
1. Blazor 서버 구성 요소에서 HttpContext 사용HttpContext는 초기 HTTP 요청 중에 사용할 수 있지만 SignalR 상호 작용 중에는 null이거나 오래되었습니다. 초기 렌더링 후에 실행되는 구성 요소에 IHttpContextAccessor를 삽입하지 마세요.
해결책: 초기화 중에 HttpContext에서 필요한 것을 캡처하고 범위가 지정된 서비스에 저장합니다.
public class UserContext
{
public string? UserId { get; set; }
public string? AccessToken { get; set; }
}
// In a component that renders during the initial HTTP request:
@inject IHttpContextAccessor HttpContextAccessor
@inject UserContext UserContext
@code {
protected override void OnInitialized()
{
var context = HttpContextAccessor.HttpContext;
UserContext.UserId = context?.User.FindFirstValue(ClaimTypes.NameIdentifier);
UserContext.AccessToken = context?.Request.Headers.Authorization
.ToString().Replace("Bearer ", "");
}
}
2. 로그인 후 인증 상태가 업데이트되지 않습니다.
로그인 API를 호출하면 성공하지만 UI에는 여전히 “로그인"이 표시됩니다.
해결책: 인증 상태가 변경된 후 AuthenticationStateProvider에서 NotifyAuthenticationStateChanged를 호출해야 합니다. 프레임워크는 토큰이 저장되었거나 쿠키가 설정되었음을 마법처럼 감지하지 않습니다.
3. 구성 요소에서 작동하지 않는 속성 승인
구성 요소에 [Authorize]를 추가했지만 인증되지 않은 사용자를 차단하지는 않습니다.
해결책: App.razor에서 일반 RouteView 대신 AuthorizeRouteView를 사용하고 있는지 확인하세요. 표준 RouteView는 인증 속성을 완전히 무시합니다.
4. 사전 렌더링으로 인해 인증 상태가 중단됩니다.
Blazor WebAssembly에서 서버 측 사전 렌더링 중에는 사용 가능한 인증 토큰이 없습니다. 구성 요소는 인증되지 않은 것으로 렌더링된 다음 WASM이 로드된 후 인증된 상태로 깜박입니다.
해결책: @rendermode InteractiveWebAssembly를 사용하여 인증에 민감한 페이지에 대한 사전 렌더링을 비활성화하거나(사전 렌더링 없이) 로드 상태를 적절하게 처리합니다.
<AuthorizeView>
<Authorized>
<p>Welcome back, @context.User.Identity?.Name</p>
</Authorized>
<Authorizing>
<p>Loading...</p>
</Authorizing>
<NotAuthorized>
<a href="/login">Sign in</a>
</NotAuthorized>
</AuthorizeView>
5. 장기 실행 회로에서의 토큰 만료
Blazor Server 회로는 몇 시간 동안 활성 상태를 유지할 수 있습니다. 토큰 또는 세션이 만료되면 사용자는 UI에서 “인증된” 상태를 유지하지만 API 호출은 실패하기 시작합니다.
해결책: 정기적인 점검을 구현하거나 RevalidatingServerAuthenticationStateProvider를 사용하십시오.
public class RevalidatingAuthStateProvider
: RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceScopeFactory _scopeFactory;
public RevalidatingAuthStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory)
: base(loggerFactory)
{
_scopeFactory = scopeFactory;
}
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
await using var scope = _scopeFactory.CreateAsyncScope();
var userManager = scope.ServiceProvider
.GetRequiredService<UserManager<IdentityUser>>();
var user = await userManager.GetUserAsync(authenticationState.User);
return user is not null;
}
}
이는 30분마다 사용자가 여전히 존재하는지(그리고 보안 스탬프가 변경되지 않았는지) 확인합니다.
결론
Blazor의 인증 및 권한 부여에는 기존 요청-응답 ASP.NET에서 생각의 전환이 필요합니다. AuthenticationStateProvider 추상화는 모든 것이 어떻게 조화를 이루는지 이해하는 열쇠입니다. 일단 이를 내면화하면 나머지는 자연스럽게 따라옵니다.
대부분의 애플리케이션에서는 ASP.NET Identity 및 기본 제공 템플릿으로 시작합니다. 이들은 사용자 관리, 비밀번호 해싱 및 토큰 생성의 무거운 작업을 처리합니다. 요구 사항이 증가함에 따라 정책 및 클레임 기반 인증을 계층화합니다. 사용자가 원할 때 외부 OAuth 공급자를 추가하세요.
호스팅 모델이 중요합니다. Blazor Server는 코드가 서버에 유지되는 보다 전통적인 보안 상태를 제공하는 반면, WebAssembly는 설계상 클라이언트를 신뢰할 수 없는 API 우선 사고를 지향하도록 유도합니다. 둘 다 본질적으로 더 안전하지는 않습니다. 단지 위협 모델이 다를 뿐입니다.
어떤 접근 방식을 선택하든 황금률을 기억하세요. UI에서의 인증은 사용자 경험을 위한 것이고, 서버의 인증은 보안을 위한 것입니다. 항상 두 가지 모두를 적용하세요.