停止刷新令牌的竞争条件?

Posted

技术标签:

【中文标题】停止刷新令牌的竞争条件?【英文标题】:Stop Race Condition of Refresh Token? 【发布时间】:2019-09-28 15:44:19 【问题描述】:

我有一个 reactjs 站点和 asp.net 核心后端,但刷新令牌有问题。

当有人登录我的网站时,他们会获得访问令牌和刷新令牌(非常标准)。现在我设置了一个比访问令牌的时间更短的计时器。

当他们打开多个标签时,这一切都很好。问题是他们都共享本地存储(需要自动登录,所以不能使用会话存储)

场景

2 个标签依次打开。在访问令牌失效前 2 分钟设置了 2 个计时器。

第一次触发首先将刷新令牌发送到服务器并带回新的刷新/访问令牌。在服务器上,发送的刷新令牌被删除。

第二个计时器在第一个计时器之后不久触发(当第一个计时器工作时),但现在很可能刷新令牌已被删除,从而使此请求无效。

如何停止这种竞争状态?

var foundRefreshToken = dbContext.Tokens.FirstOrDefault(x => x.Value == refreshToken);

if (foundRefreshToken == null)

    return null;


var newRefreshToken = CreateRefreshToken(foundRefreshToken.ClientId, foundEmployee.Id);

dbContext.Tokens.Remove(foundRefreshToken);
dbContext.Tokens.Add(newRefreshToken);
dbContext.SaveChanges();


private Token CreateRefreshToken(string clientId, string userId)
    
        return new Token()
        
            ClientId = clientId,
            EmployeeId = userId,
            Value = GenerateRefreshToken(),
            CreatedDate = DateTime.UtcNow
        ;
    

// high level js
  refreshTimer;
  setRefreshTimer(intervals) 
    this.clearRefreshTimer();
    this.refreshTimer = setInterval(() => 
      this.refreshAuthentication();
    , intervals);
  

我能想到的唯一两件事是不要删除刷新令牌(但这会导致自动登录出现问题)

或者我在本地存储中有一个标志“锁定”第一个选项卡以进行刷新,而其他选项卡则等待查看它是否这样做(猜想需要另一个计时器)。如果没有,那么下一个尝试。

还有其他人有其他想法吗?

【问题讨论】:

当您获得两个选项卡的访问和刷新令牌时,它们是相同的还是您得到两个不同的副本?另外,你不能也存储这两者的最后一次设置在本地存储中,并在它们执行之前让计时器函数检查它们吗? 第一个选项卡打开,获取访问/刷新,第二个选项卡打开并使用第一个选项卡获得的访问/刷新作为本地存储共享。所以只有 1 个。如果我有 2 个,那意味着每次都必须重新登录或其他东西 你不能在 CreateRefreshToken 上设置令牌的过期时间并使用那些 Unix 时间戳来检查令牌的有效性吗? 但是如果这个人在到期后说然后他们会自动注销,这不是预期的结果,因为他们应该能够在一周后带着刷新令牌回来并且仍然能够登录。我需要能够处理自动登录,但还要处理同时打开的多个选项卡。 您需要设置用户需要重新登录的时间...如果是一周,您的刷新令牌需要一周,以便您可以同时创建新令牌...通常刷新令牌的寿命很长,而令牌的寿命很短 【参考方案1】:

为此,我使用页面可见性 API https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API。 当标签处于非活动状态时,我会取消续订订阅并在标签再次处于活动状态时设置续订。

我的 Angular 代码是:

  constructor(
    private router: Router,
    private http: HttpClient,
    private tokenService: TokenService,
    @Inject(DOCUMENT) private readonly documentRef: Document,
    ) 
    this.getToken(); // From local Browser store
    this.loggedIn = this.isAuthenticated();
    this.status = new BehaviorSubject<boolean>(this.loggedIn);
    this.documentRef.addEventListener("visibilitychange", () => 
      console.log(document.hidden, document.visibilityState);
      if (document.hidden) 
        this.cancelRenewal();
       else 
        this.getToken();
        this.loggedIn = this.isAuthenticated();
        this.status.next(this.loggedIn);
        this.scheduleRenewal();
      
    , false);
  

【讨论】:

【参考方案2】:

最佳实践要求刷新令牌只能使用一次,并在每次使用时发布一个新令牌。再次使用旧令牌的尝试应被视为被盗令牌 - 该用户的所有未完成令牌都应失效,并且任何新的访问尝试都应要求完全登录。

当两个会话共享一个公共刷新令牌时(例如在浏览器中打开两个选项卡并且令牌存储在仅 http 的 cookie 中时),就会出现竞争条件。当两个会话同时尝试使用相同的刷新令牌进行刷新的情况发生时,服务器的第一个会话会获得一个有效的新令牌,但第二个会话发现他们的令牌现在无效并被注销。

正如 OP 所提到的,这可以通过使用诸如繁忙标志之类的机制在前端解决,这样第一次刷新必须在第二次刷新之前完成。

在后端,您可以拥有一种机制,允许在刷新令牌完全失效或删除之前在很短的时间内(仅几秒钟)重复使用它。

【讨论】:

【参考方案3】:

当您创建令牌和刷新令牌时,两者都应该有一个到期日期,例如:

return new Token()
        
            ClientId = clientId,
            EmployeeId = userId,
            Value = GenerateRefreshToken(),
            CreatedDate = DateTime.UtcNow,
            ExpirationDate = <you decide>
        ;

对于每个请求,您都应该通过比较日期来检查您的令牌是否过期。如果它已过期,您可以使用保持用户身份验证。最终,您甚至可以永远不会使刷新令牌过期,因为它必须由您的应用程序安全地存储。

刷新令牌和短期令牌背后的想法是,如果令牌被泄露,黑客只有 10 分钟的时间,然后他才需要 refresh_token 来生成新令牌...

【讨论】:

好吧,我已经这样做了,我的令牌持续 20 分钟,直到我需要一个新的。我所做的是刷新令牌是一次性使用的交易。访问令牌会附带一个新的刷新令牌,而旧的刷新令牌将是无效的。现在,如果您打开了 5 个选项卡,它们都共享相同的刷新令牌(因为它们都共享相同的本地存储),这会导致大问题 如果我保留相同的刷新令牌,这将解决问题,但现在情况如何,我很可能最终会发送 5 个访问令牌,因为每个选项卡都要求一个新的他们都认为令牌即将到期。所以这是个问题 @chobo2 我遇到了类似的问题。你是怎么解决的?

以上是关于停止刷新令牌的竞争条件?的主要内容,如果未能解决你的问题,请参考以下文章

MySQL事务没有停止for循环的竞争条件

在带有Redux的ReactJS中避免竞争条件

OAuth 2.0 刷新令牌多个选项卡

条件竞争和恶性条件竞争

条件竞争漏洞测试

Django会话竞争条件?