HarmonyOS开发中HTTPS与证书校验:网络安全实践

在数据裸奔的时代,HTTPS是你的应用穿上的第一件防护服

一、背景与动机:为什么HTTPS如此重要?

还记得几年前,运营商在你的网页里强行插入广告弹窗吗?还记得公共WiFi下,你的聊天记录被截获的新闻吗?这些都是HTTP明文传输带来的安全隐患。

HTTPS(HTTP Secure)通过TLS/SSL协议,在客户端和服务器之间建立加密通道。即使数据被截获,攻击者也只能看到一堆乱码。这就像写信时用只有你和收信人知道的密码加密,中间人截获了也看不懂。

鸿蒙应用默认要求使用HTTPS,这是系统级的安全策略。但在实际开发中,HTTPS配置不当反而会带来问题:

证书校验过严:自签名证书、企业内网证书无法通过校验,导致请求失败。

证书校验过松:忽略所有证书错误,HTTPS形同虚设,中间人攻击畅通无阻。

证书过期未处理:服务器证书更新后,客户端没有相应处理,导致服务中断。

本文将深入探讨HTTPS的工作原理,以及在鸿蒙中的正确配置方式。

二、核心原理:HTTPS握手与证书校验

2.1 HTTPS工作流程

CA机构 服务器 客户端 CA机构 服务器 客户端 1. TCP三次握手建立连接 2. TLS握手:服务端下发证书 opt [ECDHE等非RSA密钥协商] 3. 客户端校验证书合法性 默认本地根证书链验签;吊销检查才访问CA alt [开启OCSP/CRL证书吊销校验] 4. 交换预主密钥,生成会话密钥 5. TLS加密传输HTTP业务数据 SYN 连接请求 SYN+ACK 同步+确认 ACK 确认 ClientHello(版本、随机数、加密套件列表) ServerHello(选定版本、套件、服务端随机数) 证书链(站点证书+中间CA证书) ServerKeyExchange(密钥交换参数) ServerHelloDone OCSP查询 / 下载CRL列表 返回证书吊销状态 校验签名、域名匹配、有效期、吊销状态 ClientKeyExchange(加密预主密钥/客户端公钥) ChangeCipherSpec(后续报文启用加密) Finished(握手完整性校验摘要) ChangeCipherSpec(后续报文启用加密) Finished(握手完整性校验摘要) 密文HTTP请求 密文HTTP响应

2.2 证书校验链

HTTPS的安全性核心在于证书校验。证书就像服务器的"身份证",由权威机构(CA)签发。

校验项 说明 失败后果
证书签名 验证CA签名是否有效 可能是伪造证书
证书链 验证证书到根证书的完整链 无法建立信任
域名匹配 证书域名与请求域名一致 可能访问了错误服务器
有效期 当前时间在证书有效期内 证书已过期或未生效
吊销状态 证书未被CA吊销 证书已被废除

2.3 鸿蒙HTTPS配置

鸿蒙系统内置了主流CA根证书,对于正规HTTPS网站,默认配置即可正常工作。

import http from '@ohos.net.http';

// 默认HTTPS请求(自动校验证书)
async function secureRequest(): Promise<void> {
  let httpRequest = http.createHttp();
  
  try {
    // 正规HTTPS网站,无需特殊配置
    let response = await httpRequest.request(
      'https://api.example.com/data',  // 注意是https://
      {
        method: http.RequestMethod.GET
      }
    );
  
    console.info('HTTPS请求成功');
  
  } finally {
    httpRequest.destroy();
  }
}

三、代码实战:三种典型场景

场景一:证书固定(Certificate Pinning)

为了防止CA被攻破或中间人攻击,将服务器证书或公钥硬编码到客户端,只信任特定证书。

import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';

// 证书固定配置
interface PinConfig {
  hostname: string;           // 主机名
  publicKeyHashes: string[];  // 公钥SHA-256哈希值(Base64)
}

// 预定义的证书公钥哈希
const CERT_PINS: PinConfig[] = [
  {
    hostname: 'api.example.com',
    publicKeyHashes: [
      // 主证书公钥哈希
      'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
      // 备用证书公钥哈希(证书轮换时使用)
      'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB='
    ]
  },
  {
    hostname: 'cdn.example.com',
    publicKeyHashes: [
      'sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC='
    ]
  }
];

// 带证书固定的HTTP客户端
class PinnedHttpClient {
  private pins: Map<string, string[]> = new Map();
  
  constructor(pins: PinConfig[]) {
    for (let pin of pins) {
      this.pins.set(pin.hostname, pin.publicKeyHashes);
    }
  }
  
  // 发起请求(带证书固定校验)
  async request(url: string, options: http.HttpRequestOptions): Promise<http.HttpResponse> {
    let httpRequest = http.createHttp();
  
    try {
      // 解析URL获取主机名
      let hostname = this.extractHostname(url);
    
      // 检查是否需要证书固定
      if (this.pins.has(hostname)) {
        console.info(`启用证书固定: ${hostname}`);
      
        // HarmonyOS 6: 配置证书固定
        let pinHashes = this.pins.get(hostname);
      
        // 发起请求(系统会自动校验证书)
        let response = await httpRequest.request(url, {
          ...options,
          // HarmonyOS 6新增:证书固定配置
          // 注:实际API可能有所不同,此处为示意
        });
      
        return response;
      } else {
        // 无需证书固定,正常请求
        return await httpRequest.request(url, options);
      }
    
    } finally {
      httpRequest.destroy();
    }
  }
  
  // 从URL提取主机名
  private extractHostname(url: string): string {
    try {
      let urlObj = new URL(url);
      return urlObj.hostname;
    } catch {
      return '';
    }
  }
}

// 使用示例
const pinnedClient = new PinnedHttpClient(CERT_PINS);

async function fetchSecureData(): Promise<void> {
  try {
    let response = await pinnedClient.request(
      'https://api.example.com/sensitive-data',
      { method: http.RequestMethod.GET }
    );
  
    console.info('安全数据获取成功');
  } catch (error) {
    let e = error as BusinessError;
    if (e.message?.includes('certificate')) {
      console.error('证书校验失败:可能遭受中间人攻击!');
    }
  }
}

场景二:自定义证书校验

对于特殊场景,需要自定义证书校验逻辑。

import http from '@ohos.net.http';
import cert from '@ohos.security.cert';

// 自定义证书校验器
class CustomCertificateVerifier {
  // 受信任的证书指纹列表
  private trustedFingerprints: Set<string> = new Set([
    'AA:BB:CC:DD:EE:FF:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE',
    'FF:EE:DD:CC:BB:AA:99:88:77:66:55:44:33:22:11:00:FF:EE:DD:CC'
  ]);
  
  // 校验证书指纹
  async verifyCertificate(certData: Uint8Array): Promise<boolean> {
    try {
      // 计算证书SHA-1指纹
      let fingerprint = await this.calculateSHA1(certData);
    
      // 检查是否在信任列表中
      if (this.trustedFingerprints.has(fingerprint)) {
        console.info('证书校验通过');
        return true;
      }
    
      console.error('证书不在信任列表中');
      return false;
    
    } catch (error) {
      console.error('证书校验异常:', error);
      return false;
    }
  }
  
  // 计算SHA-1指纹
  private async calculateSHA1(data: Uint8Array): Promise<string> {
    // 实际实现需要使用鸿蒙的加密API
    // 此处为示意代码
    return 'AA:BB:CC:DD:EE:FF:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE';
  }
}

// 带自定义校验的HTTPS请求
async function requestWithCustomVerification(
  url: string,
  verifier: CustomCertificateVerifier
): Promise<http.HttpResponse | null> {
  
  let httpRequest = http.createHttp();
  
  try {
    // HarmonyOS 6: 配置自定义证书校验
    // 注:实际API可能有所不同
    let response = await httpRequest.request(url, {
      method: http.RequestMethod.GET,
      // 自定义校验回调(示意)
      // onCertificateVerify: (cert) => verifier.verifyCertificate(cert)
    });
  
    return response;
  
  } catch (error) {
    console.error('请求失败:', error);
    return null;
  
  } finally {
    httpRequest.destroy();
  }
}

场景三:证书过期监控与处理

监控服务器证书状态,提前发现即将过期的证书。

// 证书信息
interface CertificateInfo {
  subject: string;        // 证书主体
  issuer: string;         // 颁发者
  validFrom: Date;        // 生效时间
  validTo: Date;          // 过期时间
  fingerprint: string;    // 指纹
  isExpired: boolean;     // 是否过期
  daysUntilExpiry: number; // 距离过期天数
}

// 证书监控服务
class CertificateMonitor {
  private warningThreshold: number = 30;  // 提前30天预警
  
  // 检查证书状态
  async checkCertificate(url: string): Promise<CertificateInfo | null> {
    let httpRequest = http.createHttp();
  
    try {
      // 发起HEAD请求,只获取响应头
      let response = await httpRequest.request(url, {
        method: http.RequestMethod.HEAD
      });
    
      // 从响应中提取证书信息
      // 注:实际需要通过系统API获取证书详情
      let certInfo: CertificateInfo = {
        subject: 'CN=api.example.com',
        issuer: 'CN=Let\'s Encrypt Authority X3',
        validFrom: new Date('2024-01-01'),
        validTo: new Date('2024-12-31'),
        fingerprint: 'AA:BB:CC:DD:...',
        isExpired: false,
        daysUntilExpiry: this.calculateDaysUntil(new Date('2024-12-31'))
      };
    
      // 检查是否即将过期
      if (certInfo.daysUntilExpiry <= this.warningThreshold) {
        this.sendExpiryWarning(certInfo);
      }
    
      return certInfo;
    
    } catch (error) {
      console.error('证书检查失败:', error);
      return null;
    
    } finally {
      httpRequest.destroy();
    }
  }
  
  // 计算距离过期天数
  private calculateDaysUntil(expiryDate: Date): number {
    let now = new Date();
    let diff = expiryDate.getTime() - now.getTime();
    return Math.ceil(diff / (1000 * 60 * 60 * 24));
  }
  
  // 发送过期预警
  private sendExpiryWarning(certInfo: CertificateInfo): void {
    console.warn(`证书即将过期!剩余 ${certInfo.daysUntilExpiry}`);
    console.warn(`证书主体: ${certInfo.subject}`);
    console.warn(`过期时间: ${certInfo.validTo}`);
  
    // 实际项目中可以发送通知、邮件等
    // NotificationService.send({
    //   title: '证书过期预警',
    //   message: `${certInfo.subject} 证书将在 ${certInfo.daysUntilExpiry} 天后过期`
    // });
  }
  
  // 批量检查多个服务的证书
  async checkAllServices(services: string[]): Promise<Map<string, CertificateInfo>> {
    let results = new Map<string, CertificateInfo>();
  
    for (let service of services) {
      let certInfo = await this.checkCertificate(service);
      if (certInfo) {
        results.set(service, certInfo);
      }
    }
  
    return results;
  }
}

// 使用示例:定期检查证书
const monitor = new CertificateMonitor();

async function startCertificateMonitoring(): Promise<void> {
  // 要监控的服务列表
  let services = [
    'https://api.example.com',
    'https://cdn.example.com',
    'https://payment.example.com'
  ];
  
  // 执行检查
  let results = await monitor.checkAllServices(services);
  
  // 输出报告
  console.info('=== 证书状态报告 ===');
  results.forEach((certInfo, service) => {
    let status = certInfo.isExpired ? '❌ 已过期' :
                 certInfo.daysUntilExpiry <= 30 ? '⚠️ 即将过期' : '✅ 正常';
    console.info(`${service}: ${status} (剩余 ${certInfo.daysUntilExpiry} 天)`);
  });
}

// UI组件:证书状态展示
@Entry
@Component
struct CertificateStatusPage {
  @State certInfos: Map<string, CertificateInfo> = new Map();
  
  async aboutToAppear() {
    let monitor = new CertificateMonitor();
    this.certInfos = await monitor.checkAllServices([
      'https://api.example.com',
      'https://cdn.example.com'
    ]);
  }
  
  build() {
    Column() {
      Text('证书状态监控')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })
    
      ForEach(Array.from(this.certInfos.entries()), (entry: [string, CertificateInfo]) => {
        this.CertStatusCard(entry[0], entry[1])
      }, (entry: [string, CertificateInfo]) => entry[0])
    }
    .width('100%')
    .padding(20)
  }
  
  @Builder
  CertStatusCard(service: string, certInfo: CertificateInfo) {
    Column() {
      Row() {
        Text(service)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .layoutWeight(1)
      
        if (certInfo.isExpired) {
          Text('已过期')
            .fontSize(12)
            .fontColor('#D0021B')
            .backgroundColor('#FFE5E5')
            .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            .borderRadius(4)
        } else if (certInfo.daysUntilExpiry <= 30) {
          Text('即将过期')
            .fontSize(12)
            .fontColor('#F5A623')
            .backgroundColor('#FFF3E0')
            .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            .borderRadius(4)
        } else {
          Text('正常')
            .fontSize(12)
            .fontColor('#7ED321')
            .backgroundColor('#E8F5E9')
            .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            .borderRadius(4)
        }
      }
      .width('100%')
    
      Text(`有效期至: ${certInfo.validTo.toLocaleDateString()}`)
        .fontSize(14)
        .fontColor('#666')
        .margin({ top: 8 })
    
      Text(`剩余 ${certInfo.daysUntilExpiry}`)
        .fontSize(14)
        .fontColor('#666')
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#F5F5F5')
    .borderRadius(8)
    .margin({ bottom: 10 })
  }
}

四、踩坑与注意事项

坑点一:忽略所有证书错误

这是最危险的做法,让HTTPS形同虚设。

// ❌ 绝对不要这样做!
// 某些框架允许忽略证书错误,但鸿蒙不支持也不应该支持
async function dangerousBypass() {
  // 这种代码会让你的应用暴露在中间人攻击下
  // 攻击者可以用任意证书冒充服务器
}

正确做法:如果必须使用自签名证书,应该将证书导入系统信任库,或使用证书固定。

坑点二:HTTP与HTTPS混用

在HTTPS页面中加载HTTP资源(混合内容)会被浏览器阻止,鸿蒙也有类似限制。

// ❌ 错误:HTTPS页面中请求HTTP资源
async function mixedContent() {
  // 主页面是HTTPS
  // 但API请求是HTTP,会被阻止
  let response = await httpRequest.request(
    'http://api.example.com/data'  // 不安全!
  );
}

// ✅ 正确:统一使用HTTPS
async function secureContent() {
  let response = await httpRequest.request(
    'https://api.example.com/data'  // 安全
  );
}

坑点三:证书固定过于严格

证书固定后,如果服务器更换证书,客户端会立即无法连接。

// ❌ 错误:只固定一个证书
const SINGLE_PIN = {
  hostname: 'api.example.com',
  publicKeyHashes: [
    'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='  // 只有一个
  ]
};

// ✅ 正确:固定多个证书(包括备用)
const MULTI_PINS = {
  hostname: 'api.example.com',
  publicKeyHashes: [
    'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',  // 当前证书
    'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=',  // 备用证书
    'sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC='   // 新证书(提前部署)
  ]
};

坑点四:域名不匹配

证书是为特定域名签发的,访问其他域名(即使是同一服务器)会校验失败。

// 假设证书是为 api.example.com 签发的

// ❌ 错误:使用IP地址访问
await httpRequest.request('https://192.168.1.100/api');
// 证书域名不匹配,校验失败

// ❌ 错误:使用其他域名
await httpRequest.request('https://api.example.org/data');
// 域名不匹配

// ✅ 正确:使用证书中的域名
await httpRequest.request('https://api.example.com/data');

五、HarmonyOS 6适配指南

5.1 新增安全配置

HarmonyOS 6提供了更细粒度的HTTPS安全配置。

import http from '@ohos.net.http';

// HarmonyOS 6 HTTPS安全配置
let securityOptions: http.HttpSecurityOptions = {
  // 最低TLS版本
  minTlsVersion: http.TlsVersion.TLS_V1_2,
  
  // 最高TLS版本
  maxTlsVersion: http.TlsVersion.TLS_V1_3,
  
  // 允许的加密套件
  cipherSuites: [
    'TLS_AES_128_GCM_SHA256',
    'TLS_AES_256_GCM_SHA384',
    'TLS_CHACHA20_POLY1305_SHA256'
  ],
  
  // 是否校验证书域名
  verifyHostname: true,
  
  // 证书固定配置
  certificatePins: [
    {
      hostname: 'api.example.com',
      publicKeyHashes: [
        'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
      ]
    }
  ]
};

// 应用安全配置
let httpRequest = http.createHttp();

let response = await httpRequest.request(
  'https://api.example.com/data',
  {
    method: http.RequestMethod.GET,
    securityOptions: securityOptions  // HarmonyOS 6新增
  }
);

5.2 TLS 1.3支持

HarmonyOS 6默认支持TLS 1.3,提供更好的性能和安全性。

// 强制使用TLS 1.3
let tls13Options: http.HttpRequestOptions = {
  method: http.RequestMethod.GET,
  securityOptions: {
    minTlsVersion: http.TlsVersion.TLS_V1_3,
    maxTlsVersion: http.TlsVersion.TLS_V1_3
  }
};

// 检查服务器是否支持TLS 1.3
async function checkTLS13Support(hostname: string): Promise<boolean> {
  let httpRequest = http.createHttp();
  
  try {
    let response = await httpRequest.request(
      `https://${hostname}/`,
      {
        method: http.RequestMethod.HEAD,
        securityOptions: {
          minTlsVersion: http.TlsVersion.TLS_V1_3,
          maxTlsVersion: http.TlsVersion.TLS_V1_3
        }
      }
    );
  
    return response.responseCode === 200;
  
  } catch (error) {
    console.warn('服务器不支持TLS 1.3');
    return false;
  
  } finally {
    httpRequest.destroy();
  }
}

5.3 证书透明度(Certificate Transparency)

HarmonyOS 6支持证书透明度日志验证,进一步防止伪造证书。

// 启用证书透明度验证
let ctOptions: http.HttpRequestOptions = {
  method: http.RequestMethod.GET,
  securityOptions: {
    // 启用CT验证
    requireCertificateTransparency: true,
  
    // CT日志服务器
    ctLogServers: [
      'https://ct.googleapis.com/pilot/',
      'https://ct.googleapis.com/rocket/'
    ]
  }
};

六、总结一下下

HTTPS是移动应用安全的基础防线,正确配置至关重要。本文从三个实战场景展开:

证书固定:将信任锚点从CA转移到应用自身,即使CA被攻破也能保证安全。但要预留备用证书,避免证书轮换时服务中断。

自定义校验:针对特殊场景(如企业内网),可以自定义校验逻辑。但要谨慎使用,避免过度放松安全要求。

证书监控:主动监控服务器证书状态,提前发现即将过期的证书,给运维团队留出更换时间。

四个常见坑点:忽略证书错误(最危险)、HTTP/HTTPS混用、证书固定过严、域名不匹配。遇到证书相关问题时,先排查这四个方面。

HarmonyOS 6带来了TLS 1.3、证书透明度等新特性,安全性进一步提升。合理利用这些特性,可以让应用在网络攻击面前更加坚固。

下一篇文章,我们将专门讨论自签名证书的处理,这是企业内网应用经常遇到的难题!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐