已更新 - 在与 Matt G 讨论后,我为我的答案添加了更好的解释,以便明确我的观点。我想我一开始还不够清楚。
更新 2 - 添加点 5
我认为应该为一个客户端颁发令牌,并且必须仅由该特定客户端使用才能访问它请求访问的所有资源。
案例
- Api1 请求令牌并且可以访问 Api2、Api3、Api4、Api5。
- Api2 使用 Api1 的令牌,并且可以访问与 Api1 相同的资源。
评论
表示Api2可以访问Api3、Api4、Api5。但是,如果不应该授予 Api2 访问 Api5 的权限,会发生什么?现在你有问题。一旦出现这种情况,您就必须重新设计您的安全机制。
此外,这意味着发送到 Api2 的令牌包含与其无关的范围,这对我来说听起来有点奇怪。
另一方面,Api1 的范围可能对 Api2 意味着不同的东西,这可能会导致误解。但这将取决于您的发展。
如果您使用 Scopes 进行 authentication 和 authorization,则不应共享您的令牌,因为 Api1 可以执行例如 Api2 不应执行的代码并且这是一个安全问题。
如果 Api1 是向 IdP 请求令牌的那个。如果 Api2 发生了什么
您想将它与 Api1 分开使用吗?它不能调用其他 API,因为 Api1 没有将令牌传递给它?还是所有 API 都能够向 IdP 请求令牌,并且所有 API 都根据第一次调用的 API 将令牌传递给其他 API?您是否可能比需要的复杂程度更高?
您想要实现的目标是可行的,但对我来说这不是一个好主意。
下面我建议你解决这个问题的替代方案。
听起来您每次执行 HttpClient.Send 时都需要一个 TokenCache 和一个注入它的机制。这就是我给你的建议。
你应该创建一个叫TokenCache的类,这个类负责在每次过期、无效或为空时获取Token。
public class TokenCache : ITokenCache
{
public TokenClient TokenClient { get; set; }
private readonly string _scope;
private DateTime _tokenCreation;
private TokenResponse _tokenResponse;
public TokenCache(string scope)
{
_scope = scope;
}
private bool IsTokenValid()
{
return _tokenResponse != null &&
!_tokenResponse.IsError &&
!string.IsNullOrWhiteSpace(_tokenResponse.AccessToken) &&
(_tokenCreation.AddSeconds(_tokenResponse.ExpiresIn) > DateTime.UtcNow);
}
private async Task RequestToken()
{
_tokenResponse = await TokenClient.RequestClientCredentialsAsync(_scope).ConfigureAwait(false);
_tokenCreation = DateTime.UtcNow;
}
public async Task<string> GetAccessToken(bool forceRefresh = false)
{
if (!forceRefresh && IsTokenValid()) return _tokenResponse.AccessToken;
await RequestToken().ConfigureAwait(false);
if (!IsTokenValid())
{
throw new InvalidOperationException("An unexpected token validation error has occured during a token request.");
}
return _tokenResponse.AccessToken;
}
}
您创建一个 TokenHttpHandler 类,如下所示。每次执行 HttpClient.Send 时,此类都会设置 Bearer 令牌。请注意,我们使用 TokenCache (_tokenCache.GetAccessToken) 在 SetAuthHeaderAndSendAsync 方法中获取令牌。这样您就可以确定每次您从 api/mvc 应用程序调用另一个 api 时都会发送您的令牌。
public class TokenHttpHandler : DelegatingHandler
{
private readonly ITokenCache _tokenCache;
public TokenHttpHandler(ITokenCache tokenCache)
{
InnerHandler = new HttpClientHandler();
_tokenCache = tokenCache;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await SetAuthHeaderAndSendAsync(request, cancellationToken, false).ConfigureAwait(false);
//check for 401 and retry
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
response = await SetAuthHeaderAndSendAsync(request, cancellationToken, true);
}
return response;
}
private async Task<HttpResponseMessage> SetAuthHeaderAndSendAsync(HttpRequestMessage request, CancellationToken cancellationToken, bool forceTokenRefresh)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await _tokenCache.GetAccessToken(forceTokenRefresh).ConfigureAwait(false));
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
然后你在 ExtendedHttpClient 中使用它,如下所示。请注意,我们将 TokenHttpHandler 注入到构造函数中。
public class ExtendedHttpClient : HttpClient
{
public ExtendedHttpClient(TokenHttpHandler messageHandler) : base(messageHandler)
{
DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
}
}
最后,在您的 IoC 配置中,您需要添加新类。
如果您想为多个 MVC 应用程序/Api 重复使用上述代码,那么您应该将它放在一个共享库(例如基础架构)中,然后只为每个 IdentityServer 客户端配置 IoC。
builder.RegisterType<TokenHttpHandler>().AsSelf();
builder.RegisterType<ExtendedHttpClient>().As<HttpClient>();
builder.RegisterType<TokenCache>()
.As<ITokenCache>()
.WithParameter("scope", "YOUR_SCOPES")
.OnActivating(e => e.Instance.TokenClient = e.Context.Resolve<TokenClient>())
.SingleInstance();
builder.Register(context =>
{
var address = "YOUR_AUTHORITY";
return new TokenClient(address, "ClientID", "Secret");
})
.AsSelf();
请注意,此示例使用 ClientCredentials 流,但您可以采用此概念并对其进行修改以使其符合您的要求。
希望对您有所帮助。
亲切的问候
丹尼尔