Rails集成NetDocuments API的Invalid Grant错误处理及令牌管理
解决方案:NetDocuments API令牌管理的竞态条件与错误处理
你遇到的Invalid Grant错误核心是多线程/多进程环境下的竞态条件——多个进程同时尝试用同一个刷新令牌请求新令牌,第一个请求成功后原刷新令牌失效,后续请求就会失败。下面针对你的问题给出具体解决方案:
1. 多线程/多进程环境下确保刷新令牌仅使用一次
解决这个问题的核心是数据库级别的排他锁,Rails内存锁(如Mutex)无法跨进程生效,必须借助数据库行锁保证同一时间只有一个进程能执行刷新逻辑。
修改你的代码,用with_lock包裹刷新逻辑,同时在锁内部二次检查过期时间(避免等待锁期间凭证已被其他进程更新):
def self.netdocuments_credential credential = Credential.find_by(kind: 'netdocuments') return nil unless credential.present? # 数据库行锁:跨进程生效,同一时间仅一个进程能进入代码块 credential.with_lock do # 二次检查过期时间,避免等待锁期间凭证已被刷新 if Time.now > credential.expires_at resp = Faraday.post("#{API_BASE_URL[get_data_region]}/v1#{TOKEN_PATH}") do |req| authorization_string = Base64.encode64("#{CLIENT_ID[get_data_region]}:#{CLIENT_SECRET[get_data_region]}").gsub(/\n/, '') req.headers['Authorization'] = "Basic " + authorization_string req.body = { grant_type: "refresh_token", refresh_token: credential.refresh_token } end begin response_body = JSON.parse(resp.body) if resp.success? access_token = response_body["access_token"] refresh_token = response_body["refresh_token"] credential.update!( token: access_token, refresh_token: refresh_token, expires_at: Time.now + 3600 ) else # 主动处理API返回的错误(如Invalid Grant) error_msg = response_body['error_description'] || resp.body Rails.logger.error "NetDocuments令牌刷新失败: #{error_msg}" handle_invalid_grant(credential) end rescue JSON::ParserError => e Rails.logger.error "解析NetDocuments响应失败: #{e.message}" handle_invalid_grant(credential) end end end credential end # 新增Invalid Grant错误处理方法,替换直接删除凭证的逻辑 def self.handle_invalid_grant(credential) # 优先尝试自动恢复:比如用服务账号的client_credentials模式重新获取令牌(如果API支持) # 示例逻辑(需根据NetDocuments服务账号流程调整): # recovery_resp = Faraday.post(TOKEN_URL) do |req| # req.body = { grant_type: "client_credentials", scope: "..." } # ... # end # 如果自动恢复失败,再标记凭证无效而非直接删除 credential.update!(invalid: true) raise StandardError, "NetDocuments令牌失效,正在自动恢复,请稍后重试。" end
关键改动:
- 用
with_lock实现跨进程的排他锁,彻底避免并发刷新 - 锁内部二次校验过期时间,避免无效刷新操作
- 新增错误分支处理API返回的失败响应,替换粗暴删除凭证的逻辑
- 提供自动恢复的扩展入口,减少用户手动干预
2. 手动设置1小时过期时间是否会引发Invalid Grant?
这个不是直接原因。NetDocuments访问令牌默认有效期就是1小时,你手动设置expires_at = Time.now + 3600是符合API规范的。
如果想进一步降低并发刷新概率,可以提前5-10分钟刷新令牌(比如设置expires_at = Time.now + 3540),这样即使有少量线程同时检查,也会在令牌真正过期前完成刷新,减少并发触发的场景。
额外优化建议
- 增加日志维度:记录刷新操作的进程ID、线程ID、时间,方便排查竞态问题
- 实现退避机制:遇到
Invalid Grant时,等待几秒后再重试,避免重复无效请求 - 分布式锁备选:如果应用部署在多台服务器,数据库行锁依然有效,也可以用Redis分布式锁(如
redlock-rbgem)增强灵活性
内容的提问来源于stack exchange,提问作者Pratham Majithia
相关产品推荐
相关产品推荐

