using Fengling.Platform.Domain.AggregatesModel.UserAggregate; using Fengling.Platform.Domain.AggregatesModel.RoleAggregate; using Fengling.Platform.Infrastructure; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; using System.Linq; namespace Fengling.AuthService.Controllers; [ApiController] [Route("connect")] public class LogoutController : ControllerBase { private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictAuthorizationManager _authorizationManager; private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly ILogger _logger; public LogoutController( IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, UserManager userManager, SignInManager signInManager, ILogger logger) { _applicationManager = applicationManager; _authorizationManager = authorizationManager; _userManager = userManager; _signInManager = signInManager; _logger = logger; } [HttpGet("endsession")] [HttpPost("endsession")] [IgnoreAntiforgeryToken] public async Task EndSession() { var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("OpenIddict request is null"); // 标准做法:先尝试从 id_token_hint 中提取客户端信息 string? clientId = request.ClientId; if (string.IsNullOrEmpty(clientId) && !string.IsNullOrEmpty(request.IdTokenHint)) { try { // 从 id_token_hint 中提取 client_id (azp claim 或 aud claim) var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; if (principal != null) { // 尝试从 azp (authorized party) claim 获取 clientId = principal.GetClaim(Claims.AuthorizedParty); // 如果没有 azp,尝试从 aud (audience) claim 获取 if (string.IsNullOrEmpty(clientId)) { clientId = principal.GetClaim(Claims.Audience); } } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to extract client_id from id_token_hint"); } } // 执行登出 var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); if (result.Succeeded) { await _signInManager.SignOutAsync(); } // 处理 post_logout_redirect_uri if (!string.IsNullOrEmpty(clientId)) { var application = await _applicationManager.FindByClientIdAsync(clientId); if (application != null) { var registeredUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application); // 如果提供了 post_logout_redirect_uri,验证它是否在注册的 URI 列表中 if (!string.IsNullOrEmpty(request.PostLogoutRedirectUri)) { if (registeredUris.Contains(request.PostLogoutRedirectUri)) { // 如果提供了 state,需要附加到重定向 URI var redirectUri = request.PostLogoutRedirectUri; if (!string.IsNullOrEmpty(request.State)) { var separator = redirectUri.Contains('?') ? "&" : "?"; redirectUri = $"{redirectUri}{separator}state={Uri.EscapeDataString(request.State)}"; } return Redirect(redirectUri); } else { _logger.LogWarning( "Post-logout redirect URI {Uri} is not registered for client {ClientId}", request.PostLogoutRedirectUri, clientId); } } else { // 如果没有提供 post_logout_redirect_uri,使用第一个注册的 URI var defaultUri = registeredUris.FirstOrDefault(); if (!string.IsNullOrEmpty(defaultUri)) { _logger.LogInformation( "Using default post-logout redirect URI for client {ClientId}", clientId); return Redirect(defaultUri); } } } } // 如果无法确定重定向地址,返回默认页面 return Redirect("/"); } }