面试题答案
一键面试优化思路
- 签名算法选择:优先选用安全性高且抗逆向分析能力强的算法。例如,从 HMAC 系列算法转向 RSASSA-PSS 或 ECDSA 算法。HMAC 基于对称密钥,密钥泄露风险较大;而 RSASSA-PSS 和 ECDSA 基于非对称密钥,公钥用于验证签名,私钥仅在服务器端妥善保管,降低密钥泄露带来的风险。
- 密钥管理:
- 定期更新密钥:设定合理的密钥更新周期,例如每 1 - 3 个月更新一次。新密钥生成后,逐步过渡使用,避免对现有业务造成冲击。
- 密钥存储安全:采用硬件安全模块(HSM)来存储密钥。HSM 提供了物理和逻辑上的安全防护,防止密钥被窃取或篡改。
- 防范重放攻击:
- 添加唯一标识符:在 JWT 的 payload 中加入一个唯一标识符,如 UUID。每次生成 JWT 时,该标识符都不同。服务器端验证时,记录已使用过的标识符,拒绝重复的请求。
- 设置有效期:缩短 JWT 的有效期,例如设置为 5 - 15 分钟的短期有效。对于需要长期访问的场景,可结合 refresh token 机制,refresh token 同样需设置合理有效期且妥善存储。
- 增强算法抗逆向分析能力:
- 混淆和加密处理:对 JWT 生成过程中的敏感数据进行混淆处理,例如对 payload 中的关键信息进行加密后再放入 JWT。可采用对称加密算法如 AES 对特定字段加密,使用时在服务器端解密验证。
- 代码保护:对涉及 JWT 签名和验证的代码进行保护,如使用代码混淆工具对代码进行混淆,增加逆向分析的难度。
技术细节
- 签名算法切换:
- RSASSA-PSS:在 Java 中,可使用 Bouncy Castle 库来实现。首先生成 RSA 密钥对,如
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); keyPairGenerator.initialize(2048); KeyPair keyPair = keyPairGenerator.generateKeyPair();
。在签名时,RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); PSSParameterSpec spec = new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1); RSASigner signer = new RSASigner(); signer.init(true, privateKey); signer.update(data, 0, data.length); byte[] signature = signer.generateSignature();
。验证签名时,RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); signer.init(false, publicKey); signer.update(data, 0, data.length); boolean verified = signer.verifySignature(signature);
- ECDSA:同样在 Java 中借助 Bouncy Castle 库。生成椭圆曲线密钥对
ECKeyPairGenerator keyPairGenerator = (ECKeyPairGenerator) KeyPairGenerator.getInstance("ECDSA", "BC"); ECKeyGenerationParameters keyGenParams = new ECKeyGenerationParameters(NISTNamedCurves.getByName("secp256k1"), new SecureRandom()); keyPairGenerator.initialize(keyGenParams); KeyPair keyPair = keyPairGenerator.generateKeyPair();
。签名过程ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate(); ECDSASigner signer = new ECDSASigner(); signer.init(true, privateKey); BigInteger[] sig = signer.generateSignature(data);
。验证签名ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); signer.init(false, publicKey); boolean verified = signer.verifySignature(data, sig[0], sig[1]);
- RSASSA-PSS:在 Java 中,可使用 Bouncy Castle 库来实现。首先生成 RSA 密钥对,如
- 密钥管理:
- 密钥更新:以 Java 为例,在应用启动时,检查密钥的创建时间或版本号。如
long keyCreationTime = keyProperties.getProperty("key.creation.time", Long.parseLong("0")); long currentTime = System.currentTimeMillis(); if (currentTime - keyCreationTime > keyUpdateInterval) { // 生成新密钥并更新配置 KeyPair newKeyPair = generateNewKeyPair(); keyProperties.setProperty("private.key", new String(Base64.getEncoder().encode(newKeyPair.getPrivate().getEncoded()))); keyProperties.setProperty("public.key", new String(Base64.getEncoder().encode(newKeyPair.getPublic().getEncoded()))); keyProperties.setProperty("key.creation.time", String.valueOf(currentTime)); saveKeyProperties(keyProperties); }
- HSM 集成:不同的 HSM 有不同的集成方式。以 Thales nShield HSM 为例,通过其提供的 API 进行密钥生成、存储和签名操作。如
CK_FUNCTION_LIST_PTR pFunctionList = C_Initialize(null); CK_SESSION_HANDLE hSession; C_OpenSession(slotID, CKF_SERIAL_SESSION | CKF_RW_SESSION, null, null, &hSession); CK_OBJECT_HANDLE hKey; generateKeyInHSM(pFunctionList, hSession, &hKey); byte[] signature = signWithKeyInHSM(pFunctionList, hSession, hKey, data);
- 密钥更新:以 Java 为例,在应用启动时,检查密钥的创建时间或版本号。如
- 防范重放攻击:
- 唯一标识符:在生成 JWT 时,在 payload 中添加 UUID,如
UUID uuid = UUID.randomUUID(); claims.put("jti", uuid.toString()); String jwt = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS256, secretKey).compact();
。在验证时,Claims claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(jwt).getBody(); String jti = claims.get("jti", String.class); if (replayCache.contains(jti)) { throw new ReplayAttackException(); } replayCache.add(jti);
- 有效期设置:在生成 JWT 时设置有效期,如
long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); long expirationMillis = nowMillis + TimeUnit.MINUTES.toMillis(10); Date expiration = new Date(expirationMillis); claims.put("exp", expiration); String jwt = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS256, secretKey).compact();
。验证时,Claims claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(jwt).getBody(); Date expiration = claims.get("exp", Date.class); if (expiration.before(new Date())) { throw new ExpiredJwtException(null, null, "JWT has expired"); }
- 唯一标识符:在生成 JWT 时,在 payload 中添加 UUID,如
- 增强算法抗逆向分析能力:
- 混淆和加密处理:以对 payload 中特定字段加密为例,在 Java 中使用 AES 加密。如
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); IvParameterSpec iv = new IvParameterSpec(ivBytes); cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); byte[] encryptedData = cipher.doFinal(dataBytes); String encryptedString = Base64.getEncoder().encodeToString(encryptedData); claims.put("encrypted_field", encryptedString);
。解密验证时,SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); IvParameterSpec iv = new IvParameterSpec(ivBytes); cipher.init(Cipher.DECRYPT_MODE, secretKey, iv); byte[] decryptedData = cipher.doFinal(Base64.getDecoder().decode(encryptedString));
- 代码保护:使用 ProGuard 工具对 Java 代码进行混淆。在项目的
build.gradle
文件中配置,如buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }
。在proguard-rules.pro
文件中可定义混淆规则,如-keep class com.example.package.** { *; }
来指定需要保留的类和成员,避免关键功能被过度混淆而无法正常运行。
- 混淆和加密处理:以对 payload 中特定字段加密为例,在 Java 中使用 AES 加密。如