概览
为了获取每位用户的访问令牌以调用 Google API,Google 提供了多个 JavaScript 库:
本指南介绍了如何从这些库迁移到 Google Identity Services 库。
按照本指南进行操作,您将:
- 将已废弃的平台库替换为 Identity Services 库
- 如果使用 API 客户端库,请移除已废弃的
gapi.auth2
模块及其方法和对象,并将其替换为 Identity Services 等效项。
如需了解 Identity Services JavaScript 库的变化,请参阅概览以及用户授权的工作原理以查看关键术语和概念。
如果您要查找针对用户注册和登录的身份验证,请改为参阅从 Google 登录功能迁移。
确定您的授权流程
有两种可能的用户授权流程:隐式和授权代码。
查看您的 Web 应用,以确定当前正在使用的授权流程类型。
表示您的 Web 应用正在使用隐式流程:
- 您的 Web 应用完全基于浏览器,没有后端平台。
- 必须存在用户才能调用 Google API;您的应用仅使用访问令牌,不需要刷新令牌。
- 您的 Web 应用加载了
apis.google.com/js/api.js
。 - 您的实现基于适用于客户端 Web 应用的 OAuth 2.0。
- 您的应用使用适用于 JavaScript 的 Google API 客户端库中的
gapi.client
或gapi.auth2
模块。
这表明您的 Web 应用正在使用授权代码流程:
您的实施基于:
您的应用既在用户的浏览器中又在后端平台上执行,
您的后端平台托管授权代码端点。
您的后端平台会代表用户调用 Google API,而无需其存在,也称为离线模式。
刷新令牌由您的后端平台管理和存储。
在某些情况下,您的代码库可能同时支持这两个流程。
选择授权流程
在开始迁移之前,您需要确定是继续使用现有流程还是采用其他流程最符合您的需求。
如需了解这两个流程之间的主要区别和权衡,请查看选择授权流程。
在大多数情况下,我们推荐使用授权代码流程,因为它可以提供最高的用户安全性。实现此流程后,您的平台还可以更轻松地添加新的离线功能,例如提取更新以通知用户其日历、照片、订阅等的重大变化。
使用下面的选择器选择授权流程。
隐式流
获取访问令牌,以便在用户存在时在浏览器中使用。
隐式流程示例展示了 Web 应用在迁移到 Identity Services 前后的情况。
授权代码流程
Google 发放的用户授权代码被传递到您的后端平台,然后在该平台上交换访问令牌和刷新令牌。
授权代码流程示例展示了 Web 应用在迁移到 Identity Services 之前和之后。
在本指南中,请按照以粗体列出的说明进行操作,以添加、移除、更新或替换现有功能。
对浏览器内 Web 应用的更改
本部分将介绍在迁移到 Google Identity 服务 JavaScript 库时,您将对浏览器内 Web 应用所做的更改。
确定受影响的代码和测试
调试 Cookie 可帮助您找到受影响的代码并测试弃用后的行为。
在大型或复杂的应用中,可能很难找到所有受 gapi.auth2
模块废弃影响的代码。如需将即将弃用的功能的现有使用情况记录到控制台,请将 G_AUTH2_MIGRATION
Cookie 的值设置为 informational
。(可选)添加一个英文冒号,后跟一个键值对,以便也将记录到会话存储空间中。登录并收到凭据后,请查看收集的日志或将收集的日志发送到后端以供日后分析。例如,informational:showauth2use
会将来源和网址保存到名为 showauth2use
的会话存储密钥中。
如需验证 gapi.auth2
模块不再加载时的应用行为,请将 G_AUTH2_MIGRATION
Cookie 的值设置为 enforced
。这样一来,您就可以在强制执行日期之前测试弃用后的行为。
可能的 G_AUTH2_MIGRATION
Cookie 值:
enforced
请勿加载gapi.auth2
模块。informational
将已弃用功能的使用情况记录到 JS 控制台中。设置可选键名称时,还应记录到会话存储空间:informational:key-name
。
为了最大限度地减少对用户的影响,建议您在开发和测试期间先在本地设置此 Cookie,然后再在生产环境中使用。
库和模块
gapi.auth2
模块管理登录用户身份验证和隐式授权流程,将这个已弃用的模块及其对象和方法替换为 Google Identity 服务库。
将 Identity Services 库添加到您的 Web 应用中,只需将其添加到文档中即可:
<script src="https://accounts.google.com/gsi/client" async defer></script>
移除使用 gapi.load('auth2',
function)
加载 auth2
模块的任何实例。
Google Identity Services 库取代了 gapi.auth2
模块。您可以放心地继续使用适用于 JavaScript 的 Google API 客户端库中的 gapi.client
模块,并利用该模块从发现文档自动创建 Callable JS 方法、批量处理多个 API 调用以及 CORS 管理功能。
饼干
用户授权不要求使用 Cookie。
如需详细了解用户身份验证如何使用 Cookie,以及 Google 如何使用 Cookie 来确定其他 Google 产品和服务使用的 Cookie,请参阅从 Google 登录机制迁移。
凭据
Google Identity 服务将用户身份验证和授权分为两种不同的操作,用户凭据也是分开的:用于标识用户的 ID 令牌与用于授权的访问令牌分开返回。
如需查看这些更改,请参阅示例凭据。
隐式流
通过从授权流程中移除用户个人资料处理,分离用户身份验证和授权。
请移除以下 Google 登录 JavaScript 客户端参考文档:
方法
GoogleUser.getBasicProfile()
GoogleUser.getId()
授权代码流程
Identity Services 将浏览器内凭据拆分为 ID 令牌和访问令牌。此更改不适用于从后端平台直接调用 Google OAuth 2.0 端点或通过在您平台上的安全服务器上运行的库(例如 Google API Node.js 客户端)获得的凭据。
会话状态
以前,Google 登录通过以下方式帮助您管理用户的登录状态:
您负责管理 Web 应用的登录状态和用户会话。
请移除以下 Google 登录 JavaScript 客户端参考文档:
对象:
gapi.auth2.SignInOptions
方法:
GoogleAuth.attachClickHandler()
GoogleAuth.isSignedIn()
GoogleAuth.isSignedIn.get()
GoogleAuth.isSignedIn.listen()
GoogleAuth.signIn()
GoogleAuth.signOut()
GoogleAuth.currentUser.get()
GoogleAuth.currentUser.listen()
GoogleUser.isSignedIn()
客户端配置
更新您的 Web 应用,以针对隐式或授权代码流初始化令牌客户端。
请移除以下 Google 登录 JavaScript 客户端参考文档:
对象:
gapi.auth2.ClientConfig
gapi.auth2.OfflineAccessOptions
方法:
gapi.auth2.getAuthInstance()
GoogleUser.grant()
隐式流
按照初始化令牌客户端中的示例,添加 TokenClientConfig
对象和 initTokenClient()
调用以配置您的 Web 应用。
将“Google 登录 JavaScript 客户端引用”替换为“Google 身份服务”:
对象:
gapi.auth2.AuthorizeConfig
与TokenClientConfig
合作
方法:
gapi.auth2.init()
与google.accounts.oauth2.initTokenClient()
合作
参数:
- 使用
TokenClientConfig.login_hint
调用gapi.auth2.AuthorizeConfig.login_hint
。 - 使用
TokenClientConfig.hd
调用gapi.auth2.GoogleUser.getHostedDomain()
。
授权代码流程
按照初始化代码客户端中的示例,添加 CodeClientConfig
对象和 initCodeClient()
调用以配置您的 Web 应用。
从隐式流程切换到授权代码流程时:
对象:
gapi.auth2.AuthorizeConfig
方法:
gapi.auth2.init()
参数:
gapi.auth2.AuthorizeConfig.login_hint
gapi.auth2.GoogleUser.getHostedDomain()
令牌请求
用户手势(例如点击按钮)生成的请求会导致访问令牌通过隐式流程直接返回用户浏览器,或者在将每位用户的授权代码交换为访问令牌和刷新令牌后返回至后端平台。
隐式流
当用户已登录帐号并与 Google 保持活跃会话时,可以在浏览器中获取并使用访问令牌。对于隐式模式,即使之前已请求过访问令牌,用户也需要做出手势来请求访问令牌。
将“Google 登录 JavaScript 客户端引用”替换为“Google 身份服务”。
方法:
gapi.auth2.authorize()
与TokenClient.requestAccessToken()
合作- 使用
TokenClient.requestAccessToken()
调用GoogleUser.reloadAuthResponse()
添加链接或按钮以调用 requestAccessToken()
,从而启动弹出式用户体验流程以请求访问令牌,或在现有令牌到期时获取新令牌。
更新您的代码库以执行以下操作:
- 使用
requestAccessToken()
触发 OAuth 2.0 令牌流程。 - 支持增量授权,方法是使用
requestAccessToken
和OverridableTokenClientConfig
将针对多个范围的一个请求拆分为多个较小的请求。 - 当现有令牌过期或被撤消时,请求新令牌。
使用多个范围可能需要对代码库进行结构性更改,以便仅在需要时请求对这些范围的访问权限,而不是一次性请求访问,这称为增量授权。每个请求都应包含尽可能少的范围,最好包含单个范围。如需详细了解如何更新应用以获取增量授权,请参阅如何处理用户同意声明。
当访问令牌到期时,gapi.auth2
模块会自动为您的 Web 应用获取一个新的有效访问令牌。为了提高用户安全性,Google Identity 服务库不支持这种自动令牌刷新过程。您必须更新 Web 应用,以检测过期的访问令牌并请求新的访问令牌。有关详情,请参阅下文的“令牌处理”部分。
授权代码流程
添加链接或按钮来调用 requestCode()
,以请求 Google 提供授权代码。如需查看示例,请参阅触发 OAuth 2.0 代码流程。
请参阅下面的“令牌处理”部分,详细了解如何响应过期或撤消的访问令牌。
令牌处理
添加了错误处理功能,以便在使用过期或已撤消的访问令牌时检测失败的 Google API 调用,并请求有效的新访问令牌。
使用过期或撤消的访问令牌时,Google API 会返回 HTTP 状态代码 401 Unauthorized
和 invalid_token
错误消息。如需查看示例,请参阅无效的令牌响应。
过期令牌
访问令牌属于短期令牌,通常仅在几分钟内有效。
令牌撤消
Google 帐号所有者可以随时撤消之前授予的同意。这样做会导致现有访问令牌和刷新令牌失效。您可以使用 revoke()
或通过 Google 帐号从您的平台触发撤消操作。
将“Google 登录 JavaScript 客户端引用”替换为“Google 身份服务”。
方法:
getAuthInstance().disconnect()
与google.accounts.oauth2.revoke()
合作GoogleUser.disconnect()
与google.accounts.oauth2.revoke()
合作
当用户在您的平台上删除其帐号或希望取消同意与您的应用共享数据时,调用 revoke
。
用户意见征求提示
当您的 Web 应用或后端平台请求访问令牌时,Google 会向用户显示意见征求对话框。查看 Google 向用户显示的意见征求对话框示例。
在向您的应用发出访问令牌之前,需要有一个现有的活跃 Google 会话来提示用户同意并记录结果。如果尚未建立现有会话,用户可能需要登录 Google 帐号。
用户登录
用户既可以在单独的浏览器标签页中登录 Google 帐号,也可以在浏览器或操作系统中直接登录。我们建议在网站中添加使用 Google 帐号登录,以便在用户首次打开您的应用时,在 Google 帐号和浏览器之间建立活跃的会话。这样做具有以下优势:
- 尽可能减少用户必须登录的次数,在尚未存在活跃会话的情况下,请求访问令牌会启动 Google 帐号登录流程。
- 直接将 JWT ID 令牌凭据
email
字段用作CodeClientConfig
或TokenClientConfig
对象中login_hint
参数的值。如果您的平台没有维护用户帐号管理系统,此功能尤其有用。 - 查找 Google 帐号并将其与平台上现有的本地用户帐号相关联,以帮助最大限度地减少平台上的重复帐号。
- 创建新的本地帐号后,您的注册对话框和流程可以与用户身份验证对话框和流程明确区分开来,从而减少所需的步骤数量并提高访问者流失率。
登录之后到访问令牌发放之前,用户必须同意您的应用访问所请求的范围。
令牌和意见征求响应
同意后,系统将返回访问令牌以及用户批准或拒绝的范围列表。
借助细化的权限,用户可以批准或拒绝各个范围。请求访问多个范围时,每个范围均会被授予或拒绝,而不受其他范围的影响。您的应用根据用户的选择,选择性地启用基于单个作用域的特性和功能。
隐式流
将“Google 登录 JavaScript 客户端引用”替换为“Google 身份服务”:
对象:
gapi.auth2.AuthorizeResponse
与TokenClient.TokenResponse
合作gapi.auth2.AuthResponse
与TokenClient.TokenResponse
合作
方法:
- 使用
google.accounts.oauth2.hasGrantedAllScopes()
调用GoogleUser.hasGrantedScopes()
- 使用
google.accounts.oauth2.hasGrantedAllScopes()
调用GoogleUser.getGrantedScopes()
移除 “Google 登录 JavaScript”客户端参考:
方法:
GoogleUser.getAuthResponse()
请按照此细化权限示例,使用 hasGrantedAllScopes()
和 hasGrantedAnyScope()
更新您的 Web 应用。
授权代码流程
按照身份验证代码处理中的说明,将授权代码端点更新或添加到您的后端平台。
更新您的平台,按照使用代码模型指南中所述的步骤验证请求,并获取访问令牌和刷新令牌。
按照增量授权说明和检查用户授予的访问权限范围中的说明,更新您的平台,以根据用户批准的各个范围选择性地启用或停用特性和功能。
隐式流示例
老方法
GAPI 客户端库
使用弹出式对话框征求用户同意的弹出式对话框在浏览器中运行的适用于 JavaScript 的 Google API 客户端库示例。
gapi.client.init()
会自动加载和使用 gapi.auth2
模块,因此处于隐藏状态。
<!DOCTYPE html>
<html>
<head>
<script src="https://apis.google.com/js/api.js"></script>
<script>
function start() {
gapi.client.init({
'apiKey': 'YOUR_API_KEY',
'clientId': 'YOUR_CLIENT_ID',
'scope': 'https://www.googleapis.com/auth/cloud-translation',
'discoveryDocs': ['https://www.googleapis.com/discovery/v1/apis/translate/v2/rest'],
}).then(function() {
// Execute an API request which is returned as a Promise.
// The method name language.translations.list comes from the API discovery.
return gapi.client.language.translations.list({
q: 'hello world',
source: 'en',
target: 'de',
});
}).then(function(response) {
console.log(response.result.data.translations[0].translatedText);
}, function(reason) {
console.log('Error: ' + reason.result.error.message);
});
};
// Load the JavaScript client library and invoke start afterwards.
gapi.load('client', start);
</script>
</head>
<body>
<div id="results"></div>
</body>
</html>
JS 客户端库
使用弹出式对话框征求用户同意,在浏览器中运行适用于客户端 Web 应用的 OAuth 2.0。
手动加载 gapi.auth2
模块。
<!DOCTYPE html>
<html><head></head><body>
<script>
var GoogleAuth;
var SCOPE = 'https://www.googleapis.com/auth/drive.metadata.readonly';
function handleClientLoad() {
// Load the API's client and auth2 modules.
// Call the initClient function after the modules load.
gapi.load('client:auth2', initClient);
}
function initClient() {
// In practice, your app can retrieve one or more discovery documents.
var discoveryUrl = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest';
// Initialize the gapi.client object, which app uses to make API requests.
// Get API key and client ID from API Console.
// 'scope' field specifies space-delimited list of access scopes.
gapi.client.init({
'apiKey': 'YOUR_API_KEY',
'clientId': 'YOUR_CLIENT_ID',
'discoveryDocs': [discoveryUrl],
'scope': SCOPE
}).then(function () {
GoogleAuth = gapi.auth2.getAuthInstance();
// Listen for sign-in state changes.
GoogleAuth.isSignedIn.listen(updateSigninStatus);
// Handle initial sign-in state. (Determine if user is already signed in.)
var user = GoogleAuth.currentUser.get();
setSigninStatus();
// Call handleAuthClick function when user clicks on
// "Sign In/Authorize" button.
$('#sign-in-or-out-button').click(function() {
handleAuthClick();
});
$('#revoke-access-button').click(function() {
revokeAccess();
});
});
}
function handleAuthClick() {
if (GoogleAuth.isSignedIn.get()) {
// User is authorized and has clicked "Sign out" button.
GoogleAuth.signOut();
} else {
// User is not signed in. Start Google auth flow.
GoogleAuth.signIn();
}
}
function revokeAccess() {
GoogleAuth.disconnect();
}
function setSigninStatus() {
var user = GoogleAuth.currentUser.get();
var isAuthorized = user.hasGrantedScopes(SCOPE);
if (isAuthorized) {
$('#sign-in-or-out-button').html('Sign out');
$('#revoke-access-button').css('display', 'inline-block');
$('#auth-status').html('You are currently signed in and have granted ' +
'access to this app.');
} else {
$('#sign-in-or-out-button').html('Sign In/Authorize');
$('#revoke-access-button').css('display', 'none');
$('#auth-status').html('You have not authorized this app or you are ' +
'signed out.');
}
}
function updateSigninStatus() {
setSigninStatus();
}
</script>
<button id="sign-in-or-out-button"
style="margin-left: 25px">Sign In/Authorize</button>
<button id="revoke-access-button"
style="display: none; margin-left: 25px">Revoke access</button>
<div id="auth-status" style="display: inline; padding-left: 25px"></div><hr>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script async defer src="https://apis.google.com/js/api.js"
onload="this.onload=function(){};handleClientLoad()"
onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
</body></html>
OAuth 2.0 端点
OAuth 2.0 for Client-side Web 应用 - 使用重定向到 Google 征求用户同意,在浏览器中运行。
以下示例展示了如何从用户浏览器直接调用 Google 的 OAuth 2.0 端点,并且未使用 gapi.auth2
模块或 JavaScript 库。
<!DOCTYPE html>
<html><head></head><body>
<script>
var YOUR_CLIENT_ID = 'REPLACE_THIS_VALUE';
var YOUR_REDIRECT_URI = 'REPLACE_THIS_VALUE';
var fragmentString = location.hash.substring(1);
// Parse query string to see if page request is coming from OAuth 2.0 server.
var params = {};
var regex = /([^&=]+)=([^&]*)/g, m;
while (m = regex.exec(fragmentString)) {
params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
if (Object.keys(params).length > 0) {
localStorage.setItem('oauth2-test-params', JSON.stringify(params) );
if (params['state'] && params['state'] == 'try_sample_request') {
trySampleRequest();
}
}
// If there's an access token, try an API request.
// Otherwise, start OAuth 2.0 flow.
function trySampleRequest() {
var params = JSON.parse(localStorage.getItem('oauth2-test-params'));
if (params && params['access_token']) {
var xhr = new XMLHttpRequest();
xhr.open('GET',
'https://www.googleapis.com/drive/v3/about?fields=user&' +
'access_token=' + params['access_token']);
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.response);
} else if (xhr.readyState === 4 && xhr.status === 401) {
// Token invalid, so prompt for user permission.
oauth2SignIn();
}
};
xhr.send(null);
} else {
oauth2SignIn();
}
}
/*
* Create form to request access token from Google's OAuth 2.0 server.
*/
function oauth2SignIn() {
// Google's OAuth 2.0 endpoint for requesting an access token
var oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
// Create element to open OAuth 2.0 endpoint in new window.
var form = document.createElement('form');
form.setAttribute('method', 'GET'); // Send as a GET request.
form.setAttribute('action', oauth2Endpoint);
// Parameters to pass to OAuth 2.0 endpoint.
var params = {'client_id': YOUR_CLIENT_ID,
'redirect_uri': YOUR_REDIRECT_URI,
'scope': 'https://www.googleapis.com/auth/drive.metadata.readonly',
'state': 'try_sample_request',
'include_granted_scopes': 'true',
'response_type': 'token'};
// Add form parameters as hidden input values.
for (var p in params) {
var input = document.createElement('input');
input.setAttribute('type', 'hidden');
input.setAttribute('name', p);
input.setAttribute('value', params[p]);
form.appendChild(input);
}
// Add form to page and submit it to open the OAuth 2.0 endpoint.
document.body.appendChild(form);
form.submit();
}
</script>
<button onclick="trySampleRequest();">Try sample request</button>
</body></html>
全新方式
仅限 GIS
此示例仅显示使用令牌模型的 Google Identity 服务 JavaScript 库和用于征求用户同意的弹出式对话框。本页面旨在说明配置客户端、请求和获取访问令牌以及调用 Google API 所需的最少步骤。
<!DOCTYPE html>
<html>
<head>
<script src="https://accounts.google.com/gsi/client" onload="initClient()" async defer></script>
</head>
<body>
<script>
var client;
var access_token;
function initClient() {
client = google.accounts.oauth2.initTokenClient({
client_id: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/calendar.readonly \
https://www.googleapis.com/auth/contacts.readonly',
callback: (tokenResponse) => {
access_token = tokenResponse.access_token;
},
});
}
function getToken() {
client.requestAccessToken();
}
function revokeToken() {
google.accounts.oauth2.revoke(access_token, () => {console.log('access token revoked')});
}
function loadCalendar() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.googleapis.com/calendar/v3/calendars/primary/events');
xhr.setRequestHeader('Authorization', 'Bearer ' + access_token);
xhr.send();
}
</script>
<h1>Google Identity Services Authorization Token model</h1>
<button onclick="getToken();">Get access token</button><br><br>
<button onclick="loadCalendar();">Load Calendar</button><br><br>
<button onclick="revokeToken();">Revoke token</button>
</body>
</html>
GAPI 异步/等待
此示例展示了如何使用令牌模型添加 Google Identity 服务库、移除 gapi.auth2
模块,以及使用 JavaScript 版 Google API 客户端库调用 API。
promise、async 和 await 用于强制执行库加载顺序以及捕获和重试授权错误。只有在获得有效的访问令牌后,才会进行 API 调用。
首次加载页面时或访问令牌过期后,若访问令牌缺失,用户应按“显示日历”按钮。
<!DOCTYPE html>
<html>
<head></head>
<body>
<h1>GAPI with GIS async/await</h1>
<button id="showEventsBtn" onclick="showEvents();">Show Calendar</button><br><br>
<button id="revokeBtn" onclick="revokeToken();">Revoke access token</button>
<script>
const gapiLoadPromise = new Promise((resolve, reject) => {
gapiLoadOkay = resolve;
gapiLoadFail = reject;
});
const gisLoadPromise = new Promise((resolve, reject) => {
gisLoadOkay = resolve;
gisLoadFail = reject;
});
var tokenClient;
(async () => {
document.getElementById("showEventsBtn").style.visibility="hidden";
document.getElementById("revokeBtn").style.visibility="hidden";
// First, load and initialize the gapi.client
await gapiLoadPromise;
await new Promise((resolve, reject) => {
// NOTE: the 'auth2' module is no longer loaded.
gapi.load('client', {callback: resolve, onerror: reject});
});
await gapi.client.init({
// NOTE: OAuth2 'scope' and 'client_id' parameters have moved to initTokenClient().
})
.then(function() { // Load the Calendar API discovery document.
gapi.client.load('https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest');
});
// Now load the GIS client
await gisLoadPromise;
await new Promise((resolve, reject) => {
try {
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/calendar.readonly',
prompt: 'consent',
callback: '', // defined at request time in await/promise scope.
});
resolve();
} catch (err) {
reject(err);
}
});
document.getElementById("showEventsBtn").style.visibility="visible";
document.getElementById("revokeBtn").style.visibility="visible";
})();
async function getToken(err) {
if (err.result.error.code == 401 || (err.result.error.code == 403) &&
(err.result.error.status == "PERMISSION_DENIED")) {
// The access token is missing, invalid, or expired, prompt for user consent to obtain one.
await new Promise((resolve, reject) => {
try {
// Settle this promise in the response callback for requestAccessToken()
tokenClient.callback = (resp) => {
if (resp.error !== undefined) {
reject(resp);
}
// GIS has automatically updated gapi.client with the newly issued access token.
console.log('gapi.client access token: ' + JSON.stringify(gapi.client.getToken()));
resolve(resp);
};
tokenClient.requestAccessToken();
} catch (err) {
console.log(err)
}
});
} else {
// Errors unrelated to authorization: server errors, exceeding quota, bad requests, and so on.
throw new Error(err);
}
}
function showEvents() {
// Try to fetch a list of Calendar events. If a valid access token is needed,
// prompt to obtain one and then retry the original request.
gapi.client.calendar.events.list({ 'calendarId': 'primary' })
.then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
.catch(err => getToken(err)) // for authorization errors obtain an access token
.then(retry => gapi.client.calendar.events.list({ 'calendarId': 'primary' }))
.then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
.catch(err => console.log(err)); // cancelled by user, timeout, etc.
}
function revokeToken() {
let cred = gapi.client.getToken();
if (cred !== null) {
google.accounts.oauth2.revoke(cred.access_token, () => {console.log('Revoked: ' + cred.access_token)});
gapi.client.setToken('');
}
}
</script>
<script async defer src="https://apis.google.com/js/api.js" onload="gapiLoadOkay()" onerror="gapiLoadFail(event)"></script>
<script async defer src="https://accounts.google.com/gsi/client" onload="gisLoadOkay()" onerror="gisLoadFail(event)"></script>
</body>
</html>
GAPI 回调
此示例展示了如何使用令牌模型添加 Google Identity 服务库、移除 gapi.auth2
模块,以及使用 JavaScript 版 Google API 客户端库调用 API。
变量用于强制执行库加载顺序。GAPI 调用是在返回有效的访问令牌后从回调 中进行的 GAPI 调用。
用户应在页面首次加载时按“显示日历”按钮,并在要刷新日历信息时再次按下。
<!DOCTYPE html>
<html>
<head>
<script async defer src="https://apis.google.com/js/api.js" onload="gapiLoad()"></script>
<script async defer src="https://accounts.google.com/gsi/client" onload="gisInit()"></script>
</head>
<body>
<h1>GAPI with GIS callbacks</h1>
<button id="showEventsBtn" onclick="showEvents();">Show Calendar</button><br><br>
<button id="revokeBtn" onclick="revokeToken();">Revoke access token</button>
<script>
let tokenClient;
let gapiInited;
let gisInited;
document.getElementById("showEventsBtn").style.visibility="hidden";
document.getElementById("revokeBtn").style.visibility="hidden";
function checkBeforeStart() {
if (gapiInited && gisInited){
// Start only when both gapi and gis are initialized.
document.getElementById("showEventsBtn").style.visibility="visible";
document.getElementById("revokeBtn").style.visibility="visible";
}
}
function gapiInit() {
gapi.client.init({
// NOTE: OAuth2 'scope' and 'client_id' parameters have moved to initTokenClient().
})
.then(function() { // Load the Calendar API discovery document.
gapi.client.load('https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest');
gapiInited = true;
checkBeforeStart();
});
}
function gapiLoad() {
gapi.load('client', gapiInit)
}
function gisInit() {
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/calendar.readonly',
callback: '', // defined at request time
});
gisInited = true;
checkBeforeStart();
}
function showEvents() {
tokenClient.callback = (resp) => {
if (resp.error !== undefined) {
throw(resp);
}
// GIS has automatically updated gapi.client with the newly issued access token.
console.log('gapi.client access token: ' + JSON.stringify(gapi.client.getToken()));
gapi.client.calendar.events.list({ 'calendarId': 'primary' })
.then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
.catch(err => console.log(err));
document.getElementById("showEventsBtn").innerText = "Refresh Calendar";
}
// Conditionally ask users to select the Google Account they'd like to use,
// and explicitly obtain their consent to fetch their Calendar.
// NOTE: To request an access token a user gesture is necessary.
if (gapi.client.getToken() === null) {
// Prompt the user to select a Google Account and asked for consent to share their data
// when establishing a new session.
tokenClient.requestAccessToken({prompt: 'consent'});
} else {
// Skip display of account chooser and consent dialog for an existing session.
tokenClient.requestAccessToken({prompt: ''});
}
}
function revokeToken() {
let cred = gapi.client.getToken();
if (cred !== null) {
google.accounts.oauth2.revoke(cred.access_token, () => {console.log('Revoked: ' + cred.access_token)});
gapi.client.setToken('');
document.getElementById("showEventsBtn").innerText = "Show Calendar";
}
}
</script>
</body>
</html>
授权代码流程示例
Google Identity 服务库的弹出式窗口用户体验可以使用网址重定向直接向您的后端令牌端点返回授权代码,也可以使用在用户浏览器中运行的 JavaScript 回调处理程序(将响应代理到您的平台)。无论是哪种情况,您的后端平台都会完成 OAuth 2.0 流程,以获取有效的刷新和访问令牌。
老方法
服务器端 Web 应用
适用于在后端平台上运行的服务器端应用的 Google 登录功能,使用重定向至 Google 的用户同意机制。
<!DOCTYPE html>
<html>
<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="https://apis.google.com/js/client:platform.js?onload=start" async defer></script>
<script>
function start() {
gapi.load('auth2', function() {
auth2 = gapi.auth2.init({
client_id: 'YOUR_CLIENT_ID',
api_key: 'YOUR_API_KEY',
discovery_docs: ['https://www.googleapis.com/discovery/v1/apis/translate/v2/rest'],
// Scopes to request in addition to 'profile' and 'email'
scope: 'https://www.googleapis.com/auth/cloud-translation',
});
});
}
function signInCallback(authResult) {
if (authResult['code']) {
console.log("sending AJAX request");
// Send authorization code obtained from Google to backend platform
$.ajax({
type: 'POST',
url: 'YOUR_AUTHORIZATION_CODE_ENDPOINT_URL',
// Always include an X-Requested-With header to protect against CSRF attacks.
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
contentType: 'application/octet-stream; charset=utf-8',
success: function(result) {
console.log(result);
},
processData: false,
data: authResult['code']
});
} else {
console.log('error: failed to obtain authorization code')
}
}
</script>
</head>
<body>
<button id="signinButton">Sign In With Google</button>
<script>
$('#signinButton').click(function() {
// Obtain an authorization code from Google
auth2.grantOfflineAccess().then(signInCallback);
});
</script>
</body>
</html>
使用重定向的 HTTP/REST
使用适用于 Web 服务器应用的 OAuth 2.0 将授权代码从用户的浏览器发送到您的后端平台。通过将用户的浏览器重定向到 Google 来处理用户意见征求。
/\*
\* Create form to request access token from Google's OAuth 2.0 server.
\*/
function oauthSignIn() {
// Google's OAuth 2.0 endpoint for requesting an access token
var oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
// Create <form> element to submit parameters to OAuth 2.0 endpoint.
var form = document.createElement('form');
form.setAttribute('method', 'GET'); // Send as a GET request.
form.setAttribute('action', oauth2Endpoint);
// Parameters to pass to OAuth 2.0 endpoint.
var params = {'client\_id': 'YOUR_CLIENT_ID',
'redirect\_uri': 'YOUR_AUTHORIZATION_CODE_ENDPOINT_URL',
'response\_type': 'token',
'scope': 'https://www.googleapis.com/auth/drive.metadata.readonly',
'include\_granted\_scopes': 'true',
'state': 'pass-through value'};
// Add form parameters as hidden input values.
for (var p in params) {
var input = document.createElement('input');
input.setAttribute('type', 'hidden');
input.setAttribute('name', p);
input.setAttribute('value', params[p]);
form.appendChild(input);
}
// Add form to page and submit it to open the OAuth 2.0 endpoint.
document.body.appendChild(form);
form.submit();
}
全新方式
GIS 弹出式用户体验
此示例仅显示使用授权代码模型的 Google Identity 服务 JavaScript 库,其中包含一个用于征求用户同意的弹出式对话框以及用于接收 Google 授权代码的回调处理程序。本页面旨在说明配置客户端、征得用户同意以及向后端平台发送授权代码所需的最少步骤。
<!DOCTYPE html>
<html>
<head>
<script src="https://accounts.google.com/gsi/client" onload="initClient()" async defer></script>
</head>
<body>
<script>
var client;
function initClient() {
client = google.accounts.oauth2.initCodeClient({
client_id: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/calendar.readonly',
ux_mode: 'popup',
callback: (response) => {
var code_receiver_uri = 'YOUR_AUTHORIZATION_CODE_ENDPOINT_URI',
// Send auth code to your backend platform
const xhr = new XMLHttpRequest();
xhr.open('POST', code_receiver_uri, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onload = function() {
console.log('Signed in as: ' + xhr.responseText);
};
xhr.send('code=' + response.code);
// After receipt, the code is exchanged for an access token and
// refresh token, and the platform then updates this web app
// running in user's browser with the requested calendar info.
},
});
}
function getAuthCode() {
// Request authorization code and obtain user consent
client.requestCode();
}
</script>
<button onclick="getAuthCode();">Load Your Calendar</button>
</body>
</html>
GIS 重定向用户体验
授权代码模型支持弹出和重定向用户体验模式,以便将每位用户的授权代码发送到您的平台托管的端点。重定向用户体验模式如下所示:
<!DOCTYPE html>
<html>
<head>
<script src="https://accounts.google.com/gsi/client" onload="initClient()" async defer></script>
</head>
<body>
<script>
var client;
function initClient() {
client = google.accounts.oauth2.initCodeClient({
client_id: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/calendar.readonly \
https://www.googleapis.com/auth/photoslibrary.readonly',
ux_mode: 'redirect',
redirect_uri: 'YOUR_AUTHORIZATION_CODE_ENDPOINT_URI'
});
}
// Request an access token
function getAuthCode() {
// Request authorization code and obtain user consent
client.requestCode();
}
</script>
<button onclick="getAuthCode();">Load Your Calendar</button>
</body>
</html>
JavaScript 库
Google Identity 服务是一个用于用户身份验证和授权的 JavaScript 库,整合并替换了多个不同库和模块中的特性和功能:
迁移到 Identity 服务时要执行的操作:
现有 JS 库 | 新的 JS 库 | 备注 |
---|---|---|
apis.google.com/js/api.js |
accounts.google.com/gsi/client |
添加新库并按照隐式流程操作。 |
apis.google.com/js/client.js |
accounts.google.com/gsi/client |
添加新库和授权代码流程。 |
库快速参考
旧版 Google 登录 JavaScript 客户端库与新版 Google Identity 服务库以及备注之间的对象和方法比较,提供了迁移期间需要执行的其他信息和操作。
旧优惠 | 新观看者 | 备注 |
---|---|---|
GoogleAuth 对象及相关方法: | ||
GoogleAuth.attachmentClickHandler() | 移除 | |
GoogleAuth.currentUser.get() | 移除 | |
GoogleAuth.currentUser.listen() | 移除 | |
GoogleAuth.disconnect() | google.accounts.oauth2.revoke | 用新旧替换。用户还可能会通过 https://myaccount.google.com/permissions 进行撤消 |
GoogleAuth.grantOffAccess() | 移除,按照授权代码流程操作。 | |
GoogleAuth.isSignedIn.get() | 移除 | |
GoogleAuth.isSignedIn.listen() | 移除 | |
GoogleAuth.signIn() | 移除 | |
GoogleAuth.signOut() | 移除 | |
GoogleAuth.then() | 移除 | |
GoogleUser 对象及相关方法: | ||
GoogleUser.disconnect() | google.accounts.id.revoke | 用新旧替换。用户还可能会通过 https://myaccount.google.com/permissions 进行撤消 |
GoogleUser.getAuthResponse() | requestCode() 或 requestAccessToken() | 用新旧替换 |
GoogleUser.getBasicProfile() | 移除。请改用 ID 令牌,请参阅从 Google 登录功能迁移。 | |
GoogleUser.getgrantedScopes() | hasgrantedAnyScope() | 用新旧替换 |
GoogleUser.getHostedDomain() | 移除 | |
GoogleUser.getId() | 移除 | |
GoogleUser.grantofflineAccess() | 移除,按照授权代码流程操作。 | |
GoogleUser.grant() | 移除 | |
GoogleUser.hasgrantedScopes() | hasgrantedAnyScope() | 用新旧替换 |
GoogleUser.isSignedIn() | 移除 | |
GoogleUser.reloadAuthResponse() | requestAccessToken() | 移除旧的访问令牌,调用新的方法,替换过期或已撤消的访问令牌。 |
gapi.auth2 对象和关联的方法: | ||
gapi.auth2.AuthorizeConfig 对象 | TokenClientConfig 或 CodeClientConfig | 用新旧替换 |
gapi.auth2.AuthorizeResponse 对象 | 移除 | |
gapi.auth2.AuthResponse 对象 | 移除 | |
gapi.auth2.authorize() | requestCode() 或 requestAccessToken() | 用新旧替换 |
gapi.auth2.ClientConfig() | TokenClientConfig 或 CodeClientConfig | 用新旧替换 |
gapi.auth2.getAuthInstance() | 移除 | |
gapi.auth2.init() | initTokenClient() 或 initCodeClient() | 用新旧替换 |
gapi.auth2.offlineAccessOptions 对象 | 移除 | |
gapi.auth2.SignInOptions 对象 | 移除 | |
gapi.signin2 对象和关联的方法: | ||
gapi.signin2.render() | 移除。加载 g_id_signin 元素的 HTML DOM 或对 google.accounts.id.renderButton 的 JS 调用会触发用户登录 Google 帐号。 |
凭据示例
现有凭据
Google 登录平台库、JavaScript 版 Google API 客户端库或对 Google Auth 2.0 端点的直接调用会在单个响应中同时返回 OAuth 2.0 访问令牌和 OpenID Connect ID 令牌。
同时包含 access_token
和 id_token
的响应示例:
{
"token_type": "Bearer",
"access_token": "ya29.A0ARrdaM-SmArZaCIh68qXsZSzyeU-8mxhQERHrP2EXtxpUuZ-3oW8IW7a6D2J6lRnZrRj8S6-ZcIl5XVEqnqxq5fuMeDDH_6MZgQ5dgP7moY-yTiKR5kdPm-LkuPM-mOtUsylWPd1wpRmvw_AGOZ1UUCa6UD5Hg",
"scope": "https://www.googleapis.com/auth/calendar.readonly",
"login_hint": "AJDLj6I2d1RH77cgpe__DdEree1zxHjZJr4Q7yOisoumTZUmo5W2ZmVFHyAomUYzLkrluG-hqt4RnNxrPhArd5y6p8kzO0t8xIfMAe6yhztt6v2E-_Bb4Ec3GLFKikHSXNh5bI-gPrsI",
"expires_in": 3599,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjkzNDFhYmM0MDkyYjZmYzAzOGU0MDNjOTEwMjJkZDNlNDQ1MzliNTYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiNTM4MzQ0NjUzMjU1LTc1OGM1aDVpc2M0NXZnazI3ZDhoOGRlYWJvdnBnNnRvLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNTM4MzQ0NjUzMjU1LTc1OGM1aDVpc2M0NXZnazI3ZDhoOGRlYWJvdnBnNnRvLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTE3NzI2NDMxNjUxOTQzNjk4NjAwIiwiaGQiOiJnb29nbGUuY29tIiwiZW1haWwiOiJkYWJyaWFuQGdvb2dsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IkJBSW55TjN2MS1ZejNLQnJUMVo0ckEiLCJuYW1lIjoiQnJpYW4gRGF1Z2hlcnR5IiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hLS9BT2gxNEdnenAyTXNGRGZvbVdMX3VDemRYUWNzeVM3ZGtxTE5ybk90S0QzVXNRPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6IkJyaWFuIiwiZmFtaWx5X25hbWUiOiJEYXVnaGVydHkiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTYzODk5MTYzOCwiZXhwIjoxNjM4OTk1MjM4LCJqdGkiOiI5YmRkZjE1YWFiNzE2ZDhjYmJmNDYwMmM1YWM3YzViN2VhMDQ5OTA5In0.K3EA-3Adw5HA7O8nJVCsX1HmGWxWzYk3P7ViVBb4H4BoT2-HIgxKlx1mi6jSxIUJGEekjw9MC-nL1B9Asgv1vXTMgoGaNna0UoEHYitySI23E5jaMkExkTSLtxI-ih2tJrA2ggfA9Ekj-JFiMc6MuJnwcfBTlsYWRcZOYVw3QpdTZ_VYfhUu-yERAElZCjaAyEXLtVQegRe-ymScra3r9S92TA33ylMb3WDTlfmDpWL0CDdDzby2asXYpl6GQ7SdSj64s49Yw6mdGELZn5WoJqG7Zr2KwIGXJuSxEo-wGbzxNK-mKAiABcFpYP4KHPEUgYyz3n9Vqn2Tfrgp-g65BQ",
"session_state": {
"extraQueryParams": {
"authuser": "0"
}
},
"first_issued_at": 1638991637982,
"expires_at": 1638995236982,
"idpId": "google"
}
Google Identity 服务凭据
Google Identity 服务库会返回以下内容:
使用访问令牌进行授权时:
{ "access_token": "ya29.A0ARrdaM_LWSO-uckLj7IJVNSfnUityT0Xj-UCCrGxFQdxmLiWuAosnAKMVQ2Z0LLqeZdeJii3TgULp6hR_PJxnInBOl8UoUwWoqsrGQ7-swxgy97E8_hnzfhrOWyQBmH6zs0_sUCzwzhEr_FAVqf92sZZHphr0g", "token_type": "Bearer", "expires_in": 3599, "scope": "https://www.googleapis.com/auth/calendar.readonly" }
或用于身份验证时的 ID 令牌:
{ "clientId": "538344653255-758c5h5isc45vgk27d8h8deabovpg6to.apps.googleusercontent.com", "credential": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImMxODkyZWI0OWQ3ZWY5YWRmOGIyZTE0YzA1Y2EwZDAzMjcxNGEyMzciLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJuYmYiOjE2MzkxNTcyNjQsImF1ZCI6IjUzODM0NDY1MzI1NS03NThjNWg1aXNjNDV2Z2syN2Q4aDhkZWFib3ZwZzZ0by5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExNzcyNjQzMTY1MTk0MzY5ODYwMCIsIm5vbmNlIjoiZm9vYmFyIiwiaGQiOiJnb29nbGUuY29tIiwiZW1haWwiOiJkYWJyaWFuQGdvb2dsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXpwIjoiNTM4MzQ0NjUzMjU1LTc1OGM1aDVpc2M0NXZnazI3ZDhoOGRlYWJvdnBnNnRvLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwibmFtZSI6IkJyaWFuIERhdWdoZXJ0eSIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQU9oMTRHZ3pwMk1zRkRmb21XTF91Q3pkWFFjc3lTN2RrcUxOcm5PdEtEM1VzUT1zOTYtYyIsImdpdmVuX25hbWUiOiJCcmlhbiIsImZhbWlseV9uYW1lIjoiRGF1Z2hlcnR5IiwiaWF0IjoxNjM5MTU3NTY0LCJleHAiOjE2MzkxNjExNjQsImp0aSI6IjRiOTVkYjAyZjU4NDczMmUxZGJkOTY2NWJiMWYzY2VhYzgyMmI0NjUifQ.Cr-AgMsLFeLurnqyGpw0hSomjOCU4S3cU669Hyi4VsbqnAV11zc_z73o6ahe9Nqc26kPVCNRGSqYrDZPfRyTnV6g1PIgc4Zvl-JBuy6O9HhClAK1HhMwh1FpgeYwXqrng1tifmuotuLQnZAiQJM73Gl-J_6s86Buo_1AIx5YAKCucYDUYYdXBIHLxrbALsA5W6pZCqqkMbqpTWteix-G5Q5T8LNsfqIu_uMBUGceqZWFJALhS9ieaDqoxhIqpx_89QAr1YlGu_UO6R6FYl0wDT-nzjyeF5tonSs3FHN0iNIiR3AMOHZu7KUwZaUdHg4eYkU-sQ01QNY_11keHROCRQ", "select_by": "user" }
令牌响应无效
Google 尝试使用过期、已撤消或无效的访问令牌发出 API 请求时的响应示例:
HTTP 响应标头
www-authenticate: Bearer realm="https://accounts.google.com/", error="invalid_token"
响应正文
{
"error": {
"code": 401,
"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"errors": [
{
"message": "Invalid Credentials",
"domain": "global",
"reason": "authError",
"location": "Authorization",
"locationType": "header"
}
],
"status": "UNAUTHENTICATED"
}
}