react-native的token認證流程

在 React Native 中實現 Token 認證是移動應用開發中的常見需求,它用于驗證用戶的身份并授權其訪問受保護的 API 資源。

Token 認證的核心流程:

  1. 用戶登錄 (Login):

    • 用戶在前端輸入用戶名和密碼。
    • 前端將這些憑據發送到后端 API。
    • 后端驗證憑據。如果驗證成功,后端會生成一個 Token(通常是 JWT - JSON Web Token)并返回給前端。
    • 前端接收到 Token 后,將其安全地存儲起來(例如使用 AsyncStorage)。
    • 前端更新用戶的登錄狀態(例如 Context 或 Redux 中的狀態)。
  2. 訪問受保護資源 (Access Protected Resources):

    • 前端需要訪問后端受保護的 API 時,將之前保存的 Token 附帶在請求的 Authorization 頭中發送給后端。
    • 后端接收到請求后,驗證 Token 的有效性(例如檢查簽名、有效期、是否被吊銷等)。
    • 如果 Token 有效,后端處理請求并返回數據。
    • 如果 Token 無效(過期、篡改等),后端會返回錯誤(通常是 401 Unauthorized 或 403 Forbidden),前端需要處理這些錯誤(例如引導用戶重新登錄)。
  3. Token 刷新 (Token Refresh - 可選但推薦):

    • 為了安全性,Token 通常有較短的有效期。
    • 當訪問 Token 過期時,前端可以使用一個 Refresh Token(通常有效期較長,也存儲在客戶端)向后端請求一個新的訪問 Token。
    • 后端驗證 Refresh Token,如果有效,則簽發新的訪問 Token。
    • 這減少了用戶頻繁重新登錄的次數,同時保持了安全性。
  4. 用戶登出 (Logout):

    • 用戶選擇登出時,前端從 AsyncStorage 中移除保存的 Token。
    • 前端清除用戶相關的狀態。
    • (可選)通知后端使該 Token 失效(如果后端支持)。

React Native 中的實現細節:

我們將使用以下技術棧:

  • @react-native-async-storage/async-storage: 用于在設備上持久化存儲 Token。
  • Context API (或 Redux/Zustand 等狀態管理庫): 用于在整個應用中管理用戶的登錄狀態和 Token。
  • fetch API (或 axios): 用于進行網絡請求。
  • crypto-js (可選,用于密碼哈希): 在發送密碼到后端前進行哈希處理,增加一層客戶端安全性(盡管主要安全在于 HTTPS 和后端處理)。

詳細代碼實現與示例

我們將構建一個簡化的應用,包含:

  1. UserContext.tsx: 管理用戶登錄狀態和 Token。
  2. AuthScreen.tsx: 登錄界面。
  3. HomeScreen.tsx: 登錄后的主界面,可以訪問受保護資源并登出。
  4. App.tsx: 應用入口,根據登錄狀態決定顯示哪個界面。
1. 安裝必要的庫

Bash

npm install @react-native-async-storage/async-storage crypto-js @react-navigation/native @react-navigation/stack
npx expo install react-native-screens react-native-safe-area-context # For react-navigation
# 或
yarn add @react-native-async-storage/async-storage crypto-js @react-navigation/native @react-navigation/stack
yarn expo install react-native-screens react-native-safe-area-context
2. 后端模擬 (Python Flask 示例)

為了能夠運行前端代碼,你需要一個簡單的后端。這里提供一個 Python Flask 的極簡示例。

backend/app.py

from flask import Flask, request, jsonify
from flask_cors import CORS
import jwt
import datetime
import hashlib # 用于密碼哈希app = Flask(__name__)
CORS(app) # 允許跨域請求SECRET_KEY = "your_super_secret_key_for_jwt" # 生產環境請使用更復雜的密鑰
REFRESH_SECRET_KEY = "your_super_secret_refresh_key_for_jwt"# 簡單模擬的用戶數據庫
users_db = {"testuser": {"password_hash": hashlib.sha256("testpassword".encode('utf-8')).hexdigest(), # 密碼哈希"user_id": "user123","user_name": "Test User","email": "test@example.com","phone": "123-456-7890"}
}@app.route('/api/user_login', methods=['POST'])
def user_login():data = request.get_json()username = data.get('username')password_hash = data.get('password') # 接收前端傳來的哈希后的密碼if not username or not password_hash:return jsonify({"message": "用戶名和密碼不能為空"}), 400user_data = users_db.get(username)if user_data and user_data["password_hash"] == password_hash:# 生成訪問 Token (有效期短,例如 15 分鐘)access_token_payload = {"user_id": user_data["user_id"],"user_name": user_data["user_name"],"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)}access_token = jwt.encode(access_token_payload, SECRET_KEY, algorithm="HS256")# 生成刷新 Token (有效期長,例如 7 天)refresh_token_payload = {"user_id": user_data["user_id"],"exp": datetime.datetime.utcnow() + datetime.timedelta(days=7)}refresh_token = jwt.encode(refresh_token_payload, REFRESH_SECRET_KEY, algorithm="HS256")return jsonify({"token": access_token,"refresh_token": refresh_token, # 將 refresh_token 也返回"user_id": user_data["user_id"],"user_name": user_data["user_name"],"email": user_data["email"],"phone": user_data["phone"],"message": "登錄成功"}), 200else:return jsonify({"message": "無效的用戶名或密碼"}), 401@app.route('/api/protected_data', methods=['GET'])
def protected_data():auth_header = request.headers.get('Authorization')if not auth_header or not auth_header.startswith('Bearer '):return jsonify({"message": "未授權:缺少或無效的Authorization頭"}), 401token = auth_header.split(" ")[1]try:payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])user_id = payload.get('user_id')user_name = payload.get('user_name', '未知用戶')return jsonify({"message": f"歡迎, {user_name}! 這是受保護的數據。", "user_id": user_id}), 200except jwt.ExpiredSignatureError:return jsonify({"message": "未授權:Token 已過期"}), 401except jwt.InvalidTokenError:return jsonify({"message": "未授權:無效的 Token"}), 401@app.route('/api/refresh_token', methods=['POST'])
def refresh_token():data = request.get_json()refresh_token = data.get('refresh_token')if not refresh_token:return jsonify({"message": "未授權:缺少刷新Token"}), 400try:payload = jwt.decode(refresh_token, REFRESH_SECRET_KEY, algorithms=["HS256"])user_id = payload.get('user_id')# 重新生成新的訪問 Tokenaccess_token_payload = {"user_id": user_id,"user_name": users_db.get(user_id, {}).get("user_name", "未知用戶"), # 重新獲取用戶名"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)}new_access_token = jwt.encode(access_token_payload, SECRET_KEY, algorithm="HS256")# 也可以同時生成新的刷新 Token,以實現刷新 Token 的滾動更新new_refresh_token_payload = {"user_id": user_id,"exp": datetime.datetime.utcnow() + datetime.timedelta(days=7)}new_refresh_token = jwt.encode(new_refresh_token_payload, REFRESH_SECRET_KEY, algorithm="HS256")return jsonify({"token": new_access_token,"refresh_token": new_refresh_token # 返回新的刷新 Token}), 200except jwt.ExpiredSignatureError:return jsonify({"message": "未授權:刷新 Token 已過期,請重新登錄"}), 401except jwt.InvalidTokenError:return jsonify({"message": "未授權:無效的刷新 Token"}), 401if __name__ == '__main__':# 注意:10.0.2.2 是 Android 模擬器訪問宿主機(開發機器)的默認 IP# 如果是 iOS 模擬器或真實設備,請使用你電腦的局域網 IPapp.run(host='10.0.2.2', port=5000, debug=True)

運行后端:

  1. 保存為 backend/app.py
  2. 安裝依賴:pip install Flask Flask-Cors PyJWT
  3. 運行:python backend/app.py
3. React Native 前端代碼
src/contexts/UserContext.tsx

這是核心,負責管理用戶認證狀態、Token 的存儲與獲取。

// src/contexts/UserContext.tsx
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SHA256 } from 'crypto-js'; // 用于前端密碼哈希// 定義用戶信息接口
export interface UserInfo {user_id: string;user_name: string;email: string;phone: string;status: 'logged_in' | 'logged_out';
}// 定義 Context 的類型
interface UserContextType {user: UserInfo | null; // 當前登錄的用戶信息authToken: string | null; // 訪問 TokenrefreshToken: string | null; // 刷新 Tokenlogin: (username: string, password: string) => Promise<void>; // 登錄方法logout: () => void; // 登出方法isLoading: boolean; // 是否正在加載用戶數據 (應用啟動時)refreshAccessToken: () => Promise<boolean>; // 刷新訪問 Token 的方法
}// 創建 Context,并設置默認值
const UserContext = createContext<UserContextType>({user: null,authToken: null,refreshToken: null,login: async () => { /* no-op */ },logout: () => { /* no-op */ },isLoading: true, // 初始加載狀態為 truerefreshAccessToken: async () => false, // 默認返回 false
});// UserProvider 組件
const UserProvider = ({ children }: { children: ReactNode }) => {const [user, setUser] = useState<UserInfo | null>(null);const [authToken, setAuthToken] = useState<string | null>(null);const [refreshToken, setRefreshToken] = useState<string | null>(null);const [isLoading, setIsLoading] = useState(true);// --- 1. 應用啟動時加載存儲的用戶和 Token 信息 ---useEffect(() => {const loadStoredAuth = async () => {try {const storedAuthToken = await AsyncStorage.getItem('authToken');const storedRefreshToken = await AsyncStorage.getItem('refreshToken');const storedUserId = await AsyncStorage.getItem('userId');const storedUserName = await AsyncStorage.getItem('userName'); // 假設也存儲了用戶名if (storedAuthToken && storedRefreshToken && storedUserId && storedUserName) {setAuthToken(storedAuthToken);setRefreshToken(storedRefreshToken);// 這里可以根據實際情況從 API 重新獲取完整的用戶信息,// 或者直接使用存儲的信息(如果它足夠完整)setUser({user_id: storedUserId,user_name: storedUserName,email: 'N/A', // 示例,實際應從API獲取或存儲phone: 'N/A', // 示例status: 'logged_in',});console.log('User and tokens loaded from AsyncStorage.');} else {console.log('No existing user data or tokens found.');setUser(null);setAuthToken(null);setRefreshToken(null);}} catch (error) {console.error('Error loading user data from AsyncStorage:', error);setUser(null);setAuthToken(null);setRefreshToken(null);} finally {setIsLoading(false); // 加載完成}};loadStoredAuth();}, []); // 空依賴數組,只在組件掛載時運行一次// --- 2. 登錄方法 ---const login = useCallback(async (username: string, password: string) => {try {const hashedPassword = SHA256(password).toString(); // 客戶端密碼哈希console.log('Hashed Password (Frontend):', hashedPassword);const response = await fetch('http://10.0.2.2:5000/api/user_login', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ username, password: hashedPassword }),});if (!response.ok) {const errorData = await response.json();throw new Error(errorData.message || 'Login failed');}const data = await response.json();if (!data.token || !data.refresh_token || !data.user_id) {throw new Error('Invalid API response structure: missing token or user_id');}// 保存 Token 和用戶 IDawait AsyncStorage.setItem('authToken', data.token);await AsyncStorage.setItem('refreshToken', data.refresh_token);await AsyncStorage.setItem('userId', data.user_id);await AsyncStorage.setItem('userName', data.user_name); // 也存儲用戶名// 更新 Context 狀態setAuthToken(data.token);setRefreshToken(data.refresh_token);setUser({user_id: data.user_id,user_name: data.user_name || username,email: data.email || 'N/A',phone: data.phone || 'N/A',status: 'logged_in',});console.log('Login successful, user and tokens set.');} catch (error) {console.error('Login error:', error);// 登錄失敗時清除所有狀態await logout(); // 調用登出方法清除所有相關信息throw error; // 重新拋出錯誤以便調用方處理}}, []); // 空依賴數組,確保 login 函數的穩定性// --- 3. 登出方法 ---const logout = useCallback(async () => {console.log('Logging out...');await AsyncStorage.removeItem('authToken');await AsyncStorage.removeItem('refreshToken');await AsyncStorage.removeItem('userId');await AsyncStorage.removeItem('userName');setUser(null);setAuthToken(null);setRefreshToken(null);console.log('User logged out, tokens cleared.');}, []); // 空依賴數組,確保 logout 函數的穩定性// --- 4. 刷新訪問 Token 的方法 ---const refreshAccessToken = useCallback(async (): Promise<boolean> => {if (!refreshToken) {console.warn('No refresh token available. Cannot refresh access token.');await logout(); // 沒有刷新 Token,視為需要重新登錄return false;}try {console.log('Attempting to refresh access token...');const response = await fetch('http://10.0.2.2:5000/api/refresh_token', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ refresh_token: refreshToken }),});if (!response.ok) {const errorData = await response.json();console.error('Failed to refresh token:', errorData.message);await logout(); // 刷新失敗,強制登出return false;}const data = await response.json();if (!data.token || !data.refresh_token) { // 后端可能也返回新的刷新tokenconsole.error('Invalid refresh token response.');await logout();return false;}await AsyncStorage.setItem('authToken', data.token);await AsyncStorage.setItem('refreshToken', data.refresh_token); // 保存新的刷新 TokensetAuthToken(data.token);setRefreshToken(data.refresh_token); // 更新狀態console.log('Access token refreshed successfully.');return true;} catch (error) {console.error('Error during token refresh:', error);await logout(); // 網絡或其他錯誤,強制登出return false;}}, [refreshToken, logout]); // 依賴 refreshToken 和 logout,確保是最新的// 提供的 Context 值const contextValue = {user,authToken,refreshToken,login,logout,isLoading,refreshAccessToken,};return (<UserContext.Provider value={contextValue}>{children}</UserContext.Provider>);
};export default UserProvider;// 自定義 Hook,方便組件使用 Context
export const useUser = () => useContext(UserContext);
src/screens/AuthScreen.tsx

登錄界面,用戶輸入憑據并調用 login 方法。

// src/screens/AuthScreen.tsx
import React, { useState } from 'react';
import { View, Text, TextInput, Button, StyleSheet, ActivityIndicator, Alert } from 'react-native';
import { useUser } from '../contexts/UserContext'; // 引入 useUser Hookfunction AuthScreen() {const [username, setUsername] = useState('testuser'); // 默認用戶名const [password, setPassword] = useState('testpassword'); // 默認密碼const [isAuthenticating, setIsAuthenticating] = useState(false); // 登錄狀態const { login } = useUser(); // 獲取登錄方法const handleLogin = async () => {setIsAuthenticating(true); // 開始登錄try {await login(username, password); // 調用 Context 中的登錄方法// 登錄成功,Context 會自動更新 user 狀態,App.tsx 會負責導航} catch (error: any) {Alert.alert('登錄失敗', error.message || '請檢查用戶名和密碼。');console.error('Login error in AuthScreen:', error);} finally {setIsAuthenticating(false); // 結束登錄}};return (<View style={styles.container}><Text style={styles.title}>用戶登錄</Text><TextInputstyle={styles.input}placeholder="用戶名"value={username}onChangeText={setUsername}autoCapitalize="none"/><TextInputstyle={styles.input}placeholder="密碼"value={password}onChangeText={setPassword}secureTextEntry/><Buttontitle={isAuthenticating ? "登錄中..." : "登錄"}onPress={handleLogin}disabled={isAuthenticating} // 登錄中禁用按鈕/>{isAuthenticating && <ActivityIndicator style={styles.spinner} size="small" color="#0000ff" />}</View>);
}const styles = StyleSheet.create({container: {flex: 1,justifyContent: 'center',alignItems: 'center',padding: 20,backgroundColor: '#f5f5f5',},title: {fontSize: 28,fontWeight: 'bold',marginBottom: 30,color: '#333',},input: {width: '100%',padding: 15,borderWidth: 1,borderColor: '#ddd',borderRadius: 8,marginBottom: 15,backgroundColor: '#fff',},spinner: {marginTop: 10,},
});export default AuthScreen;
src/screens/HomeScreen.tsx

登錄后的主界面,可以訪問受保護資源并登出。

TypeScript

// src/screens/HomeScreen.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet, ActivityIndicator, Alert } from 'react-native';
import { useUser } from '../contexts/UserContext'; // 引入 useUser Hookfunction HomeScreen() {const { user, authToken, logout, refreshAccessToken } = useUser(); // 獲取用戶信息、token、登出、刷新方法const [protectedData, setProtectedData] = useState<string | null>(null);const [isFetchingData, setIsFetchingData] = useState(false);// --- 訪問受保護數據的方法 ---const fetchProtectedData = async () => {if (!authToken) {Alert.alert('錯誤', '沒有訪問 Token,請重新登錄。');return;}setIsFetchingData(true);try {const response = await fetch('http://10.0.2.2:5000/api/protected_data', {headers: {'Authorization': `Bearer ${authToken}`, // 在 Authorization 頭中攜帶 Token},});if (response.status === 401) { // Unauthorized (Token 可能過期或無效)const errorData = await response.json();if (errorData.message === "未授權:Token 已過期") {console.log('Access Token expired, attempting to refresh...');const refreshed = await refreshAccessToken(); // 嘗試刷新 Tokenif (refreshed) {// 如果刷新成功,可以考慮重新嘗試本次請求 (更高級的實現會在攔截器中自動重試)Alert.alert('提示', '訪問 Token 已刷新,請重新嘗試獲取數據。');} else {Alert.alert('會話過期', '您的會話已過期,請重新登錄。');}} else {Alert.alert('認證失敗', errorData.message || '無效的訪問 Token。');}await logout(); // 遇到 401 且無法刷新,通常需要登出return;}if (!response.ok) {const errorData = await response.json();throw new Error(errorData.message || '獲取受保護數據失敗。');}const data = await response.json();setProtectedData(data.message);} catch (error: any) {Alert.alert('錯誤', `獲取數據失敗: ${error.message}`);console.error('Error fetching protected data:', error);} finally {setIsFetchingData(false);}};return (<View style={styles.container}><Text style={styles.title}>歡迎,{user?.user_name || '用戶'}!</Text><Text style={styles.info}>您的用戶ID: {user?.user_id}</Text>{authToken && (<Text style={styles.info}>訪問 Token: {authToken.substring(0, 20)}...</Text>)}{refreshToken && (<Text style={styles.info}>刷新 Token: {refreshToken.substring(0, 20)}...</Text>)}<Buttontitle={isFetchingData ? "獲取中..." : "獲取受保護數據"}onPress={fetchProtectedData}disabled={isFetchingData || !authToken} // 沒有 Token 或正在獲取時禁用/>{isFetchingData && <ActivityIndicator style={styles.spinner} size="small" color="#0000ff" />}{protectedData && (<View style={styles.dataContainer}><Text style={styles.dataTitle}>受保護數據:</Text><Text style={styles.dataText}>{protectedData}</Text></View>)}<View style={styles.logoutButtonContainer}><Button title="登出" onPress={logout} color="red" /></View></View>);
}const styles = StyleSheet.create({container: {flex: 1,justifyContent: 'center',alignItems: 'center',padding: 20,backgroundColor: '#f5f5f5',},title: {fontSize: 24,fontWeight: 'bold',marginBottom: 10,color: '#333',},info: {fontSize: 16,marginBottom: 5,color: '#555',},spinner: {marginTop: 10,},dataContainer: {marginTop: 30,padding: 15,borderWidth: 1,borderColor: '#ccc',borderRadius: 8,backgroundColor: '#e9e9e9',width: '100%',alignItems: 'center',},dataTitle: {fontSize: 18,fontWeight: 'bold',marginBottom: 10,color: '#333',},dataText: {fontSize: 16,textAlign: 'center',color: '#666',},logoutButtonContainer: {marginTop: 40,width: '80%',},
});export default HomeScreen;
src/navigation/MainNavigator.tsx (主應用內部導航)

TypeScript

// src/navigation/MainNavigator.tsx
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from '../screens/HomeScreen';
// 你可以添加更多登錄后的頁面const Stack = createStackNavigator();function MainNavigator() {return (<Stack.Navigator screenOptions={{ headerShown: false }}><Stack.Screen name="Home" component={HomeScreen} />{/* <Stack.Screen name="Settings" component={SettingsScreen} /> */}</Stack.Navigator>);
}export default MainNavigator;
src/navigation/AuthNavigator.tsx (認證頁面導航)
// src/navigation/AuthNavigator.tsx
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import AuthScreen from '../screens/AuthScreen';const Stack = createStackNavigator();function AuthNavigator() {return (<Stack.Navigator screenOptions={{ headerShown: false }}><Stack.Screen name="Login" component={AuthScreen} />{/* <Stack.Screen name="Register" component={RegisterScreen} /> */}</Stack.Navigator>);
}export default AuthNavigator;
App.tsx (應用入口)

這是應用的主文件,它設置了 UserProvider 并根據用戶的登錄狀態來切換導航器。

TypeScript

// App.tsx
import 'react-native-gesture-handler'; // react-navigation 必備
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { ActivityIndicator, View, StyleSheet, Text } from 'react-native';import UserProvider, { useUser } from './src/contexts/UserContext'; // 引入你的 UserProvider 和 useUser Hook
import MainNavigator from './src/navigation/MainNavigator';
import AuthNavigator from './src/navigation/AuthNavigator';const Stack = createStackNavigator();// 根導航器,根據認證狀態切換主/認證流程
function RootNavigator() {const { user, isLoading } = useUser(); // 從 Context 獲取 user 和 isLoading 狀態if (isLoading) {// 如果正在加載用戶數據,顯示加載指示器或啟動屏return (<View style={styles.loadingContainer}><ActivityIndicator size="large" color="#0000ff" /><Text style={styles.loadingText}>正在加載應用數據...</Text></View>);}return (<Stack.Navigator screenOptions={{ headerShown: false }}>{user ? (// 如果 user 不為 null (已登錄),顯示主應用界面<Stack.Screen name="Main" component={MainNavigator} />) : (// 如果 user 為 null (未登錄),顯示認證界面<Stack.Screen name="Auth" component={AuthNavigator} />)}</Stack.Navigator>);
}export default function App() {return (<NavigationContainer><UserProvider><RootNavigator /></UserProvider></NavigationContainer>);
}const styles = StyleSheet.create({loadingContainer: {flex: 1,justifyContent: 'center',alignItems: 'center',backgroundColor: '#fff',},loadingText: {marginTop: 10,fontSize: 16,color: '#555',},
});
4. 項目結構
my-react-native-app/
├── App.tsx
├── package.json
├── src/
│   ├── contexts/
│   │   └── UserContext.tsx
│   ├── navigation/
│   │   ├── AuthNavigator.tsx
│   │   └── MainNavigator.tsx
│   └── screens/
│       ├── AuthScreen.tsx
│       └── HomeScreen.tsx
└── backend/ (你的后端代碼,例如 app.py)└── app.py

總結

  • UserProvider: 是 Token 認證的核心。它負責在應用啟動時從 AsyncStorage 中加載 Token,管理登錄/登出邏輯,并通過 Context API 將 user 信息、authTokenrefreshTokenisLoading 狀態暴露給整個應用。
  • isLoading 狀態: 用于在應用啟動時,等待 UserProviderAsyncStorage 加載完數據。在這期間,你應該顯示一個加載指示器,避免閃爍或提前渲染錯誤內容。
  • login 方法: 用戶登錄成功后,從后端獲取 Token 并將其保存在 AsyncStorage 和 Context 狀態中。
  • logout 方法: 清除 AsyncStorage 中的 Token 和 Context 狀態。
  • refreshAccessToken 方法: 處理訪問 Token 過期的情況。它會嘗試使用刷新 Token 獲取新的訪問 Token。
  • 網絡請求: 在需要訪問受保護資源時,從 useUser() 中獲取 authToken,并將其添加到請求的 Authorization: Bearer <token> 頭中。
  • 錯誤處理: 特別是對于 401/403 錯誤,需要捕獲并引導用戶重新認證(例如,跳轉到登錄頁)。

這個示例提供了一個基本但完整的 React Native Token 認證流程。在生產環境中,你可能還需要考慮更復雜的錯誤處理、Token 有效性檢查、安全性增強(如 HTTPS)、更強大的網絡請求庫(如 Axios 及其攔截器)以及更完善的用戶信息管理。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/82634.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/82634.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/82634.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Dify:詳解 docker-compose.yaml配置文件

詳解 docker-compose.yaml 配置文件 docker-compose.yaml 是用于定義和運行多容器 Docker 應用的配置文件。下面&#xff0c;我們將詳細解釋您提供的 docker-compose.yaml 文件&#xff0c;包括各個服務的作用、配置&#xff0c;以及它們與 .env 文件之間的關系。 文件概覽 自…

Python基于Django的主觀題自動閱卷系統【附源碼、文檔說明】

博主介紹&#xff1a;?Java老徐、7年大廠程序員經歷。全網粉絲12w、csdn博客專家、掘金/華為云/阿里云/InfoQ等平臺優質作者、專注于Java技術領域和畢業項目實戰? &#x1f345;文末獲取源碼聯系&#x1f345; &#x1f447;&#x1f3fb; 精彩專欄推薦訂閱&#x1f447;&…

今日行情明日機會——20250528

上證指數縮量收小陰線&#xff0c;個股跌多漲少&#xff0c;總體情緒偏差&#xff0c;注意風險為主。 深證指數&#xff0c;縮量收小陰線&#xff0c;連續5天陰線&#xff0c;明后天反彈的概率增大&#xff0c;但仍要注意風險。 2025年5月28日漲停股主要行業方向分析 1. 無人…

基于stm32LORA無線抄表系統仿真

資料下載地址&#xff1a;基于stm32LORA無線抄表系統仿真 1、項目介紹 基于LoRa的無線通信的電力抄表系統&#xff0c;采集節點數據&#xff0c;通過LoRa無線通信進行數據傳輸&#xff0c;最后再網關節點上顯示。 2、仿真圖 3、仿真代碼 #include "oled.h" #incl…

不同電腦同一個網絡ip地址一樣嗎

不同電腦在連接同一個WiFi時&#xff0c;它們的IP地址會相同嗎&#xff1f;相信不少朋友都對這個問題感到好奇&#xff0c;今天我們就來詳細探討一下。 一、基礎概念&#xff1a;IP地址的本質與分類 IP地址是分配給網絡設備的唯一標識符&#xff0c;用于在互聯網或局域網中定位…

CentOS 7 下 Redis 從 5.0 升級至 7.4.3 全流程實踐

目錄 前言1 查看 Redis 運行情況與配置1.1 查看 Redis 是否正在運行1.2 連接 Redis 服務并獲取配置信息1.3 查找 redis.conf 配置文件位置 2 關閉舊版本 Redis 實例2.1 使用客戶端命令關閉 Redis2.2 驗證 Redis 是否完全關閉 3 升級 GCC 編譯環境3.1 檢查當前 GCC 版本3.2 安裝…

SQLord: 基于反向數據生成和任務拆解的 Text-to-SQL 企業落地方案

曾在Text-to-SQL方向做過深入的研究&#xff0c;以此為基礎研發的DataAgent在B2B平臺成功落地&#xff0c;因此作為第一作者&#xff0c;在 The Web Conference (WWW’2025, CCF-A) 會議上發表了相關論文&#xff1a; SQLord: A Robust Enterprise Text-to-SQL Solution via R…

內網搭建NTS服務器

內網搭建NTS服務器 關鍵字 : ntp nts ipv6 NTS 是 Network Time Security&#xff08;網絡時間安全&#xff09;的縮寫,是 NTP 的一種安全擴展機制。它利用傳輸層安全&#xff08;TLS&#xff09;和相關數據的認證加密&#xff08;AEAD&#xff09;&#xff0c;為 NTP 的客戶…

AD9268、AD9643調試過程中遇到的問題

Ad9268芯片 AD9268是一款雙通道、16位、80 MSPS/105 MSPS/125 MSPS模數轉換器(ADC)。AD9268旨在支持要求高性能、低成本、小尺寸和多功能的通信應用。雙通道ADC內核采用多級差分流水線架構&#xff0c;集成輸出糾錯邏輯。每個ADC都具有寬帶寬、差分采樣保持模擬輸入放大器&…

用豆包寫單元測試

用豆包寫單元測試&#xff0c; 輸入 vue 模板內容&#xff0c;輸入 參考vue模板內容寫一個單元測試要求用jest.mock實現構造完成&#xff0c;修復bug。npm run test:unit – tests/unit/views/xxx/xxx.spec.js看下 % Stmts 語句覆蓋率&#xff1a;執行到的代碼語句占總語句的比…

css樣式塊重復調用

通譯靈碼解釋。還給了一些示例&#xff0c;包含傳參等內容 scss和sass的區別。scss與sass是兩種樣式編寫風格&#xff0c;scss是大括號加;號形式。而sass是縮進的格式使用scss為什么要要安裝sass呢。sass是一門css預處理器語言。所以要安裝。

【深度學習新浪潮】以圖搜地點是如何實現的?(含大模型方案)

1. 以圖搜地點的實現方式有哪些? 掃描手機照片中的截圖并識別出位置信息,主要有以下幾種實現方式: 通過照片元數據獲取: 原理:現代智能手機拍攝的照片通常會包含Exif(Exchangeable Image File)元數據。Exif中除了有像素信息之外,還包含了光圈、快門、白平衡、ISO、焦距…

DeepSeek R1 與 V3 的全面對比,兩個版本有什么差別?

DeepSeek R1與DeepSeek V3是深度求索&#xff08;DeepSeek&#xff09;公司推出的兩款定位不同的大語言模型&#xff0c;界面上用戶可選擇基礎模型(V3)、深度思考(R1)、聯網搜索。 基礎模型(V3)是DeepSeek的標配,沒有勾選默認就是基礎模型。為了讓用戶更清晰地了解兩款模型的差…

Spring Boot 深度集成 Ollama 指南:從聊天模型配置到生產級應用開發

Spring Boot 深度集成 Ollama 指南&#xff1a;從聊天模型配置到生產級應用開發 前言 在人工智能應用開發中&#xff0c;大語言模型&#xff08;LLM&#xff09;的本地化部署需求日益增長。Ollama 作為開源的本地LLM運行平臺&#xff0c;支持Mistral、LLaMA等主流模型&#x…

查詢oracle進程數和會話數進行優化

查看當前參數配置 首先需要查詢當前的 processes 和 sessions 參數值&#xff0c;以確定是否需要調整。 SQL SHOW PARAMETER processes; SHOW PARAMETER sessions; 這些命令可以顯示當前實例中允許的最大進程數和會話數 查詢當前連接數&#xff0c;查詢并發會話 SELECT COUNT…

頂會新方向:卡爾曼濾波+目標檢測

卡爾曼慮波&#xff0b;目標檢測創新結合&#xff0c;新作準確率突破100%! 一個有前景且好發論文的方向:卡爾曼濾波&#xff0b;目標檢測! 這種創新結合&#xff0c;得到學術界的廣泛認可&#xff0c;多篇成果陸續登上頂會頂刊。例如無人機競速系統 Swift&#xff0c;登上nat…

運維自動化工具 ansible 知識點總結

1.Ansible 基礎 1.1 Ansible簡介 Ansible 是一個開源軟件&#xff0c;提供配置管理和應用程序部署等項目通用的管理功能。它主要運行在類 Unix 系統上&#xff0c;通過特性語言來描述各種資源對象&#xff0c;進而管理類 Unix 系統和 Microsoft Windows 系統等系統資源。 官網…

基于python,html,flask,echart,ids/ips,VMware,mysql,在線sdn防御ddos系統

詳細視頻:【基于python,html,flask,echart,ids/ips,VMware,mysql,在線sdn防御ddos系統-嗶哩嗶哩】 https://b23.tv/azUqQXe

C語言進階--數據的存儲

1.數據類型介紹 內置類型 char //字符數據類型 1字節 short //短整型 2字節 int //整型 4字節 long //長整型 4/8字節 long long //更長的整型 8字節 (C99中引入的) float //單精度浮點數 4字節 double //雙精度浮點數 8字節sizeof(long…

C++學習細節回顧(匯總三)

一.多態概念 同樣是動物叫的?個?為(函數)&#xff0c;傳貓對象過去&#xff0c;就是”(>ω<)喵“&#xff0c;傳狗對象過去&#xff0c;就是"汪汪"。 1.根據對象不同類型&#xff0c;調用不同函數&#xff0c;這就叫做運行時多態(動態多態) 2.編譯時多態(靜態…