ASP.NET+OpenIddict SSO系统中Admin客户端调用API无法获取AccessToken问题
ASP.NET + OpenIddict SSO Admin客户端令牌传递问题排查
我用ASP.NET和OpenIddict搭建公司SSO系统,单独做了一个独立于身份提供者(IDP)的Admin用户管理客户端。为避免数据冗余,Admin客户端通过调用IDP提供的API操作用户数据(IDP独占数据库访问权)。
Admin客户端侧已用[Authorize(Policy = "AdminUser")]实现授权验证,但IDP侧的API控制器使用相同授权策略时,Admin客户端的请求会被判定未授权并跳转至登录页。核心问题是无法正确获取并传递AccessToken,目前从HttpContext.Query["code"]拿到的授权码始终为null,导致后续请求失败。
问题排查方向
- 授权码模式流程逻辑错误:授权码
code仅在用户完成OAuth2授权跳转回客户端的首次请求中才会出现在Query参数里。你的AdminClient是单例服务,后续常规请求(如页面刷新、API调用)的Query中根本不会包含code,这是核心逻辑漏洞。 - HttpContextAccessor注册错误:注册AdminClient时手动实例化
HttpContextAccessor()是错误的,ASP.NET Core默认已注册IHttpContextAccessor,应通过依赖注入获取实例,手动new的实例无法获取当前请求上下文。 - 令牌存储复用逻辑缺失:即使拿到授权码,也没有缓存AccessToken,每次调用API都重新获取令牌,既低效又不符合OAuth2规范。此外,单例AdminClient存储令牌会导致多用户令牌冲突,应将令牌和过期时间存入用户会话或分布式缓存。
- IDP侧API授权配置问题:确认IDP的API控制器是否正确配置JWT验证,是否允许Admin客户端调用该API,以及令牌是否包含
AdminUser等必要角色声明。
可行实现方案
方案1:利用ASP.NET Core内置令牌管理机制
在Admin客户端中使用IHttpClientFactory结合令牌处理程序,自动管理令牌的获取与传递:
- 在Program.cs中注册带令牌处理的HttpClient:
builder.Services.AddHttpClient("IDPApiClient", client => { client.BaseAddress = new Uri(clientDetails.GetSection("IssuerUrl").Value); }) .AddHttpMessageHandler(sp => { var handler = sp.GetRequiredService<AccessTokenHandler>(); handler.Scope = "user_administration"; // 对应IDP中配置的API权限范围 return handler; });
- 实现
AccessTokenHandler,通过HttpContext.GetTokenAsync("access_token")从当前用户上下文获取令牌并自动附加到请求头。
方案2:修正授权码模式流程
若坚持使用授权码模式,需调整流程:
- 在Admin客户端登录流程中,引导用户跳转至IDP授权端点,获取授权码后立即交换AccessToken,并将令牌存入用户会话(如
HttpContext.Session.SetString("access_token", token))。 - 后续API调用时,从用户会话中读取AccessToken,而非从Query参数中获取
code。 - 单例AdminClient不存储令牌,每次调用时从当前请求的会话中获取。
当前实现代码
HomeController.cs的Index方法
[HttpGet("~/")] public async Task<IActionResult> Index() { List<UserInfoModel> userModel = new(); var users = await _adminClient.GetAllUsersAsync(); foreach ( var user in users ) { UserInfoModel userInfo = new(); userInfo.Id = user.Id; userInfo.Role = user.Role; userInfo.UserName = user.UserName; userInfo.Email = user.Email; userModel.Add(userInfo); } return View(userModel); }
AdminClient.cs(单例服务)
private readonly HttpClient client = new(); private readonly string clientId; private readonly string clientSecret; private readonly ILogger logger; private readonly object accessTokenLock = new object(); private DateTime accessTokenExpiry = DateTime.MinValue; private readonly IHttpContextAccessor _httpContextAccessor; public AdminClient(string baseAddress, string clientId, string clientSecret, ILogger<AdminClient> logger, IHttpContextAccessor httpContextAccessor) { client.BaseAddress = new Uri(baseAddress); this.clientId = clientId; this.clientSecret = clientSecret; this.logger = logger; _httpContextAccessor = httpContextAccessor; }
Program.cs注册代码
builder.Services.AddSingleton<IAdminClient>(serviceProvider => new AdminClient( clientDetails.GetSection("IssuerUrl").Value?.ToString(), clientDetails.GetSection("ClientId").Value?.ToString(), clientDetails.GetSection("ClientSecret").Value?.ToString(), serviceProvider.GetRequiredService<ILogger<AdminClient>>(), new HttpContextAccessor()));
GetAllUsersAsync方法
public async Task<List<UserDTO>> GetAllUsersAsync() { try { // 获取授权码 string? authCode = _httpContextAccessor?.HttpContext?.Request.Query["code"]; string? accessToken = await GetAccessTokenAsync(authCode); HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = new Uri($"useradministration/getallusersdetailsandclaims", UriKind.Relative), Headers = { Authorization = new AuthenticationHeaderValue("Bearer", accessToken) } }; var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); var userData = System.Text.Json.JsonSerializer.Deserialize<List<UserDTO>>(content, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); return userData; } catch (Exception ex) { throw new InvalidOperationException(ex.Message); } }
GetAccessTokenAsync方法
private async Task<string> GetAccessTokenAsync(string authCode) { HttpRequestMessage request = new() { Method = HttpMethod.Post, RequestUri = new Uri($"{client.BaseAddress}connect/token"), Content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("grant_type", "authorization_code"), new KeyValuePair<string, string>("code", authCode), new KeyValuePair<string, string>("client_id", clientId), new KeyValuePair<string, string>("client_secret", clientSecret) }) }; try { HttpResponseMessage? response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); string responseContent = await response.Content.ReadAsStringAsync(); JObject json = JObject.Parse(responseContent); string accessToken = json["access_token"].ToString(); return accessToken; } catch (Exception ex) { throw new Exception(ex.Message); } }
内容的提问来源于stack exchange,提问作者Jack Reynolds
相关产品推荐
相关产品推荐

