HarmonyOS开发中HTTPS与证书校验:网络安全实践
HarmonyOS开发中HTTPS与证书校验:网络安全实践
在数据裸奔的时代,HTTPS是你的应用穿上的第一件防护服
一、背景与动机:为什么HTTPS如此重要?
还记得几年前,运营商在你的网页里强行插入广告弹窗吗?还记得公共WiFi下,你的聊天记录被截获的新闻吗?这些都是HTTP明文传输带来的安全隐患。
HTTPS(HTTP Secure)通过TLS/SSL协议,在客户端和服务器之间建立加密通道。即使数据被截获,攻击者也只能看到一堆乱码。这就像写信时用只有你和收信人知道的密码加密,中间人截获了也看不懂。
鸿蒙应用默认要求使用HTTPS,这是系统级的安全策略。但在实际开发中,HTTPS配置不当反而会带来问题:
证书校验过严:自签名证书、企业内网证书无法通过校验,导致请求失败。
证书校验过松:忽略所有证书错误,HTTPS形同虚设,中间人攻击畅通无阻。
证书过期未处理:服务器证书更新后,客户端没有相应处理,导致服务中断。
本文将深入探讨HTTPS的工作原理,以及在鸿蒙中的正确配置方式。
二、核心原理:HTTPS握手与证书校验
2.1 HTTPS工作流程
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、证书透明度等新特性,安全性进一步提升。合理利用这些特性,可以让应用在网络攻击面前更加坚固。
下一篇文章,我们将专门讨论自签名证书的处理,这是企业内网应用经常遇到的难题!
更多推荐




所有评论(0)