Expo AuthSession静默认证问题:每次启动应用需重新登录
问题分析与解决方案
你的问题既有Expo Go运行环境的限制,也有代码实现上的缺失,以下是具体分析和修复方案:
一、Expo Go的影响
Expo Go属于沙箱测试环境,每次重启应用时,应用的本地存储会被重置,导致持久化的认证状态丢失。不过即使在Expo Go中,我们也能通过正确的逻辑让静默认证在单次会话内生效;而打包为独立APK后,认证状态会被稳定保存,只有卸载应用才会丢失。
二、代码中的核心问题
- 认证状态未持久化:仅在Mobx Store的内存中保存
userName,应用重启后内存数据清空,没有从本地存储恢复会话。 - 强制触发登录界面:
preperaLogInRequest中固定设置prompt: AuthSession.Prompt.Login,会强制每次都显示登录页,无法触发静默认证。 - 缺少自动恢复会话逻辑:没有尝试用存储的Token自动恢复已登录状态,每次启动都直接进入登录流程。
三、修复后的代码实现
1. 修改Mobx Store(添加持久化与静默认证)
import { action, computed, makeObservable, observable } from "mobx"; import React from "react"; import * as AuthSession from "expo-auth-session"; import { Platform } from "react-native"; import jwtDecode, { JwtPayload } from "jwt-decode"; import { openAuthSessionAsync } from "expo-web-browser"; import AsyncStorage from '@react-native-async-storage/async-storage'; export class UserStore { auth0ClientId = "clientId"; authorizationEndpoint = "endpoint"; discoveryDoc: AuthSession.DiscoveryDocument = { authorizationEndpoint: this.authorizationEndpoint }; useProxy: boolean = false; request: AuthSession.AuthRequest | null = null; userName: string | null = null; idToken: string | null = null; // 新增:存储ID Token constructor() { this.useProxy = Platform.select({ web: false, default: false }); makeObservable(this, { preperaLogInRequest: action.bound, logIn: action.bound, userName: observable, isLoading: computed, logOut: action.bound, restoreSession: action.bound, saveSession: action.bound }); // 初始化时自动恢复会话 this.restoreSession(); } get isLoading() { return this.request === null; } // 新增:从本地存储恢复会话 async restoreSession() { try { const savedToken = await AsyncStorage.getItem('auth_id_token'); if (savedToken) { const decoded = jwtDecode<JwtPayload & { name: string }>(savedToken); // 检查Token是否过期 if (decoded.exp && decoded.exp * 1000 > Date.now()) { this.idToken = savedToken; this.userName = decoded.name; } else { await AsyncStorage.removeItem('auth_id_token'); } } } catch (error) { console.log('恢复会话失败:', error); } finally { await this.preperaLogInRequest(); } } // 新增:保存会话到本地存储 async saveSession(token: string, name: string) { this.idToken = token; this.userName = name; await AsyncStorage.setItem('auth_id_token', token); } async preperaLogInRequest() { let redirectUri = AuthSession.makeRedirectUri({ useProxy: this.useProxy }); this.request = await AuthSession.loadAsync({ redirectUri, clientId: this.auth0ClientId, responseType: "id_token", // 已有用户则尝试静默认证,否则显示登录页 prompt: this.userName ? AuthSession.Prompt.None : AuthSession.Prompt.Login, scopes: ["openid", "profile"], extraParams: { nonce: "nonce", }, }, this.discoveryDoc); } async logIn() { if (this.request) { let result = await this.request.promptAsync(this.discoveryDoc, { useProxy: this.useProxy }); if (result) { if (result.type === "error") { console.log( "Authentication error: " + (result.params.error_description || "something went wrong") ); return; } if (result.type === "success") { console.log("success"); const jwtToken = result.params.id_token; const decoded = jwtDecode<JwtPayload & { name: string }>(jwtToken); const { name } = decoded; await this.saveSession(jwtToken, name); // 持久化会话 } } else { console.log("Something went wrong during log in"); } } } async logOut() { try { let res = await openAuthSessionAsync(`https://dev-j3dkt6a3.us.auth0.com/logout?client_id=1wMZHHb2znirLmNv1GHlqDKz5JT9FN7d`, 'redirectUrl'); console.log(JSON.stringify(res)) this.userName = null; this.idToken = null; await AsyncStorage.removeItem('auth_id_token'); // 清除本地存储的Token } catch (error) { console.log("Failed to log out: " + error); } } } const userStore = new UserStore(); export const UserStoreContext = React.createContext(userStore); export const useUserStore = () => React.useContext(UserStoreContext)
2. 修改RootNavigator(调整初始化逻辑)
const RootNavigator = observer(() => { const { userName, isLoading } = useUserStore(); // 移除原有的useEffect,会话恢复逻辑已移到Store构造函数中 // useEffect(()=>{preperaLogInRequest()}, []); return ( <> { !isLoading? <Stack.Navigator> {userName ? <> <Stack.Screen name="Root" component={BottomTabNavigator} options={{ headerShown: false }} /> <Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} /> <Stack.Group screenOptions={{ presentation: 'modal' }}> <Stack.Screen name="Modal" component={ModalScreen} /> </Stack.Group> </> : <> <Stack.Screen name="LogIn" component={LogIn} options={{ headerShown: false }} /> </> } </Stack.Navigator>: <Text>Loading</Text> } </> ); })
四、额外说明
- Expo Go测试限制:在Expo Go中,每次重启应用后本地存储会被清空,仍需重新登录,但单次会话内退出再打开无需重复登录;打包为独立APK后,认证状态会持久保存,只有卸载才会丢失。
- 生产环境优化:当前代码仅简单检查ID Token过期时间,建议在生产环境中使用Refresh Token获取新的ID Token,避免用户因Token过期频繁登录。
内容的提问来源于stack exchange,提问作者Konstantin Konstantinov
相关产品推荐
相关产品推荐

