Java调用Gmail API报400 invalid_grant 刷新令牌频繁失效问题
Google OAuth refresh_token 闲置数天即失效返回invalid_grant问题修复
问题复现表现
- 基于Google OAuth Playground生成的
refresh_token,在Java业务代码中用于兑换新access_token,功能上线初期正常,间隔数天未运行后调用接口直接报错 - 接口返回错误响应:
{ "error": "invalid_grant", "error_description": "Token has been expired or revoked." }
- 重新在OAuth Playground生成新的
refresh_token替换credentials.json内的旧值后,功能立刻恢复,但闲置数天后会再次出现完全相同的错误
根因说明
- 核心配置问题:你当前GCP项目的OAuth同意屏幕处于「Testing(测试)」状态,该状态下Google会强制所有签发的
refresh_token有效期为7天,到期自动失效,和代码逻辑无关——这也是你每次换完token能用几天、到点就坏的核心原因。OAuth Playground默认生成的临时凭证本身也是7天有效期,和测试状态的规则一致。 - 代码存在两处不规范问题:
- 调用的token端点是已废弃的老地址
https://accounts.google.com/o/oauth2/token,和官方当前推荐的https://oauth2.googleapis.com/token存在校验逻辑差异,偶发会触发凭证校验异常 - 请求参数中多余传入了
project_id字段,该字段不属于token接口的合法入参,同时缺少表单请求必须的Content-Type头,错误场景下直接读取输入流会拿不到完整错误信息
- 调用的token端点是已废弃的老地址
修复步骤
- 第一步:进入GCP控制台的OAuth同意屏幕页面,将应用发布状态从「Testing」切换为「In production(生产)」。如果使用的scope不属于Google标注的敏感/受限类,不需要提交审核,切换操作即时生效。
- 第二步:状态切换完成后,重新生成一次
refresh_token写入配置,该状态下签发的refresh_token为长期有效,不会出现7天自动过期的问题,除非用户手动撤销授权、或者你重置了对应client的secret。 - 第三步:修正Java代码中的不规范逻辑,修正后参考代码如下:
private String getAccessToken() { try { credentials.put("grant_type", "refresh_token"); credentials.put("client_id", credential.get("client_id")); credentials.put("client_secret", credential.get("client_secret")); credentials.put("refresh_token", credential.get("refresh_token")); // 移除无效的project_id参数,token接口不需要该字段 StringBuilder postData = new StringBuilder(); for (Map.Entry<String, Object> param : credentials.entrySet()) { if (postData.length() != 0) { postData.append('&'); } postData.append(URLEncoder.encode(param.getKey(), "UTF-8")); postData.append('='); postData.append(URLEncoder.encode(String.valueOf(param.getValue()), "UTF-8")); } byte[] postDataBytes = postData.toString().getBytes("UTF-8"); // 替换为官方最新的token端点 URL url = new URL("https://oauth2.googleapis.com/token"); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setDoOutput(true); con.setUseCaches(false); con.setRequestMethod("POST"); // 补全表单请求头 con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); con.getOutputStream().write(postDataBytes); // 兼容错误场景的响应读取,避免4xx时直接抛异常拿不到错误信息 int responseCode = con.getResponseCode(); InputStream respStream = responseCode >= 400 ? con.getErrorStream() : con.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(respStream, "UTF-8")); StringBuffer buffer = new StringBuffer(); for (String line = reader.readLine(); line != null; line = reader.readLine()) { buffer.append(line); } JSONObject json = new JSONObject(buffer.toString()); return json.getString("access_token"); } catch (Exception ex) { log.error("Error on generating access token:"+ExceptionUtils.getFullStackTrace(ex)); } return null; }
额外注意事项
- 同一个Google账号对同一个OAuth client_id,最多只能留存50个有效
refresh_token,超过上限后最早生成的token会被Google静默作废,不要反复生成测试token覆盖配置避免正常token被挤掉。
内容的提问来源于stack exchange,提问作者mena
相关产品推荐
相关产品推荐

