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则验证用户身份。
问题排查与解决方案
核心问题
代码存在两个关键漏洞:
- 请求未携带存储的sessionToken:虽然登录时把sessionToken存到了AsyncStorage,但应用重启后调用
getAuth()时,请求头里并没有带上这个token,导致django-allauth后端无法识别用户身份,返回未认证状态。 - 未存储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调试端口 # 其他允许的域名 ]
验证修复
完成上述修改后:
- 登录应用,确认sessionToken和csrfToken都被存入AsyncStorage;
- 关闭并重启应用,检查GlobalProvider中的
getAuth()请求是否携带了两个Token; - 确认后端返回用户已认证状态,应用自动跳转到主页。
内容的提问来源于stack exchange,提问作者Davood
相关产品推荐
相关产品推荐

