使用 OAuth 或 OpenID 提供商进行身份验证

编辑页面

了解如何利用 expo-auth-session 库来实现与 OAuth 或 OpenID 提供商的身份验证。


For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

expo-auth-session 提供了一个统一的 API,用于在 Android、iOS 和 web 上实现 OAuth 和 OpenID Connect 提供方。本指南将通过几个示例展示如何使用 AuthSession API。

所有身份验证提供方的规则

使用 AuthSession API 时,以下规则适用于所有身份验证提供方:

  • 使用 WebBrowser.maybeCompleteAuthSession() 来关闭 web 弹出窗口。如果忘记添加这一行,弹窗将不会关闭。
  • 使用 AuthSession.makeRedirectUri() 创建重定向 URI,这会处理跨平台支持中的许多繁琐工作。在底层,它使用了 expo-linking
  • 使用 AuthSession.useAuthRequest() 构建请求,该 hook 支持异步初始化,这意味着移动浏览器不会阻塞身份验证。
  • request 定义之前,请确保禁用提示。
  • 在 web 上只能在用户交互中调用 promptAsync
  • 由于无法自定义应用 scheme,Expo Go 不能用于 OAuth 或启用 OpenID Connect 的应用的本地开发和测试。你可以改用 Development Build,它提供类似 Expo Go 的开发体验,并支持登录后通过 OAuth 重定向回你的应用,其工作方式与生产环境完全一致。

获取访问令牌

大多数提供方使用 OAuth 2 标准进行安全身份验证和授权。在授权码授权流程中,身份提供方会返回一个一次性代码。然后使用该代码换取用户的访问令牌。

由于你的客户端应用代码不是安全存储密钥的地方,因此有必要在服务器中交换授权码,例如通过 API 路由React Server Components。这将允许你安全地存储和使用客户端密钥,以访问提供方的令牌端点。

示例

以下示例展示了如何使用 AuthSession API 与几个常见提供方进行身份验证。

网站提供方PKCE自动发现
获取你的配置OAuth 2.0支持不可用
  • 提供方每个应用只允许一个重定向 URI。你需要为每种想使用的方法分别创建一个独立的应用:
    • 独立版 / 开发构建:com.your.app://*
    • Web:https://yourwebsite.com/*
  • redirectUri 需要两个斜杠(://)。
  • revocationEndpoint 是动态的,并且需要你的 config.clientId
GitHub Auth Example
import { useEffect } from 'react'; import * as WebBrowser from 'expo-web-browser'; import { makeRedirectUri, useAuthRequest } from 'expo-auth-session'; import { Button } from 'react-native'; WebBrowser.maybeCompleteAuthSession(); // 端点 const discovery = { authorizationEndpoint: 'https://github.com/login/oauth/authorize', tokenEndpoint: 'https://github.com/login/oauth/access_token', revocationEndpoint: 'https://github.com/settings/connections/applications/<CLIENT_ID>', }; export default function App() { const [request, response, promptAsync] = useAuthRequest( { clientId: 'CLIENT_ID', scopes: ['identity'], redirectUri: makeRedirectUri({ scheme: 'your.app' }), }, discovery ); useEffect(() => { if (response?.type === 'success') { const { code } = response.params; } }, [response]); return ( <Button disabled={!request} title="Login" onPress={() => { promptAsync(); }} /> ); }
网站提供方PKCE自动发现
注册 > ApplicationsOpenID支持可用
  • 你不能定义自定义 redirectUri,Okta 会为你提供一个。
Okta Auth Example
import { useEffect } from 'react'; import * as WebBrowser from 'expo-web-browser'; import { makeRedirectUri, useAuthRequest, useAutoDiscovery } from 'expo-auth-session'; import { Button, Platform } from 'react-native'; WebBrowser.maybeCompleteAuthSession(); export default function App() { // 端点 const discovery = useAutoDiscovery('https://<OKTA_DOMAIN>.com/oauth2/default'); // 请求 const [request, response, promptAsync] = useAuthRequest( { clientId: 'CLIENT_ID', scopes: ['openid', 'profile'], redirectUri: makeRedirectUri({ native: 'com.okta.<OKTA_DOMAIN>:/callback', }), }, discovery ); useEffect(() => { if (response?.type === 'success') { const { code } = response.params; } }, [response]); return ( <Button disabled={!request} title="Login" onPress={() => { promptAsync(); }} /> ); }

重定向 URI 模式

以下是一些你最终可能会使用的常见重定向 URI 模式示例。

独立/开发构建

yourscheme://path

在某些情况下,可能会有 1 到 3 个斜杠(/)。

  • 环境:

    • Bare 工作流
      • npx expo prebuild
    • App 或 Play Store 中的独立构建,或在本地测试
      • Android: eas buildnpx expo run:android
      • iOS: eas buildnpx expo run:ios
  • 创建: 在正确的环境中运行时,使用 AuthSession.makeRedirectUri({ native: '<YOUR_URI>' }) 来选择 native。

    • your.app://redirect -> makeRedirectUri({ scheme: 'your.app', path: 'redirect' })
    • your.app:/// -> makeRedirectUri({ scheme: 'your.app', isTripleSlashed: true })
    • your.app:/authorize -> makeRedirectUri({ native: 'your.app:/authorize' })
    • your.app://auth?foo=bar -> makeRedirectUri({ scheme: 'your.app', path: 'auth', queryParams: { foo: 'bar' } })
    • exp://u.expo.dev/[project-id]?channel-name=[channel-name]&runtime-version=[runtime-version] -> makeRedirectUri()
    • 这个链接通常可以自动创建,但我们建议你至少定义 scheme 属性。整个 URL 也可以通过传入 native 属性在应用中覆盖。通常这会用于 Google 或 Okta 之类的提供商,它们要求你使用自定义的原生 URI 重定向。你可以使用 npx uri-scheme 添加、列出并打开 URI scheme。
    • 如果你在 eject 之后更改了 expo.scheme,那么你需要使用 expo apply 命令将更改应用到你的原生项目,然后重新构建它们(yarn iosyarn android)。
  • 用法: promptAsync({ redirectUri })

改善用户体验

“登录流程”是一个非常重要的环节,在很多情况下,这正是用户会再次 承诺 使用你应用的地方。糟糕的体验可能会让用户在真正开始使用你的应用之前就放弃它。

以下是一些可用于让用户的身份验证更快、更简单且更安全的技巧:

预热浏览器

在 Android 上,你可以选择在网页浏览器使用前先预热它。这样浏览器应用就能在后台预先初始化自身。这样做可以显著加快向用户提示身份验证的速度。

import { useEffect } from 'react'; import * as WebBrowser from 'expo-web-browser'; function App() { useEffect(() => { WebBrowser.warmUpAsync(); return () => { WebBrowser.coolDownAsync(); }; }, []); // 执行身份验证 ... }

隐式登录

由于没有安全的方式将客户端密钥存储在你的应用包中,历史上许多提供商都提供了“隐式流程”,使你无需客户端密钥即可请求访问令牌。由于存在固有的安全风险,包括访问令牌注入的风险,这种方式已不再推荐。 取而代之的是,大多数提供商现在都支持带有 PKCE(Proof Key for Code Exchange)扩展的授权码流程,以便在你的客户端应用代码中安全地将授权码交换为访问令牌。了解更多关于从隐式流程迁移到带有 PKCE 的授权码流程

expo-auth-session 仍然支持隐式流程,以用于兼容旧代码。下面是一个隐式流程的示例实现。

import { useEffect } from 'react'; import * as WebBrowser from 'expo-web-browser'; import { makeRedirectUri, useAuthRequest, ResponseType } from 'expo-auth-session'; WebBrowser.maybeCompleteAuthSession(); // 端点 const discovery = { authorizationEndpoint: 'https://accounts.spotify.com/authorize', }; function App() { const [request, response, promptAsync] = useAuthRequest( { responseType: ResponseType.Token, clientId: 'CLIENT_ID', scopes: ['user-read-email', 'playlist-modify-public'], redirectUri: makeRedirectUri({ scheme: 'your.app' }), }, discovery ); useEffect(() => { if (response && response.type === 'success') { const token = response.params.access_token; } }, [response]); return <Button disabled={!request} onPress={() => promptAsync()} title="Login" />; }

存储数据

在 Android 和 iOS 等原生平台上,你可以使用名为 expo-secure-store 的库在本地安全地存储诸如访问令牌之类的数据(这不同于不安全的 AsyncStorage)。它在 Android 上提供对加密 SharedPreferences 的原生访问,并在 iOS 上提供对 keychain services 的原生访问。Web 上没有与此功能对应的实现。

你可以存储你的身份验证结果,并在之后重新恢复它们,从而避免再次提示用户登录。

import * as SecureStore from 'expo-secure-store'; const MY_SECURE_AUTH_STATE_KEY = 'MySecureAuthStateKey'; function App() { const [, response] = useAuthRequest({}); useEffect(() => { if (response && response.type === 'success') { const auth = response.params; const storageValue = JSON.stringify(auth); if (Platform.OS !== 'web') { // 将认证安全地存储在你的设备上 SecureStore.setItemAsync(MY_SECURE_AUTH_STATE_KEY, storageValue); } } }, [response]); // 更多登录代码... }