安全性大揭秘!JWT Token与Session在Web与移动App端的所有存储对比,你选对了吗?

602 阅读14分钟

JWT Token vs Session:全面对比与存储方式解析

在现代 Web 应用中,用户身份验证与会话管理是至关重要的部分。随着技术的发展,开发者有多种方式来实现这一功能,其中最常用的两种方式是 JWT(JSON Web Token)Session。此外,如何在前端存储这些认证信息(如 Cookie、localStorage 或 sessionStorage)也是一个值得探讨的话题。本文将深入探讨 JWT Token 与 Session 的区别,并详细分析它们在不同存储方式下的优缺点,同时提供必要的前端代码示例。

什么是 JWT(JSON Web Token)

JWT(JSON Web Token) 是一种开放标准(RFC 7519),用于在网络应用环境间以紧凑且自包含的方式安全地传输信息。JWT 通常用于身份验证、信息交换等场景。一个标准的 JWT 由三部分组成:

  1. Header(头部) :通常由令牌的类型(JWT)和所使用的签名算法(如 HMAC SHA256 或 RSA)组成。
  2. Payload(负载) :包含了声明(Claims),用于传递实体(通常是用户)及其他数据。
  3. Signature(签名) :用于验证令牌的真实性和完整性。

JWT 的特点:

  • 无状态:服务器无需存储会话信息,令牌本身包含所有必要的信息。
  • 可扩展性强:适合分布式系统和微服务架构。
  • 跨域支持:便于在不同域名间传递认证信息。

什么是 Session

Session(会话) 是一种服务器端的用户状态管理机制。它通过在服务器上存储用户的会话信息,并在客户端存储一个会话标识符(如 Session ID)来实现用户状态的维护。

Session 的特点:

  • 有状态:服务器需要存储每个用户的会话信息。
  • 安全性高:会话信息存储在服务器,客户端仅持有会话标识符。
  • 适用于小型应用:在用户量不大时,管理会话信息较为简单。

JWT Token 与 Session 的区别

特性JWT TokenSession
存储位置通常存储在客户端,如 localStorage、Cookie等存储在服务器端,客户端仅持有 Session ID
无状态/有状态无状态有状态
扩展性高,适合分布式系统和微服务架构受限,扩展时需要额外的会话存储方案
安全性需要妥善管理,易受 XSS 攻击影响相对安全,服务器控制会话信息
生命周期管理令牌自包含,可设定过期时间服务器可控制会话的生命周期
传输方式通常通过 Authorization Header 传输通常通过 Cookie 传输
信息携带令牌中可携带更多用户信息会话信息存储在服务器,客户端仅持有标识符
刷新机制需要实现刷新令牌机制服务器可主动刷新会话

存储 JWT 和 Session 的方式

在前端,我们主要有三种存储方式来存储 JWT 或 Session 信息:CookielocalStoragesessionStorage。每种存储方式都有其独特的特点和适用场景。

使用 Cookie 存储

Cookie 是一种存储在客户端并随每个请求自动发送到服务器的机制。它可以设置属性如 HttpOnlySecureSameSite 来增强安全性。

优点:

  • 自动随请求发送,无需前端手动管理。
  • 可以设置 HttpOnly,防止 JavaScript 访问,减少 XSS 攻击风险。
  • 支持跨域属性 SameSite,防止 CSRF 攻击。

缺点:

  • 每次请求都会携带 Cookie,增加请求体积。
  • 需要谨慎设置属性,确保安全性。

使用 localStorage 存储

localStorage 是一种持久化的 Web 存储机制,数据不会随浏览器关闭而消失,除非被显式清除。

优点:

  • 容量较大(约 5MB)。
  • 数据持久化存储,适合长期保存。
  • 易于使用,通过 JavaScript API 操作。

缺点:

  • 易受 XSS 攻击影响,恶意脚本可读取存储的数据。
  • 不会自动随请求发送,需要手动管理请求头。

使用 sessionStorage 存储

sessionStorage 类似于 localStorage,但数据仅在浏览器会话期间有效,页面关闭后数据会被清除。

优点:

  • 数据生命周期与浏览器会话绑定,更适合临时存储。
  • 容量较大(约 5MB)。
  • 易于使用,通过 JavaScript API 操作。

缺点:

  • 易受 XSS 攻击影响,恶意脚本可读取存储的数据。
  • 不会自动随请求发送,需要手动管理请求头。

各存储方式的优缺点

Cookie

  • 优点:

    • 自动随每个 HTTP 请求发送,简化认证流程。
    • 支持 HttpOnlySecure 属性,提高安全性。
    • 可以设置 SameSite 属性,防止 CSRF 攻击。
  • 缺点:

    • 容量有限(每个域名约 4KB)。
    • 每次请求都会携带 Cookie,增加带宽消耗。
    • 需要谨慎配置属性,防止安全漏洞。

localStorage

  • 优点:

    • 容量较大,适合存储较多数据。
    • 数据持久化,不会随浏览器关闭而消失。
    • 简单易用,适合单页应用(SPA)。
  • 缺点:

    • 易受 XSS 攻击,恶意脚本可读取存储的数据。
    • 需要手动管理请求头,增加开发复杂度。
    • 不支持跨域自动发送。

sessionStorage

  • 优点:

    • 数据生命周期与浏览器会话绑定,更适合临时数据。
    • 容量较大,适合存储较多数据。
    • 简单易用,适合单页应用(SPA)。
  • 缺点:

    • 易受 XSS 攻击,恶意脚本可读取存储的数据。
    • 数据在页面刷新后依然存在,需手动管理清除。
    • 不支持跨标签页共享数据。

前端代码示例

以下代码示例将展示如何在前端使用不同的存储方式来存储 JWT 令牌。

使用 Cookie 存储 JWT

在前端,可以使用 JavaScript 操作 Cookie 来存储 JWT。推荐使用 js-cookie 库来简化操作。

安装 js-cookie:

bash
Copy code
npm install js-cookie

示例代码:

javascript
Copy code
// 引入 js-cookie
import Cookies from 'js-cookie';

// 设置 JWT 到 Cookie
function setToken(token) {
  Cookies.set('token', token, { expires: 7, secure: true, sameSite: 'Strict' });
}

// 获取 JWT 从 Cookie
function getToken() {
  return Cookies.get('token');
}

// 删除 JWT 从 Cookie
function removeToken() {
  Cookies.remove('token');
}

// 使用 JWT 进行认证请求
async function fetchUserData() {
  const token = getToken();
  const response = await fetch('/api/user', {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  const data = await response.json();
  return data;
}

解释:

  • 使用 js-cookie 设置、获取和删除 Cookie。
  • 在请求头中添加 Authorization 字段,携带 JWT 令牌。

使用 localStorage 存储 JWT

示例代码:


// 设置 JWT 到 localStorage
function setToken(token) {
  localStorage.setItem('token', token);
}

// 获取 JWT 从 localStorage
function getToken() {
  return localStorage.getItem('token');
}

// 删除 JWT 从 localStorage
function removeToken() {
  localStorage.removeItem('token');
}

// 使用 JWT 进行认证请求
async function fetchUserData() {
  const token = getToken();
  const response = await fetch('/api/user', {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  const data = await response.json();
  return data;
}

解释:

  • 使用 localStoragesetItemgetItemremoveItem 方法管理 JWT 令牌。
  • 在请求头中添加 Authorization 字段,携带 JWT 令牌。

使用 sessionStorage 存储 JWT

示例代码:


// 设置 JWT 到 sessionStorage
function setToken(token) {
  sessionStorage.setItem('token', token);
}

// 获取 JWT 从 sessionStorage
function getToken() {
  return sessionStorage.getItem('token');
}

// 删除 JWT 从 sessionStorage
function removeToken() {
  sessionStorage.removeItem('token');
}

// 使用 JWT 进行认证请求
async function fetchUserData() {
  const token = getToken();
  const response = await fetch('/api/user', {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  const data = await response.json();
  return data;
}

解释:

  • 使用 sessionStoragesetItemgetItemremoveItem 方法管理 JWT 令牌。
  • 在请求头中添加 Authorization 字段,携带 JWT 令牌。

移动端应用中的 Token 存储

在现代应用开发中,安全地存储和管理用户认证信息(如 JWT Token)是确保应用安全性和用户数据保护的关键。对于 Web 应用,通常使用 Cookie 来存储 Token。然而,对于移动端应用(如 iOS 和 Android 应用),存储 Token 的方法有所不同,因为移动设备的存储机制和安全性要求与浏览器环境不同。本文将详细探讨移动端应用中存储 Token 的最佳实践和推荐方法。

iOS 平台:使用 Keychain

Keychain 是 Apple 提供的安全存储机制,用于存储敏感数据,如密码、Token 和证书。Keychain 提供高度的安全性,数据被加密存储,并且只有应用本身或特定的应用组可以访问。

Keychain 的优势:

  • 高安全性:数据经过加密,并存储在受保护的区域。
  • 持久性:数据在设备重启后仍然存在,直到被显式删除。
  • 系统集成:与 iOS 系统深度集成,支持生物识别验证(如 Touch ID 和 Face ID)。

使用 Keychain 存储 Token 的示例代码:

swift
Copy code
import Security

class KeychainService {
    static func saveToken(token: String, for account: String) -> Bool {
        guard let data = token.data(using: .utf8) else { return false }
        
        let query: [String: Any] = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : account,
            kSecValueData as String   : data,
            kSecAttrAccessible as String : kSecAttrAccessibleWhenUnlocked
        ]
        
        SecItemDelete(query as CFDictionary) // 删除已有项
        let status = SecItemAdd(query as CFDictionary, nil)
        return status == errSecSuccess
    }
    
    static func getToken(for account: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : account,
            kSecReturnData as String  : kCFBooleanTrue!,
            kSecMatchLimit as String  : kSecMatchLimitOne
        ]
        
        var dataTypeRef: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
        
        if status == errSecSuccess, let data = dataTypeRef as? Data, let token = String(data: data, encoding: .utf8) {
            return token
        }
        return nil
    }
    
    static func deleteToken(for account: String) -> Bool {
        let query: [String: Any] = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : account
        ]
        
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess
    }
}

使用示例:

swift
Copy code
// 保存 Token
let success = KeychainService.saveToken(token: "your_jwt_token", for: "userToken")
print("Token 保存成功: (success)")

// 获取 Token
if let token = KeychainService.getToken(for: "userToken") {
    print("获取到的 Token: (token)")
}

// 删除 Token
let deleted = KeychainService.deleteToken(for: "userToken")
print("Token 删除成功: (deleted)")

Android 平台:使用 EncryptedSharedPreferences 或 Keystore

在 Android 平台上,推荐使用 EncryptedSharedPreferencesAndroid Keystore 来存储敏感数据。

EncryptedSharedPreferences

EncryptedSharedPreferences 是 Android 提供的加密 SharedPreferences 实现,能够自动加密存储在 SharedPreferences 中的数据。

优势:

  • 简便易用:与普通 SharedPreferences API 兼容,易于集成。
  • 高安全性:数据在存储前自动加密,解密过程由系统管理。

示例代码:

java
Copy code
import android.content.Context;
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKeys;

public class SecureStorage {
    private static final String FILE_NAME = "secure_prefs";
    private static final String TOKEN_KEY = "user_token";
    private SharedPreferences sharedPreferences;

    public SecureStorage(Context context) throws Exception {
        String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
        sharedPreferences = EncryptedSharedPreferences.create(
                FILE_NAME,
                masterKeyAlias,
                context,
                EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        );
    }

    public void saveToken(String token) {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString(TOKEN_KEY, token);
        editor.apply();
    }

    public String getToken() {
        return sharedPreferences.getString(TOKEN_KEY, null);
    }

    public void deleteToken() {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.remove(TOKEN_KEY);
        editor.apply();
    }
}
Android Keystore

Android Keystore 提供了更高级别的安全性,通过硬件加密模块(如 TPM 或 Secure Element)来存储加密密钥,确保密钥不易被提取。

优势:

  • 极高的安全性:密钥在硬件中生成和存储,难以被攻击者获取。
  • 灵活性:可用于加密、签名等多种安全操作。

示例代码:

使用 Keystore 通常需要结合加密算法自行实现数据加密与解密,示例如下:

java
Copy code
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

public class KeystoreHelper {
    private static final String KEY_ALIAS = "my_key";
    private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
    private static final String TRANSFORMATION = "AES/GCM/NoPadding";
    
    public void generateKey() throws Exception {
        KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
                KEY_ALIAS,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
        )
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .build();
        
        KeyGenerator keyGenerator = KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE
        );
        keyGenerator.init(keyGenParameterSpec);
        keyGenerator.generateKey();
    }
    
    public byte[] encrypt(String data) throws Exception {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, getSecretKey());
        byte[] encryptionIv = cipher.getIV();
        byte[] encryptedData = cipher.doFinal(data.getBytes("UTF-8"));
        
        // 这里可以将 IV 和加密数据一起存储
        ByteBuffer byteBuffer = ByteBuffer.allocate(4 + encryptionIv.length + encryptedData.length);
        byteBuffer.putInt(encryptionIv.length);
        byteBuffer.put(encryptionIv);
        byteBuffer.put(encryptedData);
        return byteBuffer.array();
    }
    
    public String decrypt(byte[] encryptedData) throws Exception {
        ByteBuffer byteBuffer = ByteBuffer.wrap(encryptedData);
        int ivLength = byteBuffer.getInt();
        byte[] iv = new byte[ivLength];
        byteBuffer.get(iv);
        byte[] cipherText = new byte[byteBuffer.remaining()];
        byteBuffer.get(cipherText);
        
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        GCMParameterSpec spec = new GCMParameterSpec(128, iv);
        cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec);
        byte[] decryptedData = cipher.doFinal(cipherText);
        return new String(decryptedData, "UTF-8");
    }
    
    private SecretKey getSecretKey() throws Exception {
        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
        keyStore.load(null);
        return ((SecretKey) keyStore.getKey(KEY_ALIAS, null));
    }
}

使用示例:

java
Copy code
KeystoreHelper keystoreHelper = new KeystoreHelper();
keystoreHelper.generateKey();

String token = "your_jwt_token";
byte[] encryptedToken = keystoreHelper.encrypt(token);

// 存储 encryptedToken 到 SharedPreferences 或文件系统

// 解密 Token
String decryptedToken = keystoreHelper.decrypt(encryptedToken);

跨平台解决方案:React Native 和 Flutter 的安全存储

对于跨平台移动应用(如使用 React Native 或 Flutter 开发的应用),可以使用专门的安全存储库来管理 Token。

React Native

React Native 提供了多种库来实现安全存储,例如 react-native-keychainreact-native-secure-storage

使用 react-native-keychain 存储 Token 的示例:

javascript
Copy code
import * as Keychain from 'react-native-keychain';

// 保存 Token
async function saveToken(token) {
  await Keychain.setGenericPassword('userToken', token);
}

// 获取 Token
async function getToken() {
  const credentials = await Keychain.getGenericPassword();
  if (credentials) {
    return credentials.password;
  }
  return null;
}

// 删除 Token
async function deleteToken() {
  await Keychain.resetGenericPassword();
}

// 使用示例
saveToken('your_jwt_token');
getToken();
deleteToken();
Flutter

Flutter 提供了 flutter_secure_storage 插件,用于在移动设备上安全地存储数据。

使用 flutter_secure_storage 存储 Token 的示例:

dart
Copy code
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

// 创建 storage 实例
final storage = FlutterSecureStorage();

// 保存 Token
Future<void> saveToken(String token) async {
  await storage.write(key: 'userToken', value: token);
}

// 获取 Token
Future<String?> getToken() async {
  return await storage.read(key: 'userToken');
}

// 删除 Token
Future<void> deleteToken() async {
  await storage.delete(key: 'userToken');
}

// 使用示例
saveToken('your_jwt_token');
getToken();
deleteToken();

安全性考虑

安全性考虑

在选择 JWT 或 Session 以及存储方式时,安全性是一个不可忽视的重要因素。以下是一些关键的安全性考虑:

XSS 攻击

风险: 恶意脚本可以访问存储在 localStoragesessionStorage 中的 JWT,导致令牌泄露。

对策:

  • 内容安全策略(CSP): 限制网页可以加载的资源,防止恶意脚本执行。
  • 输入验证和输出编码: 防止跨站脚本注入。
  • 避免在 JWT 中存储敏感信息: 减少令牌泄露的影响。

CSRF 攻击

风险: 攻击者诱导用户在已认证的情况下执行恶意请求。

对策:

  • 使用 SameSite Cookie 属性: 限制跨站请求携带 Cookie。
  • 使用 CSRF 令牌: 在请求中包含随机生成的令牌,服务器验证其有效性。

Token 窃取与重放

风险: 令牌被窃取后,攻击者可以冒充用户进行操作。

对策:

  • 使用 HTTPS: 加密传输,防止中间人攻击。
  • 设置合理的过期时间: 限制令牌的有效期,减少被重放的时间窗口。
  • 使用刷新令牌机制: 定期更新令牌,提升安全性。

会话固定攻击

风险: 攻击者设置用户的会话标识符,导致用户使用攻击者指定的会话。

对策:

  • 在认证后重置会话标识符: 确保会话标识符的唯一性。
  • 限制会话生命周期: 设置合理的会话过期时间。

结论

JWT TokenSession 各有优缺点,选择适合的身份验证机制需要根据具体的应用场景和需求来决定。

  • JWT Token 适合分布式系统、微服务架构和需要跨域认证的应用,具有无状态、可扩展性强等优点。但需要妥善管理存储方式和安全性,防止 XSS 和 CSRF 攻击。
  • Session 适合传统的单体应用和用户量较小的系统,安全性较高,服务器可以全面控制会话生命周期。但在分布式系统中扩展性较差,需要额外的会话存储方案。

在存储方面:

  • Cookie 是一种安全性较高的存储方式,尤其是结合 HttpOnlySecure 属性,适合存储敏感的认证信息。
  • localStoragesessionStorage 适合存储非敏感的持久化数据,但需要加强 XSS 防护,避免令牌泄露。
  • 而在移动端应用中,利用平台提供的安全存储机制(如 iOS 的 Keychain 和 Android 的 EncryptedSharedPreferences 或 Keystore)能够有效地保护敏感数据。

最终,开发者应根据应用需求、架构设计和安全要求,综合考虑选择合适的身份验证机制和存储方式,确保应用的安全性和性能。

常见问题解答(FAQ)

问:JWT 和 Session 哪个更安全?

答:两者各有优缺点,安全性取决于具体的实现和存储方式。Session 通常被认为在防范 CSRF 攻击方面更有优势,而 JWT 的无状态特性在分布式系统中更具优势。关键在于如何正确配置和使用它们,以最大限度地减少安全风险。

问:我应该在什么情况下使用 JWT,什么情况下使用 Session?

答:如果你的应用需要跨域认证、分布式架构或微服务支持,JWT 是更好的选择。对于传统的单体应用或用户量较小的系统,Session 可能更适合,因为它的实现相对简单且安全性较高。

问:如何在 Web 和移动端应用中统一管理认证 Token?

答:可以采用后端统一管理认证 Token,并在不同平台使用各自的安全存储机制。例如,Web 应用使用 Cookie 或 localStorage,而移动端应用使用 Keychain 或 EncryptedSharedPreferences。确保所有平台的 Token 处理遵循相同的安全标准。

问:如何防止 JWT 被盗用?

答:确保通过 HTTPS 传输 JWT,设置合理的过期时间,使用刷新令牌机制,限制令牌的权限范围,并在服务器端验证 Token 的有效性。此外,避免在 Token 中存储敏感信息,以减少泄露后的风险。

问:在移动端应用中,如何处理 Token 的过期?

答:可以在 Token 过期前使用刷新令牌机制获取新的 Token,或者在检测到 Token 过期后,提示用户重新登录。确保刷新 Token 的过程也遵循安全最佳实践,防止刷新令牌被滥用。