Android端Axios请求401错误:React Native/Expo JWT认证循环问题
React Native/Expo Android端JWT认证循环问题
我正在开发首个React Native/Expo应用,在PHP服务器上实现了自定义JWT认证方案。该方案在iOS端运行正常,但Android端调用fetchUserData()时出现问题:token似乎未被成功存储,导致应用反复尝试刷新token,陷入循环。
已尝试的操作:
- 将Axios替换为Fetch
- 改用AsyncStorage存储token
但Android端问题依旧。
相关代码
import axios from 'axios'; import * as SecureStore from 'expo-secure-store'; const API_BASE_URL = 'https://serverdomain.url/backend'; // Add a request interceptor to log the actual URL being requested axios.interceptors.request.use(request => { console.log(`Intercepted Request: ${request.method.toUpperCase()} ${request.url}`); if (request.data) { console.log(`Payload: ${JSON.stringify(request.data)}`); } return request; }, error => { console.error('Request error:', error); return Promise.reject(error); }); // Function to store and retrieve tokens securely export async function setAccessToken(token) { await SecureStore.setItemAsync('access_token', token); } export async function getAccessToken() { return await SecureStore.getItemAsync('access_token'); } export async function clearAccessToken() { await SecureStore.deleteItemAsync('access_token'); } // API request wrapper with automatic token handling export async function apiRequest(endpoint, method = 'GET', data = null) { const token = await getAccessToken(); const url = `${API_BASE_URL}/${endpoint}`; try { const response = await axios({ url, method, data, headers: token ? { Authorization: `Bearer ${token}` } : {}, withCredentials: true, // Ensures HTTP-only cookies are included }); return response.data; } catch (error) { console.error(`Error requesting: ${method} ${url}`, error); if (error.response && error.response.status === 401) { // Attempt to refresh the token const newToken = await refreshAccessToken(); if (newToken) { return apiRequest(endpoint, method, data); } } throw error; } } // Refresh the access token export async function refreshAccessToken() { try { const response = await axios.post(`${API_BASE_URL}/refresh.php`, {}, { withCredentials: true }); const newToken = response.data.access_token; await setAccessToken(newToken); return newToken; } catch (error) { await clearAccessToken(); return null; } } // Fetch logged-in user's data export async function fetchUserData() { console.log(`${API_BASE_URL}/api/user.php`); const token = await getAccessToken(); console.log("Retrieved token:", token); try { const data = await apiRequest('api/user.php?id=1', 'POST', null); console.log("User data response:", data); return data; } catch (error) { console.error("Error fetching user data:", error.response ? error.response.data : error.message); return null; } }
Logcat日志
2025-02-12 10:55:36.806 1663-1676 GnssLocationProvider system_server I WakeLock released by handleMessage(REPORT_SV_STATUS, 0, com.android.server.location.GnssLocationProvider$SvStatusInfo@6a9178e) 2025-02-12 10:55:36.903 32421-3100 ReactNativeJS host.exp.exponent I Requesting new token 2025-02-12 10:55:37.014 32421-3100 ReactNativeJS host.exp.exponent I Setting token 2025-02-12 10:55:37.016 1508-1508 keystore keystore I 1 0 2025-02-12 10:55:37.020 1663-1677 BroadcastQueue system_server W Background execution not allowed: receiving Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } to com.google.android.gms/.stats.service.DropBoxEntryAddedReceiver 2025-02-12 10:55:37.020 1663-1677 BroadcastQueue system_server W Background execution not allowed: receiving Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } to com.google.android.gms/.chimera.GmsIntentOperationService$PersistentTrustedReceiver 2025-02-12 10:55:37.020 28684-28684 GmsReceiverSupport com.google.android.gms.persistent W intent throttled: Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } 2025-02-12 10:55:37.025 32421-3100 ReactNativeJS host.exp.exponent I Saving new token 2025-02-12 10:55:37.025 32421-3100 ReactNativeJS host.exp.exponent I New token obtained 2025-02-12 10:55:37.025 32421-3100 ReactNativeJS host.exp.exponent I Getting token 2025-02-12 10:55:37.026 1508-1508 keystore keystore I 1 0 2025-02-12 10:55:37.029 1663-1677 BroadcastQueue system_server W Background execution not allowed: receiving Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } to com.google.android.gms/.stats.service.DropBoxEntryAddedReceiver 2025-02-12 10:55:37.029 1663-1677 BroadcastQueue system_server W Background execution not allowed: receiving Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } to com.google.android.gms/.chimera.GmsIntentOperationService$PersistentTrustedReceiver 2025-02-12 10:55:37.029 28684-28684 GmsReceiverSupport com.google.android.gms.persistent W intent throttled: Intent { act=android.intent.action.DROPBOX_ENTRY_ADDED flg=0x10 (has extras) } 2025-02-12 10:55:37.030 32421-3100 ReactNativeJS host.exp.exponent I Calling API 2025-02-12 10:55:37.148 32421-3100 ReactNativeJS host.exp.exponent I Requesting new token 2025-02-12 10:55:37.266 32421-3100 ReactNativeJS host.exp.exponent I Setting token 2025-02-12 10:55:37.267 1508-1508 keystore keystore I 1 0 2025-02-12 10:55:37
问题排查与解决方案
1. 修复跨域Cookie配置(Android专属)
Android对跨域Cookie的处理比iOS严格,需确保PHP服务器的CORS响应头配置正确:
- 必须设置
Access-Control-Allow-Credentials: true Access-Control-Allow-Origin不能用*,要指定Expo应用的具体域名(比如https://exp.host/@your-username/your-app)refresh.php返回的refresh token cookie需设置SameSite=None、Secure属性(HTTPS环境下),确保跨域能携带
2. 解决SecureStore异步竞态问题
从日志看,token存储后立即读取可能存在延迟,修改refreshAccessToken验证存储结果:
export async function refreshAccessToken() { try { const response = await axios.post(`${API_BASE_URL}/refresh.php`, {}, { withCredentials: true }); const newToken = response.data.access_token; await setAccessToken(newToken); // 验证token是否成功存储 const storedToken = await getAccessToken(); if (!storedToken) { console.error("Token存储后无法读取"); return null; } return newToken; } catch (error) { await clearAccessToken(); return null; } }
3. 限制重试次数避免无限循环
当前apiRequest在401时无限制重试,添加重试次数上限:
export async function apiRequest(endpoint, method = 'GET', data = null, retryCount = 0) { const maxRetries = 1; // 仅重试1次 const token = await getAccessToken(); const url = `${API_BASE_URL}/${endpoint}`; try { const response = await axios({ url, method, data, headers: token ? { Authorization: `Bearer ${token}` } : {}, withCredentials: true, }); return response.data; } catch (error) { console.error(`Error requesting: ${method} ${url}`, error); if (error.response && error.response.status === 401 && retryCount < maxRetries) { const newToken = await refreshAccessToken(); if (newToken) { return apiRequest(endpoint, method, data, retryCount + 1); } } throw error; } }
4. 验证请求头是否携带token
在axios拦截器中添加日志,确认Authorization头是否正确:
axios.interceptors.request.use(request => { console.log(`Intercepted Request: ${request.method.toUpperCase()} ${request.url}`); console.log(`Authorization Header: ${request.headers.Authorization}`); // 新增此行 if (request.data) { console.log(`Payload: ${JSON.stringify(request.data)}`); } return request; }, error => { console.error('Request error:', error); return Promise.reject(error); });
5. 确认Expo权限配置
确保app.json中配置了必要的网络权限:
{ "expo": { "android": { "permissions": ["ACCESS_NETWORK_STATE", "INTERNET"] } } }
内容的提问来源于stack exchange,提问作者J Guerrero
相关产品推荐
相关产品推荐

