使用 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。
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 | 自动发现 |
|---|---|---|---|
| 注册 > Applications | OpenID | 支持 | 可用 |
- 你不能定义自定义
redirectUri,Okta 会为你提供一个。
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 build或npx expo run:android - iOS:
eas build或npx expo run:ios
- Android:
- Bare 工作流
-
创建: 在正确的环境中运行时,使用
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 ios、yarn 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]); // 更多登录代码... }

