You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

为已通过Cookie认证的Active Directory用户生成JWT令牌,实现.NET Framework与.NET 6 SignalR应用的免重复认证

解决方案:让已认证的应用A用户免登访问应用B的SignalR服务

首先,先帮你分析下之前两个方案失败的核心原因:

  • Cookie认证方案:.NET Framework和.NET 6的Cookie认证底层依赖的DataProtection机制默认不兼容,而且SignalR是无状态设计,即使同域名,.NET 6服务也无法解析.NET Framework生成的加密Cookie内容(除非手动配置DataProtection共享,但步骤繁琐且不符合无状态服务的设计理念)。
  • 获取AD的access_token失败:虽然你设置了SaveTokens=true,但OpenIdConnect中间件默认不会把access_token存入Authentication.Properties.Dictionary,而是存在认证票据的TokenResponse对象里,需要通过特定方式提取。

不过更推荐的方案是利用你应用A中已配置的OAuthAuthorizationServerOptions,为已认证用户颁发自定义JWT令牌,然后在应用B中验证这个令牌来实现免登。下面是具体的落地步骤:


步骤1:完善应用A的OAuth Provider,支持从已认证用户生成JWT

你的应用A已经配置了OAuthAuthorizationServerOptions,但需要确保ApplicationOAuthProvider能处理已认证用户的令牌颁发请求(不需要再走用户名密码验证流程)。

修改ApplicationOAuthProviderGrantResourceOwnerCredentials方法,添加已认证用户的令牌生成逻辑:

public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
    // 新增逻辑:如果用户已经通过OpenIdConnect认证,直接生成令牌
    var authenticatedUser = context.OwinContext.Authentication.User;
    if (authenticatedUser.Identity.IsAuthenticated)
    {
        var identity = new ClaimsIdentity(authenticatedUser.Identity);
        // 可根据需求添加自定义声明(比如用户角色、部门等)
        identity.AddClaim(new Claim(ClaimTypes.Name, authenticatedUser.Identity.Name));
        
        var props = new AuthenticationProperties(new Dictionary<string, string>
        {
            { "userName", authenticatedUser.Identity.Name }
        });
        
        var ticket = new AuthenticationTicket(identity, props);
        context.Validated(ticket);
        return;
    }

    // 保留原有的用户名密码验证逻辑(如果需要支持其他场景)
    // ...
}

步骤2:在应用A中添加获取JWT的API端点

新增一个仅允许已认证用户访问的API,调用OAuth端点为当前用户生成JWT令牌:

[Authorize]
[RoutePrefix("api/auth")]
public class AuthController : ApiController
{
    [HttpGet]
    [Route("get-jwt")]
    public async Task<IHttpActionResult> GetJwtToken()
    {
        var owinContext = Request.GetOwinContext();
        var tokenContext = new OAuthGrantResourceOwnerCredentialsContext(
            owinContext,
            Startup.OAuthOptions,
            string.Empty,
            string.Empty,
            new Dictionary<string, string>()
        );

        var provider = new ApplicationOAuthProvider(Startup.PublicClientId);
        await provider.GrantResourceOwnerCredentials(tokenContext);

        if (tokenContext.Ticket != null)
        {
            var tokenFormatter = new OAuthBearerAccessFormat();
            var token = tokenFormatter.Protect(tokenContext.Ticket);
            return Ok(new { 
                access_token = token, 
                expires_in = (int)Startup.OAuthOptions.AccessTokenExpireTimeSpan.TotalSeconds 
            });
        }

        return Unauthorized();
    }
}

步骤3:在应用B(.NET 6 SignalR)中配置JWT认证

Program.cs中添加JWT认证和SignalR的配置:

var builder = WebApplication.CreateBuilder(args);

// 配置JWT认证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // 关键:这里的参数要和应用A的OAuth配置完全一致
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "你的应用A域名(如https://yourappA.com)",
            ValidAudience = "你的应用B域名(如https://yourappB.com)",
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("你的强密钥(需和应用A的OAuth配置一致)"))
        };

        // 适配SignalR的令牌传递(WebSocket无法在Header中携带令牌,需从查询字符串提取)
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

// 添加SignalR服务
builder.Services.AddSignalR();

var app = builder.Build();

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

// 映射SignalR端点并启用授权验证
app.MapHub<YourSignalRHub>("/hubs/yourhub").RequireAuthorization();

app.Run();

注意:应用A的OAuthOptions需要指定固定的签名密钥,避免默认的DataProtection密钥不一致导致验证失败。在应用A的Startup.cs中修改OAuth配置:

OAuthOptions = new OAuthAuthorizationServerOptions
{
    // 其他原有配置...
    AccessTokenFormat = new JwtFormat(new TokenValidationParameters
    {
        ValidIssuer = "你的应用A域名",
        ValidAudience = "你的应用B域名",
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your_strong_secret_key"))
    }, new JwtSecurityTokenHandler())
};

步骤4:在应用A的Angular前端中获取令牌并连接SignalR

在Angular代码中,先调用应用A的API获取JWT令牌,再携带令牌连接SignalR:

import { HubConnectionBuilder } from '@microsoft/signalr';

async connectToSignalR() {
    // 从应用A的API获取JWT令牌(携带当前用户的Cookie)
    const tokenResponse = await fetch('/api/auth/get-jwt', { credentials: 'include' });
    const tokenData = await tokenResponse.json();
    const accessToken = tokenData.access_token;

    // 连接SignalR,自动将令牌放入查询字符串
    const connection = new HubConnectionBuilder()
        .withUrl('https://yourappB.com/hubs/yourhub', {
            accessTokenFactory: () => accessToken
        })
        .build();

    await connection.start();
    console.log('SignalR连接成功');
}

补充:如何正确获取AD的access_token(如果想用AD原生令牌)

如果你不想自定义JWT,而是想用AD颁发的access_token给应用B验证,可以在应用A的OnSecurityTokenValidated事件中把access_token存入认证票据:

private Task OnSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
    // 将AD的access_token存入认证票据的Properties
    context.AuthenticationTicket.Properties.Dictionary["access_token"] = context.ProtocolMessage.AccessToken;
    
    // 原有的逻辑...
    context.AuthenticationTicket.Properties.AllowRefresh = true;
    var idToken = context.ProtocolMessage.IdToken;
    var claims = context.AuthenticationTicket.Identity;
    var accountId = AuthHelper.UpdateClaimsIdentity(claims, idToken);
    var remoteIpAddress = AuthHelper.GetRemoteIpAddress(claims);
    return Task.FromResult(0);
}

之后你就可以通过Request.GetOwinContext().Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType).Result.Properties.Dictionary["access_token"]获取到AD的access_token,应用B则需要配置Azure AD的JWT验证(使用AD的公钥解析令牌)。


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

火山引擎 最新活动