在另一个 Web API 中管理 Web API JWT 令牌的最佳实践

Posted

技术标签:

【中文标题】在另一个 Web API 中管理 Web API JWT 令牌的最佳实践【英文标题】:Best practices for managing Web API JWT token in another Web API 【发布时间】:2021-01-11 17:28:51 【问题描述】:

我的项目:

Web API 项目 - ASP .NET Framework 4.8

有问题?

代码流程如下:

1.) API 被调用 -> 它必须调用另一个 API -> 2.) 获取 JWT 身份验证令牌 -> 3.) 调用所需的方法。

问题是,如果我的 API 被调用 100 次,我将对 GetJwtToken() 方法进行 100 次调用,并为所需的方法本身调用另外 100 次,这似乎是身份验证服务器的开销。令牌本身的寿命为 2 小时。

是否有关于如何记录?

我尝试了什么?

我已经尝试了以下解决方案,但我仍然不确定它们是否可以被视为良好做法。

一个具有两个静态属性TokenValidTo 的静态类和一个更新这些属性的静态方法GetJwtToken()。在每次调用所需的外部 API 方法之前,我们会检查 ValidTo 属性并通过静态方法更新 Token 的值(如果它已过期)。 在我们的服务中,我们有一个静态私有字段Token。调用外部API方法的方法被trycatch块包围。如果令牌已过期,Catch(WebException ex) 预计会出现未经授权的异常。我检查 HTTP 状态代码 401 - 未经授权。
if (response.StatusCode == HttpStatusCode.Unauthorized)

如果我们进入 if 子句,我们会通过在 catch 块中调用 GetJwtToken() 方法来更新 Token 属性,然后再次递归调用该方法。这样,我们只有在令牌过期并且抛出未经授权的异常时才更新令牌。

我得到的另一个想法,但没有测试 isActionFilterAttribute 覆盖 OnActionExecuting(HttpActionContext actionContext) 方法。在我们进入 Web API 控制器之前,action 属性已经检查了我们是否有Token 以及它是否已经过期。这里的问题是我不确定在哪里保存Token 属性。可能作为另一个类中的静态值。

还有其他方法可以在另一个 Web API 中管理 Web API 的 JWT 令牌吗?什么是最佳实践? 一些代码 sn-ps、伪代码或文章将不胜感激。


编辑1: 我已经阅读了this 的问题,但这对我没有帮助,因为它是关于如何管理前端部分的令牌。这里的项目是 Web API,它都在服务器端。 编辑2: 在这里和那里编辑了一些句子,因此更具可读性。 编辑3: 添加了一个我想到的选项。

【问题讨论】:

【参考方案1】:

也许考虑使用 API 来保存 AuthToken(有状态)。

我对您当前的流程感到困惑,通常您会有一个AuthApi,它具有提供AuthorizationTokens 的功能。

一旦调用者拥有AuthToken,它应该保存它;如您所知,对于前端,考虑本地存储、会话存储和 cookie,对于后端或 API,您可以考虑使用有状态 API 将令牌保存为全局状态,因此它可以将其附加到每个请求而无需执行与您的AuthApi 来回切换(不过,它会在令牌过期时进行一次旅行,等等)。

【讨论】:

有状态 API 是什么意思?我的项目是 .Net Framework 4.8 Web API。如果我将它保存在某处的“状态”中,那对我有什么帮助?整个代码流是什么?令牌的有效期为 2 小时,因此在此之后,它将开始引发授权错误。您建议的事情就像我使用静态类的第一个解决方案,不是吗?【参考方案2】:

我会用某种BaseApiService 来处理这个问题

public class BaseApiService

    private readonly IHttpClientFactory httpClientFactory;
    private readonly ITokenHandler tokenHandler;

    public BaseApiService(IHttpClientFactory httpClientFactory, ITokenHandler tokenHandler)
    
        this.httpClientFactory = httpClientFactory;
        this.tokenHandler = tokenHandler;
    

    protected async Task<HttpResponseMessage> RequestAsync(HttpRequestMessage requestMessage)
    
        var httpClient = httpClientFactory.CreateClient();
        requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenHandler.Token);
        var response = await httpClient.SendAsync(requestMessage);
        
        if (!response.IsSuccessStatusCode)
        
            if (response.StatusCode == HttpStatusCode.Unauthorized)
            
                var token = await tokenHandler.UpdateTokenAsync();
                requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
                return await RequestAsync(requestMessage);
            
        

        return response;
    

它将负责发出请求、响应序列化(注意,为了简单起见,我使用了字符串响应)和处理每个请求的令牌。此外,您可能需要考虑处理错误并处理无限循环,因为它当前正在调用 self(例如,在第二次调用时,如果再次未经授权,则退出并出错)。

令牌处理程序将在 DI 中定义为单例,这是实现

public interface ITokenHandler

    string Token  get; 
    Task<string> UpdateTokenAsync();


public class TokenHandler : ITokenHandler

    private readonly IHttpClientFactory httpClientFactory;
    public string Token  get; private set;  

    public TokenHandler(IHttpClientFactory httpClientFactory)
    
        this.httpClientFactory = httpClientFactory;
    
    
    public async Task<string> UpdateTokenAsync()
    
        var httpClient = httpClientFactory.CreateClient();
        var result = await httpClient.PostAsync("/external-api/token", new FormUrlEncodedContent(new []
        
            new KeyValuePair<string, string>("username", "external-admin"),
            new KeyValuePair<string, string>("password", "external-password"),
        ));

        // or handle it however you want
        var token = result.IsSuccessStatusCode
            ? await result.Content.ReadAsStringAsync()
            : null;

        if (!String.IsNullOrEmpty(token))
        
            Token = token;
        

        return Token;
    

这就是你使用BaseApiService的方式

public class TodoService : BaseApiService

        public TodoService(IHttpClientFactory httpClientFactory, ITokenHandler tokenHandler) 
        : base(httpClientFactory, tokenHandler)
    
    

    public async Task<string> GetTodoAsync(int id)
    
        var response = await RequestAsync(new HttpRequestMessage(HttpMethod.Get, $"/todo/id"));
        return await response.Content.ReadAsStringAsync();
    

我认为您不需要添加任何 ValidTo 逻辑,而只需依赖来自 3rd 方 API 的 Unauthorized 响应,因为您只会使代码复杂化并且您必须处理 Unauthorized无论如何都要回复。

唯一的问题是您可以通过lockTokenHandler 获取/设置令牌,但这只是一个基本示例,展示了我将如何实现它的想法。

【讨论】:

【参考方案3】:

由于字数限制,我会扩展我的 cmets 来回答。

首先,重新考虑/重新检查为什么需要为每个 API 调用调用身份验证服务器?您是否有某种数据存储,例如数据库、缓存(内存或远程)、Azure Blob 存储或共享文件夹?如果有,您可以考虑将访问令牌持久保存到您选择的数据存储中。

现在,让我们处理令牌过期时间。取决于外部 API 如何授予访问令牌(我假设这里是 OAuth2),您通常可以访问令牌的到期时间,例如使用 expires_in in the response。 expires_in 等于自 unix 纪元以来的秒数,因此您应该知道令牌何时到期。然后,您可以保存授予数据存储的令牌及其到期时间和刷新令牌。 当您使用缓存时,您可以将缓存条目设置为在其中的令牌过期前分钟过期。

当您收到下一次 API 调用时,请检查您的数据存储中是否有“有效”令牌。如果否,则调用以获取新的 JWT 令牌并使用上述方法将其持久化。否则,请尝试使用数据存储中的令牌进行 API 调用。如果您有后台服务,例如 WebJob 或 Hangfire,您可以定期根据令牌验证端点(如果您的外部 API 提供)验证所有令牌,并在需要时刷新它们。

您应该始终处理未经授权的回复。令牌可以在过期之前被撤销。如果您收到未经授权的响应,您可以尝试使用外部 API 重新进行身份验证并刷新保存在数据存储中的令牌。如果令牌生成需要用户参与,您可以将 401 返回给您的客户端。

最后,您还需要考虑安全性。当您持久保存令牌时,即使是保存到您自己的数据存储中,您也需要对它们进行加密。 This 适用于 ASP.NET Core,但仍然值得一读并在您的 API 中执行类似的操作。

【讨论】:

感谢您将 cmets 扩展为答案。 感谢您的回答,非常感谢!

以上是关于在另一个 Web API 中管理 Web API JWT 令牌的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

在 Web API 中管理版本

Angular+Web API 应用程序中的会话管理

Web API 服务在读取流时挂起

ASP.NET Core Web API中使用Swagger

使用 OWIN 身份从多个 API 客户端注册 Web API 2 外部登录

docker专项(六)利用docker Api提供web操作docker服务