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

React Native中AsyncStorage持久化用户认证失效问题排查

React Native 持久化用户认证状态问题排查与解决

问题描述

我正在开发一款React Native应用,需要实现用户认证状态跨应用重启持久化,使用AsyncStorage存储会话token。登录功能正常,但关闭并重新打开应用后,用户会被登出,需要重新登录(这是我的第一个React Native项目)。我使用django-allauth库,该库在React中运行正常,但在React Native中出现问题。

关键实现代码

API与认证工具类

import AsyncStorage from "@react-native-async-storage/async-storage";

const Client = Object.freeze({
  APP: "app",
  BROWSER: "browser",
});

const CLIENT = Client.APP;

const BASE_URL = `http://localhost:7013/_allauth/${CLIENT}/v1`;
const ACCEPT_JSON = {
  Accept: "application/json",
  "Content-Type": "application/json",
};

export const URLs = Object.freeze({
  // Meta
  CONFIG: BASE_URL + "/config",

  // Auth: Basics
  LOGIN: BASE_URL + "/auth/login",
  SESSION: BASE_URL + "/auth/session",
  SIGNUP: BASE_URL + "/auth/signup",

  // Auth: Sessions
  SESSIONS: BASE_URL + "/auth/sessions",
});

export const AuthenticatorType = Object.freeze({
  TOTP: "totp",
  RECOVERY_CODES: "recovery_codes",
});

// Function to retrieve CSRF token
async function getCSRFToken() {
  try {
    const csrfToken = await AsyncStorage.getItem("csrfToken");
    return csrfToken;
  } catch (error) {
    console.error("Error retrieving CSRF token:", error);
    return null;
  }
}

async function request(method, path, data, headers = {}) {
  try {
    const options = {
      method,
      headers: {
        ...ACCEPT_JSON,
        ...headers,
      },
    };

    // Don't pass authentication related headers to the logout endpoint
    if (path !== URLs.LOGOUT) {
      const csrfToken = await getCSRFToken();
      if (csrfToken) {
        options.headers["X-CSRFToken"] = csrfToken;
      }
    }

    if (typeof data !== "undefined") {
      options.body = JSON.stringify(data);
    }

    const response = await fetch(path, options);
    const result = await response.json();

    // Handle session token updates and authentication state changes
    if (response.ok && result.session_token) {
      await AsyncStorage.setItem("sessionToken", result.session_token);
    }

    return result;
  } catch (error) {
    console.error("Request failed:", error);
    throw error;
  }
}
export async function login(data) {
  return await request("POST", URLs.LOGIN, data);
}

export async function logout() {
  return await request("DELETE", URLs.SESSION);
}

export async function signUp(data) {
  return await request("POST", URLs.SIGNUP, data);
}

export async function getConfig() {
  return await request("GET", URLs.CONFIG);
}

export async function getAuth() {
  return await request("GET", URLs.SESSION);
}

全局状态提供者

import React, { createContext, useContext, useEffect, useState } from "react";

import { getAuth } from "@/lib/allauth";

const GlobalContext = createContext();
export const useGlobalContext = () => useContext(GlobalContext);

const GlobalProvider = ({ children }) => {
  const [isLogged, setIsLogged] = useState(false);
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchCurrentUser = async () => {
      try {
        const res = await getAuth();
        console.log("provider auth");
        console.log(res);
        if (res && res.status === 200 && res.meta.is_authenticated) {
          setIsLogged(true);
          setUser(res.data.user);
        } else {
          setIsLogged(false);
          setUser(null);
        }
      } catch (error) {
        console.error("Error fetching authentication status:", error);
      } finally {
        setLoading(false);
      }
    };

    fetchCurrentUser();
  }, []);

  return (
    <GlobalContext.Provider
      value={{
        isLogged,
        setIsLogged,
        user,
        setUser,
        loading,
      }}
    >
      {children}
    </GlobalContext.Provider>
  );
};

export default GlobalProvider;

主布局组件

import { useEffect } from "react";
import { useFonts } from "expo-font";
import "react-native-url-polyfill/auto";
import { SplashScreen, Stack } from "expo-router";

import GlobalProvider from "@/context/GlobalProvider";

// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();

const RootLayout = () => {
  const [fontsLoaded, error] = useFonts({
    "Poppins-Black": require("../assets/fonts/Poppins-Black.ttf"),
    "Poppins-Bold": require("../assets/fonts/Poppins-Bold.ttf"),
    "Poppins-ExtraBold": require("../assets/fonts/Poppins-ExtraBold.ttf"),
    "Poppins-ExtraLight": require("../assets/fonts/Poppins-ExtraLight.ttf"),
    "Poppins-Light": require("../assets/fonts/Poppins-Light.ttf"),
    "Poppins-Medium": require("../assets/fonts/Poppins-Medium.ttf"),
    "Poppins-Regular": require("../assets/fonts/Poppins-Regular.ttf"),
    "Poppins-SemiBold": require("../assets/fonts/Poppins-SemiBold.ttf"),
    "Poppins-Thin": require("../assets/fonts/Poppins-Thin.ttf"),
  });

  useEffect(() => {
    if (error) throw error;

    if (fontsLoaded) {
      SplashScreen.hideAsync();
    }
  }, [fontsLoaded, error]);

  if (!fontsLoaded) {
    return null;
  }

  if (!fontsLoaded && !error) {
    return null;
  }

  return (
    <GlobalProvider>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="(auth)" options={{ headerShown: false }} />
        <Stack.Screen name="index" options={{ headerShown: false }} />
        <Stack.Screen name="search/[query]" options={{ headerShown: false }} />
      </Stack>
    </GlobalProvider>
  );
};

export default RootLayout;

登录组件

import { useState } from "react";
import { Link, router } from "expo-router";
import { SafeAreaView } from "react-native-safe-area-context";
import { View, Text, ScrollView, Dimensions, Alert, Image } from "react-native";

import { images } from "@/constants";
import { CustomButton, FormField } from "@/components";
// import { getCurrentUser, signIn } from "@/lib/appwrite";
import { login } from "@/lib/allauth";
import { useGlobalContext } from "@/context/GlobalProvider";

const SignIn = () => {
  const { setUser, setIsLogged } = useGlobalContext();
  const [isSubmitting, setSubmitting] = useState(false);
  const [form, setForm] = useState({
    email: "",
    password: "",
  });

  const submit = async () => {
    if (form.email === "" || form.password === "") {
      Alert.alert("Error", "Please fill in all fields");
      return;
    }

    setSubmitting(true);

    try {
      const result = await login({
        email: form.email,
        password: form.password,
      });

      if (result && result.status === 200 && result.meta.is_authenticated) {
        console.log("login");
        console.log(result);
        setUser(result.data.user);
        setIsLogged(true);
        Alert.alert("Success", "User signed in successfully");
        router.replace("/home");
      } else {
        Alert.alert("Error", "Invalid login credentials");
      }
    } catch (error) {
      Alert.alert("Error", error.message);
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <SafeAreaView className="bg-primary h-full">
      <ScrollView>
        <View
          className="w-full flex justify-center h-full px-4 my-6"
          style={{
            minHeight: Dimensions.get("window").height - 100,
          }}
        >
          <Image
            source={images.logo}
            resizeMode="contain"
            className="w-[115px] h-[34px]"
          />

          <Text className="text-2xl font-semibold text-white mt-10 font-psemibold">
            Log in to SmartTime
          </Text>

          <FormField
            title="Email"
            value={form.email}
            handleChangeText={(e) => setForm({ ...form, email: e })}
            otherStyles="mt-7"
            keyboardType="email-address"
          />

          <FormField
            title="Password"
            value={form.password}
            handleChangeText={(e) => setForm({ ...form, password: e })}
            otherStyles="mt-7"
          />

          <CustomButton
            title="Sign In"
            handlePress={submit}
            containerStyles="mt-7"
            isLoading={isSubmitting}
          />

          <View className="flex justify-center pt-5 flex-row gap-2">
            <Text className="text-lg text-gray-100 font-pregular">
              Don't have an account?
            </Text>
            <Link
              href="/signup"
              className="text-lg font-psemibold text-secondary"
            >
              Signup
            </Link>
          </View>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
};

export default SignIn;

当前问题

登录功能正常,但关闭或刷新应用后,用户需要重新登录,无法保持登录状态。

已采取的步骤

  • 登录时将会话token存入AsyncStorage;
  • 应用启动时在GlobalProvider组件中检查会话token;
  • 若存在会话token则验证用户身份。

问题排查与解决方案

核心问题

代码存在两个关键漏洞:

  1. 请求未携带存储的sessionToken:虽然登录时把sessionToken存到了AsyncStorage,但应用重启后调用getAuth()时,请求头里并没有带上这个token,导致django-allauth后端无法识别用户身份,返回未认证状态。
  2. 未存储CSRF Token:django-allauth依赖CSRF Token验证请求合法性,你只存了sessionToken,没存CSRF Token,导致后续请求因为缺少CSRF Token被后端拒绝。

修复步骤

1. 新增Session Token获取函数并在请求中携带

修改API工具类,添加获取sessionToken的函数,并在请求时将其加入请求头(根据django-allauth的API要求调整,这里假设用X-Session-Token头):

// 添加获取sessionToken的函数
async function getSessionToken() {
  try {
    const token = await AsyncStorage.getItem("sessionToken");
    return token;
  } catch (error) {
    console.error("获取sessionToken失败:", error);
    return null;
  }
}

// 修改request函数,在请求时携带sessionToken
async function request(method, path, data, headers = {}) {
  try {
    const options = {
      method,
      headers: {
        ...ACCEPT_JSON,
        ...headers,
      },
      // 跨域请求需要携带凭证,若后端配置了CORS允许则添加
      credentials: 'include'
    };

    if (path !== URLs.LOGOUT) {
      const csrfToken = await getCSRFToken();
      const sessionToken = await getSessionToken();
      if (csrfToken) {
        options.headers["X-CSRFToken"] = csrfToken;
      }
      // 携带sessionToken到请求头
      if (sessionToken) {
        options.headers["X-Session-Token"] = sessionToken;
        // 若后端用Cookie验证,可替换为:
        // options.headers["Cookie"] = `sessionid=${sessionToken}`;
      }
    }

    // 其余代码保持不变...
  } catch (error) {
    console.error("Request failed:", error);
    throw error;
  }
}

2. 登录时同时存储CSRF Token

修改request函数,在登录成功后同时保存后端返回的CSRF Token:

async function request(method, path, data, headers = {}) {
  try {
    // 其余代码保持不变...

    const response = await fetch(path, options);
    const result = await response.json();

    // 保存sessionToken和CSRF Token
    if (response.ok) {
      if (result.session_token) {
        await AsyncStorage.setItem("sessionToken", result.session_token);
      }
      // 从响应头或返回结果中获取CSRF Token(根据后端返回格式调整)
      const csrfToken = response.headers.get("X-CSRFToken") || result.csrf_token;
      if (csrfToken) {
        await AsyncStorage.setItem("csrfToken", csrfToken);
      }
    }

    return result;
  } catch (error) {
    console.error("Request failed:", error);
    throw error;
  }
}

3. 认证失败时清除本地无效Token

修改GlobalProvider中的认证逻辑,当请求失败或未认证时,清除本地存储的无效Token,避免下次启动时重复使用:

const fetchCurrentUser = async () => {
  try {
    const res = await getAuth();
    console.log("provider auth");
    console.log(res);
    if (res && res.status === 200 && res.meta.is_authenticated) {
      setIsLogged(true);
      setUser(res.data.user);
    } else {
      // 未认证时清除本地Token
      await AsyncStorage.removeItem("sessionToken");
      await AsyncStorage.removeItem("csrfToken");
      setIsLogged(false);
      setUser(null);
    }
  } catch (error) {
    console.error("Error fetching authentication status:", error);
    // 请求失败时清除本地Token
    await AsyncStorage.removeItem("sessionToken");
    await AsyncStorage.removeItem("csrfToken");
    setIsLogged(false);
    setUser(null);
  } finally {
    setLoading(false);
  }
};

4. 后端CORS配置(可选但重要)

确保django-allauth后端的CORS配置允许携带凭证,例如在Django的CORS设置中添加:

CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [
    "http://localhost:8081",  # React Native调试端口
    # 其他允许的域名
]

验证修复

完成上述修改后:

  1. 登录应用,确认sessionToken和csrfToken都被存入AsyncStorage;
  2. 关闭并重启应用,检查GlobalProvider中的getAuth()请求是否携带了两个Token;
  3. 确认后端返回用户已认证状态,应用自动跳转到主页。

内容的提问来源于stack exchange,提问作者Davood

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 20:45:54