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

无用户登录场景下,安卓应用HttpURLConnection请求REST服务的安全方案咨询

嘿,这个问题提得太关键了——硬编码校验字符串上线商用绝对是埋了个定时炸弹!无用户登录的场景下,我们得从防篡改、防伪造、防中间人攻击这几个核心维度入手,给你一套实用的组合方案,附带可直接参考的代码示例:

无登录场景下Android REST请求的安全实现方案

一、先筑牢基础:SSL证书绑定(SSL Pinning)

这是必须做的第一步,用来防止中间人抓包篡改请求。默认情况下,HttpURLConnection会信任系统预装的所有根证书,攻击者可以伪造证书拦截你的请求。通过SSL Pinning,我们让App只信任自己服务端的证书,拒绝其他任何非法证书。

实现步骤:

  1. 从服务端获取证书的SHA256哈希值(可以用openssl命令生成:openssl s_client -connect your-domain.com:443 | openssl x509 -noout -fingerprint -sha256,记得去掉结果里的冒号)
  2. 在HttpURLConnection中自定义TrustManager,校验服务器证书的哈希值

代码示例:

import javax.net.ssl.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

public class SSLPinningHelper {
    // 替换成你的服务端证书SHA256哈希值(无冒号)
    private static final String PINNED_CERT_SHA256 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdef";

    public static SSLSocketFactory getPinnedSSLSocketFactory() throws Exception {
        TrustManager[] trustManagers = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}

                    @Override
                    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                        boolean isPinned = false;
                        for (X509Certificate cert : chain) {
                            String certSha256 = getCertificateSha256(cert);
                            if (PINNED_CERT_SHA256.equals(certSha256)) {
                                isPinned = true;
                                break;
                            }
                        }
                        if (!isPinned) {
                            throw new CertificateException("非法服务器证书,拒绝连接!");
                        }
                    }

                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }
                }
        };

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagers, null);
        return sslContext.getSocketFactory();
    }

    private static String getCertificateSha256(X509Certificate cert) throws Exception {
        java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
        byte[] digest = md.digest(cert.getEncoded());
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

在HttpURLConnection中启用Pin:

URL url = new URL("https://your-rest-service.com/api/endpoint");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

// 绑定SSL证书
conn.setSSLSocketFactory(SSLPinningHelper.getPinnedSSLSocketFactory());
// 保留默认主机名校验(如果证书域名和请求域名一致,不要禁用)

// 其他请求配置
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);

二、核心防护:API密钥+请求签名机制

硬编码校验字符串的致命问题是反编译App就能轻松拿到。我们需要把密钥安全存储,并且每次请求生成唯一签名,服务端通过签名验证请求合法性,同时防止重放攻击。

关键要点:

  1. 安全存储API密钥:用Android Keystore加密存储(系统级加密,比硬编码安全N倍),避免直接写在代码里。
  2. 签名生成规则:用时间戳、随机nonce、请求体摘要+API密钥,通过HMAC-SHA256生成签名,服务端用相同规则验证。
  3. 防重放攻击:加入时间戳(校验是否在5分钟有效窗口内)和随机nonce(服务端记录已使用的nonce,防止重复请求)。

代码示例:

1. 用Android Keystore存储API密钥

import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import java.security.KeyStore;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

public class KeystoreHelper {
    private static final String KEYSTORE_NAME = "AndroidKeyStore";
    private static final String API_KEY_ALIAS = "MyAppApiSecret";

    // 首次启动App时调用,生成并存储加密密钥
    public static void initApiKey() throws Exception {
        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_NAME);
        keyStore.load(null);

        if (!keyStore.containsAlias(API_KEY_ALIAS)) {
            KeyGenerator keyGenerator = KeyGenerator.getInstance(
                    KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_NAME);
            KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
                    API_KEY_ALIAS,
                    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .build();
            keyGenerator.init(spec);
            keyGenerator.generateKey();
        }
    }

    // 获取存储的加密密钥
    public static SecretKey getApiKey() throws Exception {
        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_NAME);
        keyStore.load(null);
        return (SecretKey) keyStore.getKey(API_KEY_ALIAS, null);
    }
}

2. 生成请求签名

import javax.crypto.Mac;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class RequestSignHelper {
    // 生成随机nonce(防止重放)
    public static String generateNonce() {
        return String.valueOf(new Random().nextLong());
    }

    // 生成秒级时间戳
    public static long generateTimestamp() {
        return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
    }

    // 生成HMAC-SHA256签名
    public static String generateSignature(SecretKey secretKey, String timestamp, String nonce, String requestBody) throws NoSuchAlgorithmException, InvalidKeyException {
        // 拼接签名源字符串:必须和服务端规则完全一致
        String signSource = timestamp + nonce + requestBody;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(secretKey);
        byte[] signBytes = mac.doFinal(signSource.getBytes(StandardCharsets.UTF_8));
        
        // 转十六进制字符串
        StringBuilder sb = new StringBuilder();
        for (byte b : signBytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

3. 发送带签名的请求

// 准备请求体
String requestBody = "{\"param1\":\"value1\",\"param2\":\"value2\"}";

// 生成签名参数
String nonce = RequestSignHelper.generateNonce();
long timestamp = RequestSignHelper.generateTimestamp();
SecretKey apiKey = KeystoreHelper.getApiKey();
String signature = RequestSignHelper.generateSignature(apiKey, String.valueOf(timestamp), nonce, requestBody);

URL url = new URL("https://your-rest-service.com/api/endpoint");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

// 启用SSL Pinning
conn.setSSLSocketFactory(SSLPinningHelper.getPinnedSSLSocketFactory());

// 设置请求头传递签名信息
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
conn.setRequestProperty("X-Nonce", nonce);
conn.setRequestProperty("X-Signature", signature);
conn.setDoOutput(true);

// 写入请求体
try (OutputStream os = conn.getOutputStream()) {
    byte[] input = requestBody.getBytes(StandardCharsets.UTF_8);
    os.write(input, 0, input.length);
}

// 处理响应
int responseCode = conn.getResponseCode();
// ... 后续响应处理逻辑

三、可选增强:请求体加密

如果请求体包含敏感数据(比如用户隐私信息、业务核心数据),可以用AES-GCM加密请求体,服务端收到后解密。AES-GCM同时提供加密和完整性校验,安全性更高。

代码示例(AES-GCM加密)

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

public class EncryptionHelper {
    private static final int GCM_IV_LENGTH = 12; // GCM推荐IV长度
    private static final int GCM_TAG_LENGTH = 16; // 校验标签长度

    // 加密请求体
    public static String encryptRequestBody(String plainText, SecretKey secretKey) throws Exception {
        SecureRandom secureRandom = new SecureRandom();
        byte[] iv = new byte[GCM_IV_LENGTH];
        secureRandom.nextBytes(iv);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);

        byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
        // 拼接IV和加密数据,方便服务端解密
        byte[] result = new byte[iv.length + encryptedBytes.length];
        System.arraycopy(iv, 0, result, 0, iv.length);
        System.arraycopy(encryptedBytes, 0, result, iv.length, encryptedBytes.length);
        return Base64.getEncoder().encodeToString(result);
    }
}

使用时只需替换请求体:

String encryptedBody = EncryptionHelper.encryptRequestBody(requestBody, apiKey);
// 将encryptedBody作为请求体发送

四、服务端验证逻辑(简要说明)

服务端必须同步实现以下校验:

  1. 检查时间戳是否在有效窗口内(比如当前时间±5分钟),拒绝超时请求。
  2. 校验nonce是否已被使用(用Redis存储已使用的nonce,过期时间和时间戳窗口一致)。
  3. 用相同的API密钥、timestamp、nonce、请求体(加密则先解密)生成签名,和请求头的X-Signature对比,一致则通过校验。

最后几个重要提醒

  • 密钥绝对不能硬编码:哪怕是混淆后的代码,反编译后也能找到硬编码字符串,必须用Keystore或NDK方式存储。
  • 签名规则要和服务端严格对齐:参数顺序、哈希算法、编码方式差一点都会导致验证失败。
  • 定期轮换密钥:如果怀疑密钥泄露,及时更换API密钥,避免损失扩大。
  • 添加请求频率限制:防止API被恶意调用滥用。

内容的提问来源于stack exchange,提问作者TheAlmac2

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 03:39:35