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

.NET 8 Blazor Server集成LDAP认证:用户信息为空及Handler失效问题

.NET 8 Blazor Server LDAP认证问题解决方案

核心问题梳理

在集成LDAP认证时遇到两个关键问题:

  • HttpContext.User相关信息(Claims、IsAuthenticated、Email)均为空/False
  • 自定义LdapAuthenticationHandler未生效

一、修复认证服务配置(Program.cs)

取消认证相关注释,并调整中间件顺序与配置:

using Microsoft.AspNetCore.Authentication;
using RCBuisinessLogic.Authentication;
using RCBuisinessLogic.DataAccess;
using RCWebApp.Components;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
builder.Services.AddSingleton<BaseDAL>();
builder.Services.AddSingleton<AuthenticationConfiguration>();
builder.Services.AddTransient<UserInformation>();

// 注册Razor组件服务
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// 配置认证服务
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = "LDAP";
    options.DefaultChallengeScheme = "LDAP";
})
.AddScheme<AuthenticationSchemeOptions, LdapAuthenticationHandler>("LDAP", options => { });

// 配置授权策略
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
});

var app = builder.Build();

// 确保认证、授权中间件顺序正确(必须在路由前)
app.UseAuthentication();
app.UseAuthorization();

// 登录重定向逻辑(修复空引用问题)
app.Use(async (context, next) =>
{
    if (context.User?.Identity?.IsAuthenticated == true && !context.Request.Path.StartsWithSegments("/userlogin"))
    {
        context.Response.Redirect("/dashboard");
        return;
    }
    await next();
});

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

二、修正自定义认证Handler逻辑

原Handler存在表单读取适配问题,调整后兼容Blazor Server交互模式:

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using RCBuisinessLogic.Authentication;
using System.DirectoryServices;
using System.Security.Claims;
using System.Text.Encodings.Web;

public class LdapAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private readonly UserInformation _userInformation;

    public LdapAuthenticationHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        UserInformation userInformation)
        : base(options, logger, encoder, clock)
    {
        _userInformation = userInformation;
    }

    // 此方法主要用于API式登录验证,Blazor交互式组件需手动触发认证
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var authHeader = Request.Headers.Authorization.ToString();
        if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic "))
        {
            return AuthenticateResult.NoResult();
        }

        var credentials = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(authHeader.Substring(6))).Split(':');
        var username = credentials[0];
        var password = credentials[1];

        string domain = "mycorp.com";
        if (AuthenticateWithLdap(username, password, domain, out var userId, out var email))
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, username),
                new Claim(ClaimTypes.NameIdentifier, userId),
                new Claim(ClaimTypes.Email, email)
            };

            var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);
            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
            var ticket = new AuthenticationTicket(claimsPrincipal, Scheme.Name);

            return AuthenticateResult.Success(ticket);
        }

        return AuthenticateResult.Fail("Invalid username or password.");
    }

    private bool AuthenticateWithLdap(string username, string password, string domain, out string userId, out string email)
    {
        userId = null;
        email = null;

        try
        {
            var ldapPath = $"LDAP://{domain}";
            var fullUsername = username.Contains("@") ? username : $"{domain}\\{username}";
            
            using var de = new DirectoryEntry(ldapPath, fullUsername, password);
            de.RefreshCache(); // 触发LDAP绑定验证凭证

            using var ds = new DirectorySearcher(de)
            {
                Filter = $"(sAMAccountName={EscapeLdapSearchFilter(username)})",
                PropertiesToLoad = { "sAMAccountName", "mail" }
            };

            var result = ds.FindOne();
            if (result != null)
            {
                userId = result.Properties["sAMAccountName"][0].ToString();
                email = result.Properties.Contains("mail") ? result.Properties["mail"][0].ToString() : string.Empty;
                return true;
            }
        }
        catch (DirectoryServicesCOMException ex)
        {
            Logger.LogError(ex, "LDAP authentication failed");
        }

        return false;
    }

    private static string EscapeLdapSearchFilter(string searchFilter)
    {
        var escape = new System.Text.StringBuilder();
        foreach (char c in searchFilter)
        {
            switch (c)
            {
                case '\\': escape.Append(@"\5c"); break;
                case '*': escape.Append(@"\2a"); break;
                case '(': escape.Append(@"\28"); break;
                case ')': escape.Append(@"\29"); break;
                case '\0': escape.Append(@"\00"); break;
                case '/': escape.Append(@"\2f"); break;
                default: escape.Append(c); break;
            }
        }
        return escape.ToString();
    }
}

三、修复登录组件逻辑(UserLogin.razor)

原代码仅验证LDAP凭证,未将用户信息写入HttpContext,调整后完成完整认证流程:

@page "/userlogin"
@using System.ComponentModel.DataAnnotations
@using System.Security.Claims
@using Microsoft.AspNetCore.Authentication
@using System.DirectoryServices
@using RCBuisinessLogic
@using RCBuisinessLogic.Authentication
@using RCWebApp.Models
@rendermode InteractiveServer
@inject IHttpContextAccessor HttpContextAccessor
@inject NavigationManager NavigationManager
@inject UserInformation UserInformation

<div class="user-login" style="height: 630px;">
    <EditForm Model="@Login" OnSubmit="HandleLogin" FormName="UserLoginForm">
        <DataAnnotationsValidator />
        <ValidationSummary />

        <div class="form-group">
            <label for="UserName">Username:</label>
            <InputText id="UserName" class="form-control" @bind-Value="Login.UserName" />
            <ValidationMessage For="@(() => Login.UserName)" />
        </div>

        <div class="form-group">
            <label for="Password">Password:</label>
            <InputText id="Password" class="form-control" @bind-Value="Login.Password" Type="password" />
            <ValidationMessage For="@(() => Login.Password)" />
        </div>

        <button type="submit" class="btn btn-primary">Login</button>
        @if (!string.IsNullOrEmpty(ErrorMessage))
        {
            <div class="text-danger mt-2">@ErrorMessage</div>
        }
    </EditForm>
</div>

@code {
    private Login Login { get; set; } = new Login();
    private string ErrorMessage { get; set; } = string.Empty;

    private async Task HandleLogin()
    {
        ErrorMessage = string.Empty;
        string domain = "mycorp.com";
        string ldapPath = $"LDAP://{domain}";

        try
        {
            if (AuthenticateWithLdap(ldapPath, domain, Login.UserName, Login.Password, out var userId, out var email))
            {
                // 创建用户Claims
                var claims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, Login.UserName),
                    new Claim(ClaimTypes.NameIdentifier, userId),
                    new Claim(ClaimTypes.Email, email)
                };

                // 完成登录,将用户信息写入HttpContext
                var claimsIdentity = new ClaimsIdentity(claims, "LDAP");
                var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                await HttpContextAccessor.HttpContext.SignInAsync("LDAP", claimsPrincipal);

                // 跳转至仪表盘
                NavigationManager.NavigateTo("/dashboard");
            }
            else
            {
                ErrorMessage = "Invalid username or password.";
            }
        }
        catch (Exception ex)
        {
            ErrorMessage = $"Login failed: {ex.Message}";
        }
    }

    private bool AuthenticateWithLdap(string ldapPath, string domain, string userName, string password, out string userId, out string email)
    {
        userId = null;
        email = null;

        try
        {
            var fullUsername = userName.Contains("@") ? userName : $"{domain}\\{userName}";
            using var de = new DirectoryEntry(ldapPath, fullUsername, password);
            de.RefreshCache(); // 触发LDAP绑定验证凭证

            using var ds = new DirectorySearcher(de)
            {
                Filter = $"(sAMAccountName={EscapeLdapSearchFilter(userName)})",
                PropertiesToLoad = { "sAMAccountName", "mail" }
            };

            var result = ds.FindOne();
            if (result != null)
            {
                userId = result.Properties["sAMAccountName"][0].ToString();
                email = result.Properties.Contains("mail") ? result.Properties["mail"][0].ToString() : string.Empty;
                _ = UserInformation.GetByName($"{domain}\\{userName}", email);
                return true;
            }
        }
        catch (DirectoryServicesCOMException ex)
        {
            ErrorMessage = ex.ErrorCode == -2147023570 ? "Invalid username or password." : $"LDAP server error: {ex.Message}";
        }

        return false;
    }

    private static string EscapeLdapSearchFilter(string searchFilter)
    {
        var escape = new System.Text.StringBuilder();
        foreach (char c in searchFilter)
        {
            switch (c)
            {
                case '\\': escape.Append(@"\5c"); break;
                case '*': escape.Append(@"\2a"); break;
                case '(': escape.Append(@"\28"); break;
                case ')': escape.Append(@"\29"); break;
                case '\0': escape.Append(@"\00"); break;
                case '/': escape.Append(@"\2f"); break;
                default: escape.Append(c); break;
            }
        }
        return escape.ToString();
    }
}

四、问题根源解析

  1. HttpContext.User为空:
    原登录逻辑仅验证LDAP凭证,未调用SignInAsync将用户Claims写入HttpContext,导致认证状态未持久化。Blazor Server交互式组件需手动完成登录流程。

  2. 自定义Handler未生效:
    原Handler依赖读取Request.Form,但Blazor Server交互式组件的表单提交通过SignalR传递,而非传统HTTP表单,因此无法触发自动认证逻辑。Handler主要用于API端点的后续身份验证,登录流程需在组件内手动实现。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 15:59:51