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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 11:55:55