使用Google Calendar API遇Signet授权错误(invalid_grant)求助
问题分析与解决方案
核心错误原因
- 授权码(code)重复使用:OAuth 2.0的授权码是一次性的,且有效期仅约10分钟。你通过Devise Omniauth登录时,Omniauth已经用该code换取过一次token,再次使用同一个code会直接触发
invalid_grant错误。 - Redirect URI不匹配:你在
client_options中配置的redirect_uri指向Devise的回调地址,但Calendar控制器的callback路由是独立的,URI不匹配会导致谷歌服务器验证授权请求失败。 - 错误存储code而非token:你将授权code存在用户模型中,而非直接存储谷歌返回的access_token和refresh_token,这完全违背了OAuth的流程逻辑。
修复步骤
1. 调整Devise Omniauth配置(获取Calendar权限)
修改config/initializers/devise.rb,添加Calendar权限并开启离线访问(用于获取refresh_token):
config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], scope: 'email,profile,calendar.readonly', access_type: 'offline', # 必须添加才能获取refresh_token prompt: 'select_account consent' # 确保首次授权时用户同意离线访问
2. 修改用户模型的Omniauth回调逻辑
直接存储谷歌返回的token信息,而非code:
def self.from_omniauth(auth) where(provider: auth.provider, uid: auth.uid).first_or_create do |user| user.email = auth.info.email user.password = Devise.friendly_token[0, 20] # 存储谷歌授权token user.google_access_token = auth.credentials.token user.google_refresh_token = auth.credentials.refresh_token user.google_token_expires_at = Time.at(auth.credentials.expires_at) end end
3. 重构Calendar控制器逻辑
删除错误的code复用逻辑,直接使用用户存储的token,并处理token过期自动刷新:
class Webhooks::CalendarController < ApplicationController before_action :authenticate_user! skip_after_action :verify_authorized # 如果需要单独的Calendar授权流程(用户已登录但未授权Calendar),保留此方法 def redirect client = Signet::OAuth2::Client.new(client_options) redirect_to client.authorization_uri.to_s, allow_other_host: true end def callback client = Signet::OAuth2::Client.new(client_options) client.code = params[:code] # 使用当前请求的一次性code,而非用户存储的旧code response = client.fetch_access_token! # 将新token存入用户模型 current_user.update!( google_access_token: response['access_token'], google_refresh_token: response['refresh_token'], google_token_expires_at: Time.now + response['expires_in'].seconds ) redirect_to calendar_url end def calendars client = Signet::OAuth2::Client.new(client_options) client.access_token = current_user.google_access_token client.refresh_token = current_user.google_refresh_token client.expires_at = current_user.google_token_expires_at.to_i # 自动刷新过期的token if client.expired? response = client.refresh_access_token! current_user.update!( google_access_token: response['access_token'], google_token_expires_at: Time.now + response['expires_in'].seconds ) client.access_token = response['access_token'] end service = Google::Apis::CalendarV3::CalendarService.new service.authorization = client @calendar_list = service.list_calendar_lists end private def client_options { client_id: ENV.fetch("GOOGLE_CLIENT_ID", ""), client_secret: ENV.fetch("GOOGLE_CLIENT_SECRET", ""), authorization_uri: "https://accounts.google.com/o/oauth2/auth", token_credential_uri: "https://accounts.google.com/o/oauth2/token", scope: Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY, redirect_uri: webhooks_callback_url # 使用Rails路由助手确保URI与控制台配置一致 } end end
4. 开发者控制台配置检查
确保OAuth 2.0客户端ID的授权重定向URI包含Calendar控制器的回调地址(例如http://localhost:3000/webhooks/callback),与client_options中的redirect_uri完全一致。
内容的提问来源于stack exchange,提问作者maxagno3
相关产品推荐
相关产品推荐

