如何处理精细权限

概览

借助精细权限,用户可以更精细地控制与每个应用共享哪些账号数据。这些权限可提供更强大的控制力、透明度和安全性,让用户和开发者都能从中受益。本指南将帮助您了解必要的更改和步骤,以便成功更新应用来处理精细权限。

什么是精细权限?

假设您开发了一款效率提升应用,该应用同时请求电子邮件范围和日历范围。您的用户可能只想将您的应用用于 Google 日历,而不想用于 Gmail。借助精细的 OAuth 权限,用户可以选择仅授予 Google 日历权限,而不授予 Gmail 权限。通过让用户授予对特定数据的访问权限,可以最大限度地减少数据泄露,增强信任,并让用户能够以隐私至上的方式控制其数字生活。请务必设计应用来处理此类情况。

请求多个非登录范围时

登录和非登录范围

对于同时请求登录和非登录范围的应用,用户首先会看到登录范围(email、profile 和 openid)的权限请求页面。用户同意分享其基本身份信息(姓名、邮箱和个人资料照片)后,会看到非登录范围的精细权限请求页面。在这种情况下,应用必须检查用户授予了哪些范围,而不能假定用户授予了所有请求的范围。在以下示例中,Web 应用请求了所有三个登录范围和一个 Google 云端硬盘非登录范围。用户同意登录范围后,会看到 Google 云端硬盘权限的精细权限请求页面:

登录和非登录范围

多个非登录范围

当应用请求多个非登录范围时,系统会向用户显示精细权限请求页面。用户可以选择要批准与应用共享哪些权限。以下是一个精细权限请求页面示例,该页面请求访问用户的 Gmail 邮件和 Google 日历数据:

多个非登录范围

对于仅请求登录范围(email、profile 和 openid)的应用,细化权限请求页面不适用。用户可以批准或拒绝整个登录请求。换句话说,如果应用仅请求登录范围(一个、两个或全部三个),则不适用精细权限请求页面。

对于仅请求一个非登录范围的应用,精细权限请求页面不适用。换句话说,用户要么批准要么拒绝整个请求,并且权限请求页面中没有复选框。下表总结了何时会显示精细权限请求页面。

登录范围数量 非登录范围的数量 精细权限请求页面
1-3 0 不适用
1-3 1+ 适用
0 1 不适用
0 2+ 适用

确定您的应用是否受到影响

全面检查应用中所有使用 Google OAuth 2.0 授权端点发出权限请求的部分。请注意那些请求多个范围的应用,因为它们会触发向用户显示的精细权限请求页面。在这种情况下,请确保您的代码可以处理用户仅授权部分范围的情况。

如何确定应用是否使用了多个范围

检查应用代码或传出网络调用,确定应用发出的 Google OAuth 2.0 授权请求是否会导致显示精细权限请求页面。

检查应用代码

检查应用代码中调用 Google OAuth 授权端点以请求用户权限的部分。如果您使用某个 Google API 客户端库,通常可以在客户端初始化步骤中找到应用请求的范围。下一部分展示了一些示例。您应参考应用用于处理 Google OAuth 2.0 的 SDK 的文档,并参考以下示例中的指南,确定您的应用是否受到影响。

Google Identity 服务

以下 Google Identity 服务 JavaScript 库代码段使用多个非登录范围初始化 TokenClient。当 Web 应用请求用户授权时,系统会显示精细权限请求页面。

const 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: (response) => {
    ...
  },
});

Python

以下代码段使用 google-auth-oauthlib.flow 模块来构建授权请求;scope 参数包含两个非登录范围。当 Web 应用请求用户授权时,系统会显示精细权限请求页面。

import google.oauth2.credentials
import google_auth_oauthlib.flow

# Use the client_secret.json file to identify the application requesting
# authorization. The client ID (from that file) and access scopes are required.
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
    'client_secret.json',
    scopes=['https://www.googleapis.com/auth/calendar.readonly',
                    'https://www.googleapis.com/auth/contacts.readonly'])

Node.js

以下代码段会创建一个 google.auth.OAuth2 对象,该对象定义了授权请求中的参数,其 scope 参数包含两个非登录范围。当 Web 应用请求用户授权时,系统会显示精细权限请求页面。

const {google} = require('googleapis');

/**

  • To use OAuth2 authentication, we need access to a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI
  • from the client_secret.json file. To get these credentials for your application, visit
  • https://console.cloud.google.com/apis/credentials. */ const oauth2Client = new google.auth.OAuth2( YOUR_CLIENT_ID, YOUR_CLIENT_SECRET, YOUR_REDIRECT_URL );

// Access scopes for read-only Calendar and Contacts. const scopes = [ 'https://www.googleapis.com/auth/calendar.readonly', 'https://www.googleapis.com/auth/contacts.readonly'] ];

// Generate a url that asks permissions const authorizationUrl = oauth2Client.generateAuthUrl({ // 'online' (default) or 'offline' (gets refresh_token) access_type: 'offline', /** Pass in the scopes array defined earlier. * Alternatively, if only one scope is needed, you can pass a scope URL as a string */ scope: scopes, // Enable incremental authorization. Recommended as best practices. include_granted_scopes: true });

检查出站网络调用

检查网络调用的方法因应用客户端类型而异。

在检查网络调用时,请查找发送到 Google OAuth 授权端点的请求,并检查 scope 参数。

这些值会导致显示精细权限请求页面。

  • scope 参数包含登录范围和非登录范围。

    以下示例请求包含所有三个登录范围和一个非登录范围,用于查看用户 Google 云端硬盘文件的元数据:

    https://accounts.google.com/o/oauth2/v2/auth?
    access_type=offline&
    scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20openid%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.metadata.readonly&
    include_granted_scopes=true&
    response_type=code&
    redirect_uri=YOUR_REDIRECT_URL&
    client_id=YOUR_CLIENT_ID
  • scope 参数包含多个非登录范围。

    以下示例请求包含两个非登录范围,用于查看用户的 Google 云端硬盘元数据和管理特定的 Google 云端硬盘文件:

    https://accounts.google.com/o/oauth2/v2/auth?
    access_type=offline&
    scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.metadata.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.file&
    include_granted_scopes=true&
    response_type=code&
    redirect_uri=YOUR_REDIRECT_URL&
    client_id=YOUR_CLIENT_ID

处理精细权限的最佳实践

如果您确定需要更新应用以处理精细权限,则应更新代码,以便妥善处理多个范围的同意情况。所有应用都应遵循以下最佳实践:

  1. 查看 Google API 服务:用户数据政策,并确保您遵守这些政策。
  2. 请求执行任务所需的特定范围。您必须遵守 Google OAuth 2.0 政策,仅请求您需要的范围。您应避免在登录时请求多个范围,除非这对应用的核心功能至关重要。将多个范围捆绑在一起,尤其是对于不熟悉应用功能的初次使用的用户,可能会让他们难以理解这些权限的必要性。这可能会引起用户警惕,并阻止用户进一步与您的应用互动。
  3. 在发出授权请求之前,向用户提供理由。清楚地说明您的应用为何需要所请求的权限、您将如何处理用户的数据,以及用户批准请求后将如何受益。 我们的研究表明,这些说明可以提高用户的信任度和互动度。
  4. 每当应用请求范围时,请使用 增量授权,以避免管理多个访问令牌。
  5. 检查用户授予了哪些范围。一次请求多个范围时,用户可能不会授予应用请求的所有范围。您的应用应始终检查用户授予了哪些范围,并通过停用相关功能来处理任何范围拒绝。遵循有关处理多个范围的意见征求的 Google OAuth 2.0 政策,并且仅在用户明确表示有意使用需要该范围的特定功能时,才再次提示用户征求意见。

更新应用以处理精细权限

Android 应用

您应查阅用于与 Google OAuth 2.0 交互的 SDK 的文档,并根据最佳实践更新应用,以处理精细权限。

如果您使用 Play 服务中的 auth.api.signin SDK 与 Google OAuth 2.0 交互,则可以使用 requestPermissions 函数请求所需的最小范围集,并使用 hasPermissions 函数检查用户在请求精细权限时授予了哪些范围。

Chrome 扩展程序应用

您应根据最佳实践,使用 Chrome Identity API 来处理 Google OAuth 2.0。

以下示例展示了如何正确处理精细权限。

manifest.json

示例清单文件为 Chrome 扩展程序应用声明了两个非登录范围。

{
  "name": "Example Chrome extension application",
  ...
  "permissions": [
      "identity"
    ],
  "oauth2" : {
      "client_id": "YOUR_CLIENT_ID",
      "scopes":["https://www.googleapis.com/auth/calendar.readonly",
                "https://www.googleapis.com/auth/contacts.readonly"]
  }
}

不正确的做法

全有或全无

用户点击该按钮即可启动授权流程。该代码段假定用户会看到一个“全有或全无”的权限请求页面,其中包含 manifest.json 文件中指定的两个范围。它忽略了检查用户授予了哪些范围。

oauth.js

...
document.querySelector('button').addEventListener('click', function () {
  chrome.identity.getAuthToken({ interactive: true },
      function (token) {
          if (token === undefined) {
            // User didn't authorize both scopes.
            // Updating the UX and application accordingly
            ...
          } else {
            // User authorized both or one of the scopes.
            // It neglects to check which scopes users granted and assumes users granted all scopes.

            // Calling the APIs, etc.
            ...
          }
      });
});

正确方法

最小范围

选择所需的最小范围集

应用应仅请求所需的一组最小范围。建议您的应用在需要完成任务时一次请求一个范围。

在此示例中,假设 manifest.json 文件中声明的两个范围都是所需的最小范围集。oauth.js 文件使用 Chrome Identity API 向 Google 发起授权流程。您应选择启用精细权限,以便用户能够更好地控制向您的应用授予权限。您的应用应通过检查用户授权的范围来正确处理用户的响应。

oauth.js

...
document.querySelector('button').addEventListener('click', function () {
  chrome.identity.getAuthToken({ interactive: true, enableGranularPermissions: true },
      function (token, grantedScopes) {
          if (token === undefined) {
            // User didn't authorize any scope.
            // Updating the UX and application accordingly
            ...
          } else {
            // User authorized the request. Now, check which scopes were granted.
            if (grantedScopes.includes('https://www.googleapis.com/auth/calendar.readonly'))
            {
              // User authorized Calendar read permission.
              // Calling the APIs, etc.
              ...
            }
            else
            {
              // User didn't authorize Calendar read permission.
              // Update UX and application accordingly
              ...
            }

            if (grantedScopes.includes('https://www.googleapis.com/auth/contacts.readonly'))
            {
              // User authorized Contacts read permission.
              // Calling the APIs, etc.
              ...
            }
            else
            {
              // User didn't authorize Contacts read permission.
              // Update UX and application accordingly
              ...
            }
          }
      });
});

iOS、iPadOS 和 macOS 应用

您应查阅您用于与 Google OAuth 2.0 交互的 SDK 的文档,并根据最佳实践更新您的应用,以处理精细权限。

如果您使用适用于 iOS 和 macOS 的 Google 登录库与 Google OAuth 2.0 交互,则应查看有关处理精细权限的文档。

Web 应用

您应查阅用于与 Google OAuth 2.0 交互的 SDK 的文档,并根据最佳实践更新应用,以处理精细权限。

服务器端(离线)访问

服务器端(离线)访问模式要求您执行以下操作:

以下代码段展示了一个 NodeJS 示例,该示例请求两个非登录范围。用户将看到精细权限请求页面。

不正确的做法

全有或全无

用户会被重定向到授权网址。该代码段假定用户会看到一个“全有或全无”权限请求页面,其中包含 scopes 数组中指定的两个范围。它忽略了检查用户授予了哪些范围。

main.js

...
const oauth2Client = new google.auth.OAuth2(
  YOUR_CLIENT_ID,
  YOUR_CLIENT_SECRET,
  YOUR_REDIRECT_URL
);

// Access scopes for two non-Sign-In scopes - Google Calendar and Contacts
const scopes = [
  'https://www.googleapis.com/auth/contacts.readonly',
  'https://www.googleapis.com/auth/calendar.readonly'
];

// Generate a url that asks permissions for the Google Calendar and Contacts scopes
const authorizationUrl = oauth2Client.generateAuthUrl({
  // 'online' (default) or 'offline' (gets refresh_token)
  access_type: 'offline',
  // Pass in the scopes array defined earlier
  scope: scopes,
  // Enable incremental authorization. Recommended as best practices.
  include_granted_scopes: true
});

async function main() {
  const server = http.createServer(async function (req, res) {
    // Example on redirecting user to Google OAuth 2.0 server.
    if (req.url == '/') {
      res.writeHead(301, { "Location": authorizationUrl });
    }
    // Receive the callback from Google OAuth 2.0 server.
    if (req.url.startsWith('/oauth2callback')) {
      // Handle the Google OAuth 2.0 server response
      let q = url.parse(req.url, true).query;

      if (q.error) {
        // User didn't authorize both scopes.
        // Updating the UX and application accordingly
        ...
      } else {
        // User authorized both or one of the scopes.
        // It neglects to check which scopes users granted and assumes users granted all scopes.

        // Get access and refresh tokens (if access_type is offline)
        let { tokens } = await oauth2Client.getToken(q.code);
        // Calling the APIs, etc.
        ...
      }
    }
    res.end();
  }).listen(80);
}
正确方法

最小范围

选择所需的最小范围集

应用应仅请求所需的一组最小范围。建议您的应用在需要完成任务时一次请求一个范围。每当应用请求范围时,都应使用增量授权,以避免管理多个访问令牌。

如果您的应用必须请求多个非登录范围,您应始终在请求时使用增量授权,并检查用户授予了哪些范围。

在此示例中,假设应用需要这两个范围才能正常运行。您应选择启用精细权限,以便用户能够更好地控制向您的应用授予权限。您的应用应通过检查用户已授权的范围来正确处理用户的响应。

main.js

...
const oauth2Client = new google.auth.OAuth2(
  YOUR_CLIENT_ID,
  YOUR_CLIENT_SECRET,
  YOUR_REDIRECT_URL
);

// Access scopes for two non-Sign-In scopes - Google Calendar and Contacts
const scopes = [
  'https://www.googleapis.com/auth/contacts.readonly',
  'https://www.googleapis.com/auth/calendar.readonly'
];

// Generate a url that asks permissions for the Google Calendar and Contacts scopes
const authorizationUrl = oauth2Client.generateAuthUrl({
  // 'online' (default) or 'offline' (gets refresh_token)
  access_type: 'offline',
  // Pass in the scopes array defined earlier
  scope: scopes,
  // Enable incremental authorization. Recommended as best practices.
  include_granted_scopes: true,
  // Set to true to enable more granular permissions for Google OAuth 2.0 client IDs created before 2019.
  // No effect for newer Google OAuth 2.0 client IDs, since more granular permissions is always enabled for them.
  enable_granular_consent: true
});

async function main() {
  const server = http.createServer(async function (req, res) {
    // Redirect users to Google OAuth 2.0 server.
    if (req.url == '/') {
      res.writeHead(301, { "Location": authorizationUrl });
    }
    // Receive the callback from Google OAuth 2.0 server.
    if (req.url.startsWith('/oauth2callback')) {
      // Handle the Google OAuth 2.0 server response
      let q = url.parse(req.url, true).query;

      if (q.error) {
        // User didn't authorize both scopes.
        // Updating the UX and application accordingly
        ...
      } else {
        // Get access and refresh tokens (if access_type is offline)
        let { tokens } = await oauth2Client.getToken(q.code);
        oauth2Client.setCredentials(tokens);

        // User authorized the request. Now, check which scopes were granted.
        if (tokens.scope.includes('https://www.googleapis.com/auth/calendar.readonly'))
        {
          // User authorized Calendar read permission.
          // Calling the APIs, etc.
          ...
        }
        else
        {
          // User didn't authorize Calendar read permission.
          // Calling the APIs, etc.
          ...
        }

        // Check which scopes user granted the permission to application
        if (tokens.scope.includes('https://www.googleapis.com/auth/contacts.readonly'))
        {
          // User authorized Contacts read permission.
          // Calling the APIs, etc.
          ...
        }
        else
        {
          // User didn't authorize Contacts read permission.
          // Update UX and application accordingly
          ...
        }
      }
    }
    res.end();
  }).listen(80);
}

请参阅服务器端 Web 应用指南,了解如何从基于服务器的应用访问 Google API。

仅限客户端访问

  • 对于使用 Google Identity 服务 JavaScript 库与 Google OAuth 2.0 交互的应用,您应查看这篇有关处理精细权限的文档。
  • 对于使用 JavaScript 直接调用 Google OAuth 2.0 授权端点的应用,您应查看这篇关于处理精细权限的文档。

测试更新后的应用在处理精细权限方面的表现

  1. 列出用户可以响应权限请求的所有情况,以及应用的预期行为。例如,如果用户仅授权了三个请求范围中的两个,您的应用应做出相应行为。
  2. 在启用精细权限的情况下测试应用。您可以通过以下两种方式启用精细权限:

    1. 检查应用的 OAuth 2.0 权限请求页面,看看是否已为应用启用精细权限。您还可以通过 Google Cloud 控制台创建新的 Web、Android 或 iOS Google OAuth 2.0 客户端 ID,以用于测试目的,因为这些客户端 ID 始终会启用精细权限。
    2. 调用 Google OAuth 授权端点时,将参数 enable_granular_consent 设置为 true。部分 SDK 明确支持此参数。对于其他平台,请查看相关文档,了解如何手动添加此参数及其值。如果您的实现不支持添加该参数,您可以按照上一点所述,通过 Google Cloud 控制台创建新的 Web、Android 或 iOS Google OAuth 2.0 客户端 ID,仅用于测试目的。

  3. 测试更新后的应用时,请使用个人 Google 账号 (@gmail.com),而不是 Workspace 账号。这是因为,具有全网域授权或标记为受信任的 Workspace 企业版应用目前不受精细权限变更的影响。 因此,使用贵组织的 Workspace 账号进行测试可能无法按预期显示新的精细权限请求页面。