You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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结合令牌处理程序,自动管理令牌的获取与传递:

  1. 在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;
});
  1. 实现AccessTokenHandler,通过HttpContext.GetTokenAsync("access_token")从当前用户上下文获取令牌并自动附加到请求头。

方案2:修正授权码模式流程

若坚持使用授权码模式,需调整流程:

  1. 在Admin客户端登录流程中,引导用户跳转至IDP授权端点,获取授权码后立即交换AccessToken,并将令牌存入用户会话(如HttpContext.Session.SetString("access_token", token))。
  2. 后续API调用时,从用户会话中读取AccessToken,而非从Query参数中获取code。
  3. 单例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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 22:21:08