为已通过Cookie认证的Active Directory用户生成JWT令牌,实现.NET Framework与.NET 6 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能处理已认证用户的令牌颁发请求(不需要再走用户名密码验证流程)。
修改ApplicationOAuthProvider的GrantResourceOwnerCredentials方法,添加已认证用户的令牌生成逻辑:
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




