You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Expo AuthSession静默认证问题:每次启动应用需重新登录

问题分析与解决方案

你的问题既有Expo Go运行环境的限制,也有代码实现上的缺失,以下是具体分析和修复方案:

一、Expo Go的影响

Expo Go属于沙箱测试环境,每次重启应用时,应用的本地存储会被重置,导致持久化的认证状态丢失。不过即使在Expo Go中,我们也能通过正确的逻辑让静默认证在单次会话内生效;而打包为独立APK后,认证状态会被稳定保存,只有卸载应用才会丢失。

二、代码中的核心问题

  1. 认证状态未持久化:仅在Mobx Store的内存中保存userName,应用重启后内存数据清空,没有从本地存储恢复会话。
  2. 强制触发登录界面:preperaLogInRequest中固定设置prompt: AuthSession.Prompt.Login,会强制每次都显示登录页,无法触发静默认证。
  3. 缺少自动恢复会话逻辑:没有尝试用存储的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 22:57:26