You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor Server Teams应用:如何结合AuthorizeRouteView实现Teams身份验证?

实现Blazor Server + Teams身份验证与AuthorizeRouteView集成

核心思路

要将Teams身份验证与Blazor的AuthorizeRouteView绑定,需要自定义AuthenticationStateProvider,让Blazor的授权系统识别Teams的用户身份;同时要验证应用是否在Teams环境内运行,拒绝外部访问。


1. 自定义TeamsAuthenticationStateProvider

这个类负责向Blazor提供当前用户的认证状态,整合Teams的teamsUserCredential逻辑:

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Identity.Client;
using Microsoft.TeamsFx;
using Microsoft.Graph;
using System.Security.Claims;

public class TeamsAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly TeamsUserCredential _teamsUserCredential;
    private readonly string[] _scopes;
    private ClaimsPrincipal _cachedUser;

    public TeamsAuthenticationStateProvider(TeamsUserCredential teamsUserCredential, string[] scopes)
    {
        _teamsUserCredential = teamsUserCredential;
        _scopes = scopes;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        // 优先返回缓存的用户身份
        if (_cachedUser != null)
        {
            return new AuthenticationState(_cachedUser);
        }

        try
        {
            // 尝试获取现有令牌,验证登录状态
            await _teamsUserCredential.GetTokenAsync(_scopes);
            
            // 获取用户信息(复用你现有逻辑)
            var graph = GetGraphServiceClient(_teamsUserCredential);
            var profile = await graph.Me.GetAsync();
            
            // 构建ClaimsPrincipal供Blazor授权系统使用
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, profile.DisplayName),
                new Claim(ClaimTypes.Email, profile.Mail ?? profile.UserPrincipalName),
                new Claim(ClaimTypes.NameIdentifier, profile.Id)
            };
            
            _cachedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "TeamsAuth"));
            return new AuthenticationState(_cachedUser);
        }
        catch (MsalUiRequiredException)
        {
            // 未登录,返回未认证状态
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
        }
        catch (Exception)
        {
            // 其他错误,返回未认证状态
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
        }
    }

    // 触发Teams登录并更新认证状态
    public async Task LoginAsync()
    {
        await _teamsUserCredential.LoginAsync(_scopes);
        // 通知Blazor重新检测认证状态
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    // 复用你现有Graph客户端构建方法
    private GraphServiceClient GetGraphServiceClient(TokenCredential tokenCredential)
    {
        return new GraphServiceClient(tokenCredential, _scopes);
    }
}

2. 注册服务到DI容器

在Program.cs中注册自定义认证提供者和Teams相关服务:

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.TeamsFx;

var builder = WebApplication.CreateBuilder(args);

// 注册Teams SDK服务
builder.Services.AddTeamsFx(builder.Configuration);

// 注册自定义认证提供者
builder.Services.AddScoped<AuthenticationStateProvider, TeamsAuthenticationStateProvider>(sp =>
{
    var teamsCredential = sp.GetRequiredService<TeamsUserCredential>();
    var scopes = new[] { "User.Read" }; // 替换为你的实际权限范围
    return new TeamsAuthenticationStateProvider(teamsCredential, scopes);
});

// 添加Blazor授权核心服务
builder.Services.AddAuthorizationCore();

// 常规Blazor服务注册
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

var app = builder.Build();

// 中间件配置
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

3. 修改App.razor使用AuthorizeRouteView

替换默认的RouteView为AuthorizeRouteView,配置未授权/未认证时的提示:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <div class="alert alert-danger mt-4">
                        你没有权限访问此页面。
                    </div>
                </NotAuthorized>
                <Authorizing>
                    <div class="text-center mt-5">
                        正在验证身份...
                    </div>
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p class="mt-4">抱歉,未找到该页面。</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

4. 限制仅Teams内访问

在MainLayout.razor中添加Teams环境检测逻辑,拒绝外部访问:

@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@implements IAsyncDisposable

@code {
    private IJSObjectReference _teamsSdk;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            // 加载Teams JS SDK并初始化
            _teamsSdk = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "https://res.cdn.office.net/teams-js/2.16.0/js/MicrosoftTeams.min.js");
            await _teamsSdk.InvokeVoidAsync("initialize");
            
            // 尝试获取Teams上下文,验证是否在Teams内
            await _teamsSdk.InvokeAsync<object>("getContext");
        }
        catch (Exception)
        {
            // 无法加载SDK或获取上下文,跳转至提示页
            NavigationManager.NavigateTo("/not-in-teams");
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (_teamsSdk != null)
        {
            await _teamsSdk.DisposeAsync();
        }
    }
}

创建NotInTeams.razor提示页:

@page "/not-in-teams"
<h3 class="mt-4">请在Microsoft Teams内打开此应用</h3>
<p>本应用仅支持在Microsoft Teams客户端或Teams网页版中访问。</p>

5. 在页面中触发登录

如果用户未认证,可在页面或布局中添加登录触发逻辑:

@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager

@code {
    private bool _isAuthenticated;

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

        if (!_isAuthenticated)
        {
            // 触发Teams登录弹窗
            await ((TeamsAuthenticationStateProvider)AuthStateProvider).LoginAsync();
            // 登录后跳转回原页面
            NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
        }
    }
}

内容的提问来源于stack exchange,提问作者Invop

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 20:53:10