身份验证与授权

身份验证用于确认“当前用户是谁”,授权用于判断“用户能做什么”。Blazor可以读取ASP.NET Core提供的用户身份,并根据角色或策略控制组件和页面显示,但真正的数据与操作安全仍必须由服务器端授权保证。

提供身份验证状态

Blazor通过AuthenticationStateProvider获取AuthenticationState,其中的User是一个ClaimsPrincipal。组件可以查询用户是否通过身份验证、读取用户名和角色声明。应用首先需要按实际方案配置Cookie、OpenID Connect等ASP.NET Core身份验证,然后启用授权和级联身份验证状态:

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = "Cookies";
    options.DefaultChallengeScheme = "Cookies";
}).AddCookie("Cookies");

builder.Services.AddAuthorization();

// .NET 8新增的服务注册方式,为组件树提供级联身份验证状态。
builder.Services.AddCascadingAuthenticationState();

请求管道中还应按应用需要启用身份验证、授权和防伪中间件:

app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();

在.NET 7及更早版本中,常见做法是在Routes.razor中用<CascadingAuthenticationState>包裹<Router>;在.NET 8 Blazor Web App中,推荐使用AddCascadingAuthenticationState()服务注册,不再额外包裹这个组件。它把身份验证状态作为级联值提供给组件树,使AuthorizeView、AuthorizeRouteView等组件能够获取同一状态。

还需要在Components/_Imports.razor中导入授权组件命名空间,否则AuthenticationStateProvider、AuthorizeView和AuthorizeRouteView等示例无法解析:

@using Microsoft.AspNetCore.Components.Authorization

组件也可以直接注入AuthenticationStateProvider读取当前用户:

@inject AuthenticationStateProvider AuthStateProvider

@code {
    private async Task<string?> GetUserNameAsync()
    {
        var state = await AuthStateProvider.GetAuthenticationStateAsync();
        var user = state.User;
        return user.Identity?.IsAuthenticated == true
            ? user.Identity.Name
            : null;
    }
}

自定义AuthenticationStateProvider

需要自行维护登录状态时,可以继承AuthenticationStateProvider。下面的简化例子演示了返回当前用户以及通知状态变化的方法,但它不是完整的登录实现,真实应用必须先在可信环境中验证凭据并建立安全的登录会话。

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;

public class DemoAuthStateProvider : AuthenticationStateProvider
{
    private ClaimsPrincipal currentUser = new(new ClaimsIdentity());

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        return Task.FromResult(new AuthenticationState(currentUser));
    }

    public void SignIn(string userName)
    {
        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim(ClaimTypes.Name, userName),
                new Claim(ClaimTypes.Role, "Admin")
            },
            authenticationType: "Demo");

        currentUser = new ClaimsPrincipal(identity);
        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(currentUser)));
    }

    public void SignOut()
    {
        currentUser = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(currentUser)));
    }
}

GetAuthenticationStateAsync()返回当前状态;登录或退出后调用NotifyAuthenticationStateChanged(),订阅身份验证状态的组件才会收到更新并重新渲染。自定义提供程序通常按实际宿主模型注册:

builder.Services.AddScoped<AuthenticationStateProvider, DemoAuthStateProvider>();

在交互式Server中,Scoped提供程序通常属于当前电路;在静态SSR中,用户身份仍来自当前HTTP请求。涉及跨静态SSR和交互阶段的认证时,应使用应用统一的身份验证方案,不要把只在某个组件实例中修改的内存字段当成可靠的登录会话。

使用AuthorizeView控制局部界面

AuthorizeView根据身份验证状态、角色或策略选择要显示的内容:

<AuthorizeView Roles="Admin">
    <Authorized>
        <p>欢迎,@context.User.Identity?.Name。</p>
        <button>管理用户</button>
    </Authorized>
    <NotAuthorized>
        <p>你没有管理权限。</p>
    </NotAuthorized>
    <Authorizing>
        <p>正在确认身份……</p>
    </Authorizing>
</AuthorizeView>

Authorized、NotAuthorized和Authorizing分别对应已授权、未授权和身份验证状态尚未确定的阶段。不指定Roles或Policy时,AuthorizeView默认只要求用户已登录;也可以设置Policy="RequireAdmin"使用预先注册的授权策略。

需要注意,AuthorizeView只是控制UI呈现。隐藏按钮或页面内容并不能阻止用户直接调用接口,相关Web API、数据库操作和其他服务端端点仍必须执行授权。

使用[Authorize]和AuthorizeRouteView保护页面

可路由页面可以通过[Authorize]声明授权元数据:

@page "/admin"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "Admin")]

<h1>管理后台</h1>

为了让路由系统根据该元数据选择授权结果,应将Routes.razor中的RouteView替换为AuthorizeRouteView:

<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="routeData"
                            DefaultLayout="typeof(Layout.MainLayout)">
            <NotAuthorized>
                <p>请登录,或确认当前账号是否有权访问此页面。</p>
            </NotAuthorized>
        </AuthorizeRouteView>
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
    <NotFound>
        <p>页面不存在。</p>
    </NotFound>
</Router>

[Authorize]声明页面的访问要求,AuthorizeRouteView负责在路由匹配后应用这些要求并显示未授权内容;两者职责不同。仅把[Authorize]放在普通非路由组件上,不会自动形成完整的安全保护。生产环境通常还应对已登录但权限不足和尚未登录分别给出合适处理,并在服务器端点上再次应用相同的角色或策略规则。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。