基于OpenIddict的.NET Core OIDC应用间SSO授权码流转实现指引
跨OpenIddict OIDC应用自动登录实现方案
针对你提到的两个基于.NET Core + OpenIddict的OIDC应用跳转需求,直接拆解成配置、代码实现和注意事项三部分:
一、核心逻辑梳理
用户在应用1(ServerA+ClientA)登录后,点击跳转按钮时,ClientA向ServerA申请针对应用2客户端(ClientB)的授权码,然后重定向到应用2的OIDC服务端(ServerB)的授权端点,携带该授权码。ServerB验证授权码的合法性(通过对接ServerA的令牌验证接口)后,自动为用户颁发ServerB的令牌,完成ClientB的自动登录。
二、双方OpenIddict配置调整
1. 应用1(ServerA)配置:注册ClientB为信任客户端
ServerA需要将应用2的ClientB注册为合法客户端,允许其通过授权码流获取凭证:
builder.Services.AddOpenIddict() .AddServer(options => { // 保留原有配置(如授权端点、令牌端点、加密密钥等) options.RegisterClient("ClientB", client => { client.SetClientId("clientb_official"); // 自定义ClientID,两边保持一致 client.SetClientSecret("your_secure_client_secret"); // 生产环境用Secret Manager或环境变量存储 client.SetRedirectUris("https://clientb.yourdomain.com/signin-oidc"); // ClientB的OIDC回调地址 client.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Profile); client.SetGrantTypes(OpenIddictConstants.GrantTypes.AuthorizationCode); client.SetResponseTypes(OpenIddictConstants.ResponseTypes.Code); }); });
2. 应用2(ServerB)配置:信任ServerA为外部身份提供者
ServerB需要启用外部OIDC认证,把ServerA作为可信身份源,同时注册ClientB为自己的客户端:
// 1. 添加对ServerA的外部OIDC认证 builder.Services.AddAuthentication() .AddOpenIdConnect("ServerA_Auth", "应用1身份登录", options => { options.Authority = "https://servera.yourdomain.com"; // ServerA的根地址 options.ClientId = "clientb_official"; // 和ServerA注册的ClientID一致 options.ClientSecret = "your_secure_client_secret"; options.ResponseType = OpenIdConnectResponseType.Code; options.Scope.Add(OpenIdConnectScope.Email); options.Scope.Add(OpenIdConnectScope.Profile); options.SaveTokens = true; // 保存从ServerA获取的凭证 // 自动携带传入的授权码,跳过用户选择步骤 options.Events = new OpenIdConnectEvents { OnRedirectToIdentityProvider = context => { if (context.Properties.TryGetValue("servera_auth_code", out var code)) { context.ProtocolMessage.Code = code; } return Task.CompletedTask; } }; }); // 2. 配置ServerB自身的OpenIddict服务,为ClientB颁发令牌 builder.Services.AddOpenIddict() .AddServer(options => { // 保留原有配置(如令牌颁发、资源范围等) options.RegisterClient("ClientB", client => { client.SetClientId("clientb_official"); client.SetClientSecret("your_secure_client_secret"); client.SetRedirectUris("https://clientb.yourdomain.com/signin-oidc"); client.SetScopes(OpenIddictConstants.Scopes.OpenId, "clientb_api_resource"); // 应用2的资源范围 client.SetGrantTypes(OpenIddictConstants.GrantTypes.AuthorizationCode); }); });
三、应用1(ClientA)跳转按钮实现
1. 后端逻辑:生成授权码并构造重定向URL
在ClientA的跳转按钮对应的Razor Page后端,实现生成授权码并跳转的逻辑:
public class IndexModel : PageModel { private readonly IOpenIddictAuthorizationService _authorizationService; public IndexModel(IOpenIddictAuthorizationService authorizationService) { _authorizationService = authorizationService; } public async Task<IActionResult> OnPostRedirectToApp2() { if (!User.Identity.IsAuthenticated) { return RedirectToPage("/Account/Login"); } // 向ServerA申请针对ClientB的授权码 var authPrincipal = await _authorizationService.CreateAuthorizationAsync( principal: User, subject: User.FindFirstValue(ClaimTypes.NameIdentifier), client: "clientb_official", scopes: new[] { OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.Email } ); var authCode = await _authorizationService.CreateAuthorizationCodeAsync(authPrincipal); // 构造重定向到ServerB授权端点的URL,携带授权码 var redirectUrl = new UriBuilder("https://serverb.yourdomain.com/connect/authorize") { Query = string.Join("&", new Dictionary<string, string> { ["client_id"] = "clientb_official", ["redirect_uri"] = "https://clientb.yourdomain.com/signin-oidc", ["response_type"] = "code", ["scope"] = "openid clientb_api_resource", ["code"] = authCode, ["prompt"] = "none" // 强制自动登录,不显示选择页面 }.Select(kv => $"{kv.Key}={Uri.EscapeDataString(kv.Value)}")) }; return Redirect(redirectUrl.ToString()); } }
2. 前端按钮:添加POST表单
在ClientA的Razor Page视图中添加跳转按钮:
<form method="post" asp-page-handler="RedirectToApp2"> <button type="submit" class="btn btn-primary">跳转到应用2</button> </form>
四、应用2(ClientB)自动登录与令牌颁发
当ServerB验证完ServerA的授权码后,会重定向到ClientB的signin-oidc回调地址,此时ClientB会自动完成身份认证,你可以在回调后触发ServerB的令牌颁发逻辑:
1. 配置回调路由
确保ClientB的Program.cs中启用了认证和路由:
app.UseAuthentication(); app.UseAuthorization(); app.MapRazorPages(); app.MapControllers(); // 确保signin-oidc路由被正确处理
2. 自动获取ServerB令牌
在ClientB的首页或专用回调页面中,完成用户登录并获取ServerB的令牌:
public class HomeModel : PageModel { private readonly IOpenIddictTokenService _tokenService; public HomeModel(IOpenIddictTokenService tokenService) { _tokenService = tokenService; } public async Task<IActionResult> OnGet() { // 如果用户未通过ServerA认证,触发跳转认证 if (!User.Identity.IsAuthenticated) { return Challenge("ServerA_Auth"); } // 向ServerB申请自身的访问令牌 var tokenPrincipal = await _tokenService.CreateTokenAsync( grantType: OpenIddictConstants.GrantTypes.AuthorizationCode, clientId: "clientb_official", subject: User.FindFirstValue(ClaimTypes.NameIdentifier), scopes: new[] { "clientb_api_resource" } ); // 将令牌保存到Cookie,供后续访问应用2资源使用 await HttpContext.SignInAsync(tokenPrincipal); return Page(); } }
五、关键注意事项
- 安全优先:生产环境禁止硬编码客户端密钥,使用.NET Secret Manager、环境变量或Azure Key Vault等安全存储方案;全程启用HTTPS,防止授权码泄露。
- 授权码有效期:OpenIddict默认授权码有效期为5分钟,若跳转流程耗时较长,可在ServerA的配置中通过
options.SetAuthorizationCodeLifetime(TimeSpan.FromMinutes(10))调整。 - 凭证验证:ServerB在验证ServerA的授权码时,会自动校验签名、Issuer、Audience等字段,确保凭证合法有效,无需额外手动验证。
- Scope一致性:确保ServerA和ServerB的Scope配置匹配,保证用户身份信息(如邮箱、昵称)能正确传递。
内容的提问来源于stack exchange,提问作者Dah Sra
相关产品推荐
相关产品推荐

