乐闻世界logo
搜索文章和话题

移动应用中如何实施 CSRF 防护,有哪些特殊考虑?

2月19日 17:57

移动应用中的 CSRF 防护与 Web 应用有所不同,因为移动应用通常不使用浏览器自动发送 Cookie 的机制,但仍然需要考虑各种安全风险。

移动应用 CSRF 的特殊性

1. 认证方式差异

Web 应用

  • 使用 Cookie 存储 Session ID
  • 浏览器自动发送 Cookie
  • 容易受到 CSRF 攻击

移动应用

  • 使用 Token(JWT、OAuth)
  • 手动管理认证信息
  • 相对不易受到传统 CSRF 攻击
  • 但存在其他安全风险

2. 网络环境差异

javascript
// 移动应用面临的网络挑战 const mobileChallenges = { networkUnreliable: '网络不稳定可能导致重放攻击', deviceCompromise: '设备被 Root 或越狱', appTampering: '应用被篡改或重新打包', insecureStorage: '不安全的本地存储' };

移动应用 CSRF 防护策略

1. 使用 Token 认证(推荐)

iOS 实现

swift
// Swift - Token 认证管理 class TokenManager { static let shared = TokenManager() private let keychain = Keychain() func saveToken(_ token: String) { // 使用 Keychain 安全存储 Token keychain["authToken"] = token } func getToken() -> String? { return keychain["authToken"] } func clearToken() { keychain["authToken"] = nil } } // 网络请求管理器 class NetworkManager { static let shared = NetworkManager() private let session = URLSession.shared func request<T: Decodable>( _ endpoint: String, method: String = "GET", body: Data? = nil, completion: @escaping (Result<T, Error>) -> Void ) { guard let url = URL(string: endpoint) else { completion(.failure(NetworkError.invalidURL)) return } var request = URLRequest(url: url) request.httpMethod = method request.httpBody = body // 添加认证 Token if let token = TokenManager.shared.getToken() { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } session.dataTask(with: request) { data, response, error in if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(NetworkError.noData)) return } do { let result = try JSONDecoder().decode(T.self, from: data) completion(.success(result)) } catch { completion(.failure(error)) } }.resume() } }

Android 实现

java
// Java - Token 认证管理 public class TokenManager { private static TokenManager instance; private SharedPreferences preferences; private TokenManager(Context context) { preferences = context.getSharedPreferences("auth", Context.MODE_PRIVATE); } public static synchronized TokenManager getInstance(Context context) { if (instance == null) { instance = new TokenManager(context); } return instance; } public void saveToken(String token) { preferences.edit().putString("authToken", token).apply(); } public String getToken() { return preferences.getString("authToken", null); } public void clearToken() { preferences.edit().remove("authToken").apply(); } } // 网络请求拦截器 public class AuthInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); // 添加认证 Token String token = TokenManager.getInstance(context).getToken(); if (token != null) { Request authenticatedRequest = originalRequest.newBuilder() .header("Authorization", "Bearer " + token) .build(); return chain.proceed(authenticatedRequest); } return chain.proceed(originalRequest); } }

2. 设备指纹识别

javascript
// 服务器端 - 设备指纹验证 class DeviceFingerprintService { async generateFingerprint(deviceInfo) { const fingerprintData = { deviceId: deviceInfo.deviceId, os: deviceInfo.os, osVersion: deviceInfo.osVersion, appVersion: deviceInfo.appVersion, screenResolution: deviceInfo.screenResolution, timestamp: Date.now() }; // 生成设备指纹 const fingerprint = crypto.createHash('sha256') .update(JSON.stringify(fingerprintData)) .digest('hex'); return fingerprint; } async validateFingerprint(userId, fingerprint) { const storedFingerprint = await this.getStoredFingerprint(userId); if (!storedFingerprint) { // 首次使用,存储指纹 await this.storeFingerprint(userId, fingerprint); return true; } // 验证指纹是否匹配 return storedFingerprint === fingerprint; } }

3. 请求签名

swift
// Swift - 请求签名 class RequestSigner { static func signRequest(_ request: URLRequest, secretKey: String) -> URLRequest { var signedRequest = request // 生成时间戳和随机数 let timestamp = String(Int(Date().timeIntervalSince1970)) let nonce = UUID().uuidString // 构造签名字符串 let method = request.httpMethod ?? "GET" let url = request.url?.absoluteString ?? "" let body = request.httpBody?.base64EncodedString() ?? "" let signString = "\(method)\n\(url)\n\(timestamp)\n\(nonce)\n\(body)" // 生成 HMAC-SHA256 签名 let signature = signString.hmacSHA256(key: secretKey) // 添加签名头 signedRequest.setValue(timestamp, forHTTPHeaderField: "X-Timestamp") signedRequest.setValue(nonce, forHTTPHeaderField: "X-Nonce") signedRequest.setValue(signature, forHTTPHeaderField: "X-Signature") return signedRequest } } // HMAC-SHA256 扩展 extension String { func hmacSHA256(key: String) -> String { let keyData = key.data(using: .utf8)! let messageData = self.data(using: .utf8)! var digestData = Data(count: Int(CC_SHA256_DIGEST_LENGTH)) keyData.withUnsafeBytes { keyBytes in messageData.withUnsafeBytes { messageBytes in CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyBytes.baseAddress, keyData.count, messageBytes.baseAddress, messageData.count, &digestData) } } return digestData.base64EncodedString() } }
java
// Java - 请求签名 public class RequestSigner { public static Request signRequest(Request request, String secretKey) { // 生成时间戳和随机数 String timestamp = String.valueOf(System.currentTimeMillis() / 1000); String nonce = UUID.randomUUID().toString(); // 构造签名字符串 String method = request.method(); String url = request.url().toString(); String body = bodyToString(request.body()); String signString = method + "\n" + url + "\n" + timestamp + "\n" + nonce + "\n" + body; // 生成 HMAC-SHA256 签名 String signature = hmacSHA256(signString, secretKey); // 添加签名头 return request.newBuilder() .addHeader("X-Timestamp", timestamp) .addHeader("X-Nonce", nonce) .addHeader("X-Signature", signature) .build(); } private static String hmacSHA256(String data, String key) { try { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "HmacSHA256"); mac.init(secretKeySpec); byte[] hash = mac.doFinal(data.getBytes()); return Base64.encodeToString(hash, Base64.NO_WRAP); } catch (Exception e) { throw new RuntimeException("Failed to generate signature", e); } } }

4. 双因素认证

swift
// Swift - 双因素认证 class TwoFactorAuthManager { static func verifyOTP(userId: String, otp: String, completion: @escaping (Bool) -> Void) { // 发送 OTP 到服务器验证 let endpoint = "https://api.example.com/verify-otp" var request = URLRequest(url: URL(string: endpoint)!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body = [ "userId": userId, "otp": otp ] request.httpBody = try? JSONSerialization.data(withJSONObject: body) URLSession.shared.dataTask(with: request) { data, response, error in if let httpResponse = response as? HTTPURLResponse { completion(httpResponse.statusCode == 200) } else { completion(false) } }.resume() } }

服务器端验证

1. Token 验证中间件

javascript
// Express.js - Token 验证中间件 const jwt = require('jsonwebtoken'); function authenticateMobile(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'No token provided' }); } const token = authHeader.substring(7); try { const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; next(); } catch (error) { return res.status(401).json({ error: 'Invalid token' }); } } // 请求签名验证中间件 function verifySignature(req, res, next) { const timestamp = req.headers['x-timestamp']; const nonce = req.headers['x-nonce']; const signature = req.headers['x-signature']; if (!timestamp || !nonce || !signature) { return res.status(401).json({ error: 'Missing signature headers' }); } // 验证时间戳(防止重放攻击) const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) { // 5 分钟 return res.status(401).json({ error: 'Request expired' }); } // 验证 nonce(防止重放攻击) if (isNonceUsed(nonce)) { return res.status(401).json({ error: 'Nonce already used' }); } // 验证签名 const expectedSignature = generateSignature(req, timestamp, nonce); if (signature !== expectedSignature) { return res.status(401).json({ error: 'Invalid signature' }); } // 标记 nonce 为已使用 markNonceAsUsed(nonce); next(); }

2. 设备指纹验证

javascript
// 设备指纹验证中间件 function verifyDeviceFingerprint(req, res, next) { const userId = req.user.id; const fingerprint = req.headers['x-device-fingerprint']; if (!fingerprint) { return res.status(401).json({ error: 'Device fingerprint required' }); } deviceFingerprintService.validateFingerprint(userId, fingerprint) .then(isValid => { if (!isValid) { return res.status(401).json({ error: 'Invalid device fingerprint' }); } next(); }) .catch(error => { res.status(500).json({ error: 'Fingerprint validation failed' }); }); }

最佳实践

1. 安全存储

swift
// iOS - 使用 Keychain import Security class KeychainHelper { static func save(key: String, data: Data) -> Bool { let query = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data ] as [String: Any] SecItemDelete(query as CFDictionary) return SecItemAdd(query as CFDictionary, nil) == errSecSuccess } static func load(key: String) -> Data? { let query = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] as [String: Any] var dataTypeRef: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) if status == errSecSuccess { return dataTypeRef as? Data } return nil } }
java
// Android - 使用 EncryptedSharedPreferences public class SecureStorage { private static EncryptedSharedPreferences preferences; public static void init(Context context) { try { MasterKey masterKey = new MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); preferences = (EncryptedSharedPreferences) EncryptedSharedPreferences.create( context, "secure_prefs", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); } catch (Exception e) { throw new RuntimeException("Failed to initialize secure storage", e); } } public static void saveString(String key, String value) { preferences.edit().putString(key, value).apply(); } public static String getString(String key) { return preferences.getString(key, null); } }

2. 证书绑定

swift
// iOS - SSL 证书绑定 class CertificatePinning { static func validateCertificate(for serverTrust: SecTrust) -> Bool { // 获取服务器证书 guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else { return false } // 获取证书数据 let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data // 加载本地证书 guard let localCertificatePath = Bundle.main.path(forResource: "certificate", ofType: "cer"), let localCertificateData = try? Data(contentsOf: URL(fileURLWithPath: localCertificatePath)) else { return false } // 比较证书 return serverCertificateData == localCertificateData } }

移动应用的 CSRF 防护需要结合平台特性和安全最佳实践,确保在各种网络环境和设备状态下都能提供有效的安全保护。

标签:CSRF