常见HarmonyOS开发中自签名证书处理:企业内网HTTPS
当正规军的CA证书无法覆盖你的内网服务,自签名证书就是你的民兵武装
一、背景与动机:为什么需要自签名证书?
正规HTTPS证书由权威CA机构(如DigiCert、Let's Encrypt)签发,浏览器和操作系统默认信任这些CA。但企业内网场景下,这套机制遇到了问题:
内网域名无法验证 :CA只签发公网域名证书,192.168.1.100或internal.company.local这种内网地址,CA无法验证所有权。
成本考虑:企业可能有几十上百个内网服务,每个都买证书成本太高。Let's Encrypt虽然免费,但不支持内网域名。
离线环境:某些安全级别高的企业,内网完全隔离,无法连接外部CA进行验证。
开发测试 :本地开发环境需要HTTPS测试,但不可能为localhost申请证书。
自签名证书就是解决这些问题的方案------自己给自己签发证书,不需要CA参与。但问题来了:既然不是CA签发的,系统默认不信任,如何让鸿蒙应用信任自签名证书呢?
二、核心原理:自签名证书的信任链
2.1 正规证书 vs 自签名证书
#mermaid-svg-gm2WnYKzJ1Y6ZsCd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .error-icon{fill:#552222;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .marker.cross{stroke:#333333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd p{margin:0;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .cluster-label text{fill:#333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .cluster-label span{color:#333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .cluster-label span p{background-color:transparent;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .label text,#mermaid-svg-gm2WnYKzJ1Y6ZsCd span{fill:#333;color:#333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .node rect,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .node circle,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .node ellipse,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .node polygon,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .rough-node .label text,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .node .label text,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .image-shape .label,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .icon-shape .label{text-anchor:middle;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .rough-node .label,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .node .label,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .image-shape .label,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .icon-shape .label{text-align:center;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .node.clickable{cursor:pointer;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .arrowheadPath{fill:#333333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .cluster text{fill:#333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .cluster span{color:#333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd rect.text{fill:none;stroke-width:0;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .icon-shape,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .icon-shape p,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .icon-shape .label rect,#mermaid-svg-gm2WnYKzJ1Y6ZsCd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .primary>*{fill:#4A90E2!important;stroke:#2E5C8A!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .primary span{fill:#4A90E2!important;stroke:#2E5C8A!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .primary tspan{fill:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .warning>*{fill:#F5A623!important;stroke:#C17D10!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .warning span{fill:#F5A623!important;stroke:#C17D10!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .warning tspan{fill:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .error>*{fill:#D0021B!important;stroke:#8B0012!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .error span{fill:#D0021B!important;stroke:#8B0012!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .error tspan{fill:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .info>*{fill:#7ED321!important;stroke:#5BA318!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .info span{fill:#7ED321!important;stroke:#5BA318!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-gm2WnYKzJ1Y6ZsCd .info tspan{fill:#fff!important;} 自签名证书链
系统不信任
手动导入信任库
自签名证书
internal.company.local
❌ 无CA签发
❌ 校验失败
✅ 手动信任
正规证书链
系统内置信任
根证书
DigiCert Root CA
中间证书
DigiCert Intermediate
服务器证书
api.example.com
✅ 自动信任
2.2 信任自签名证书的方式
| 方式 | 适用场景 | 安全性 | 实现难度 |
|---|---|---|---|
| 系统信任库导入 | 企业设备统一管理 | 高 | 低(运维操作) |
| 应用内证书固定 | 特定应用信任 | 中 | 中(代码实现) |
| 自定义校验逻辑 | 灵活控制 | 低-高 | 高(需谨慎) |
| 忽略证书错误(不推荐) | 仅开发测试 | 极低 | 低 |
2.3 自签名证书生成
在讨论鸿蒙如何处理之前,先了解如何生成自签名证书。
bash
# 使用OpenSSL生成自签名证书
# 1. 生成私钥
openssl genrsa -out server.key 2048
# 2. 生成证书签名请求(CSR)
openssl req -new -key server.key -out server.csr \
-subj "/CN=internal.company.local/O=MyCompany/C=CN"
# 3. 生成自签名证书(有效期365天)
openssl x509 -req -days 365 -in server.csr \
-signkey server.key -out server.crt
# 4. 查看证书信息
openssl x509 -in server.crt -text -noout
# 5. 导出为PKCS12格式(某些服务器需要)
openssl pkcs12 -export -out server.p12 \
-inkey server.key -in server.crt \
-password pass:yourpassword
三、代码实战:三种处理方案
场景一:应用内嵌入证书文件
将自签名证书打包到应用中,在请求时使用。
typescript
import http from '@ohos.net.http';
import fs from '@ohos.file.fs';
import { BusinessError } from '@ohos.base';
// 证书管理器
class SelfSignedCertManager {
private certificates: Map<string, Uint8Array> = new Map();
// 从应用资源加载证书
async loadCertificate(hostname: string, certPath: string): Promise<boolean> {
try {
// 读取证书文件
let file = fs.openSync(certPath, fs.OpenMode.READ_ONLY);
let stat = fs.statSync(certPath);
let buffer = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file);
// 转换为Uint8Array并存储
let certData = new Uint8Array(buffer);
this.certificates.set(hostname, certData);
console.info(`证书加载成功: ${hostname}`);
return true;
} catch (error) {
console.error(`证书加载失败: ${hostname}`, error);
return false;
}
}
// 获取指定主机的证书
getCertificate(hostname: string): Uint8Array | undefined {
return this.certificates.get(hostname);
}
// 获取所有已加载证书的主机
getTrustedHosts(): string[] {
return Array.from(this.certificates.keys());
}
}
// 自签名证书HTTP客户端
class SelfSignedHttpClient {
private certManager: SelfSignedCertManager;
private defaultTimeout: number = 30000;
constructor(certManager: SelfSignedCertManager) {
this.certManager = certManager;
}
// 发起HTTPS请求(使用自签名证书)
async request(
url: string,
options: http.HttpRequestOptions
): Promise<http.HttpResponse | null> {
let httpRequest = http.createHttp();
try {
// 解析URL获取主机名
let hostname = this.extractHostname(url);
// 检查是否有对应的自签名证书
let certData = this.certManager.getCertificate(hostname);
if (certData) {
console.info(`使用自签名证书访问: ${hostname}`);
// HarmonyOS 6: 配置自定义证书
// 注:实际API需要参考官方文档
let response = await httpRequest.request(url, {
...options,
connectTimeout: this.defaultTimeout,
readTimeout: this.defaultTimeout
// 自定义证书配置(示意)
// customCertificates: [certData]
});
return response;
} else {
// 无自签名证书,使用默认HTTPS
console.info(`使用默认HTTPS访问: ${hostname}`);
return await httpRequest.request(url, {
...options,
connectTimeout: this.defaultTimeout,
readTimeout: this.defaultTimeout
});
}
} catch (error) {
let e = error as BusinessError;
console.error(`请求失败: ${e.message}`);
return null;
} finally {
httpRequest.destroy();
}
}
// 提取主机名
private extractHostname(url: string): string {
try {
let urlObj = new URL(url);
return urlObj.hostname;
} catch {
return '';
}
}
}
// 初始化和使用示例
async function initSelfSignedClient(): Promise<SelfSignedHttpClient> {
let certManager = new SelfSignedCertManager();
// 加载内网服务器的自签名证书
await certManager.loadCertificate(
'internal.company.local',
'/resources/certs/internal.crt'
);
await certManager.loadCertificate(
'192.168.1.100',
'/resources/certs/server100.crt'
);
console.info('已信任的自签名证书主机:', certManager.getTrustedHosts());
return new SelfSignedHttpClient(certManager);
}
// UI组件中使用
@Entry
@Component
struct InternalApiPage {
@State responseData: string = '';
@State client: SelfSignedHttpClient | null = null;
async aboutToAppear() {
this.client = await initSelfSignedClient();
}
async fetchInternalData() {
if (!this.client) return;
let response = await this.client.request(
'https://internal.company.local/api/data',
{
method: http.RequestMethod.GET,
header: { 'Content-Type': 'application/json' }
}
);
if (response && response.responseCode === 200) {
this.responseData = response.result as string;
}
}
build() {
Column() {
Button('获取内网数据')
.onClick(() => this.fetchInternalData())
Text(this.responseData || '暂无数据')
.margin({ top: 20 })
}
.padding(20)
}
}
场景二:动态下载和缓存证书
对于证书可能更新的场景,从服务器动态获取证书并缓存。
typescript
import http from '@ohos.net.http';
import fs from '@ohos.file.fs';
import preferences from '@ohos.data.preferences';
// 证书缓存管理
class CertificateCache {
private cacheDir: string;
private preferences: preferences.Preferences | null = null;
constructor(cacheDir: string) {
this.cacheDir = cacheDir;
}
// 初始化
async init(context: Context): Promise<void> {
try {
this.preferences = await preferences.getPreferences(context, 'cert_cache');
} catch (error) {
console.error('初始化证书缓存失败:', error);
}
}
// 获取缓存的证书路径
async getCachedCertPath(hostname: string): Promise<string | null> {
if (!this.preferences) return null;
try {
let certInfo = await this.preferences.get(hostname, '') as string;
if (certInfo) {
let info = JSON.parse(certInfo);
// 检查证书是否过期
if (new Date(info.expiry) > new Date()) {
return info.path;
}
}
} catch (error) {
console.error('获取缓存证书失败:', error);
}
return null;
}
// 缓存证书
async cacheCertificate(
hostname: string,
certData: Uint8Array,
expiry: Date
): Promise<string> {
// 保存证书文件
let certPath = `${this.cacheDir}/${hostname}.crt`;
let file = fs.openSync(certPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
fs.writeSync(file.fd, certData.buffer);
fs.closeSync(file);
// 保存元数据
if (this.preferences) {
await this.preferences.put(hostname, JSON.stringify({
path: certPath,
expiry: expiry.toISOString(),
cachedAt: new Date().toISOString()
}));
await this.preferences.flush();
}
return certPath;
}
// 清除过期证书
async cleanExpired(): Promise<void> {
if (!this.preferences) return;
let allKeys = await this.preferences.getAll();
for (let [hostname, info] of Object.entries(allKeys)) {
try {
let certInfo = JSON.parse(info as string);
if (new Date(certInfo.expiry) < new Date()) {
// 删除过期证书文件
fs.unlinkSync(certInfo.path);
// 删除缓存记录
await this.preferences.delete(hostname);
}
} catch (error) {
// 忽略解析错误
}
}
await this.preferences.flush();
}
}
// 动态证书获取器
class DynamicCertificateFetcher {
private cache: CertificateCache;
private certManager: SelfSignedCertManager;
constructor(cache: CertificateCache, certManager: SelfSignedCertManager) {
this.cache = cache;
this.certManager = certManager;
}
// 获取证书(优先使用缓存)
async getCertificate(hostname: string): Promise<boolean> {
// 1. 检查缓存
let cachedPath = await this.cache.getCachedCertPath(hostname);
if (cachedPath) {
console.info(`使用缓存证书: ${hostname}`);
return await this.certManager.loadCertificate(hostname, cachedPath);
}
// 2. 从服务器下载证书
console.info(`下载证书: ${hostname}`);
let certData = await this.downloadCertificate(hostname);
if (certData) {
// 解析证书有效期
let expiry = await this.parseCertificateExpiry(certData);
// 缓存证书
let certPath = await this.cache.cacheCertificate(hostname, certData, expiry);
// 加载到内存
return await this.certManager.loadCertificate(hostname, certPath);
}
return false;
}
// 从证书服务器下载证书
private async downloadCertificate(hostname: string): Promise<Uint8Array | null> {
let httpRequest = http.createHttp();
try {
// 假设有一个专门的证书分发服务
let certServer = 'https://cert-server.company.com';
let response = await httpRequest.request(
`${certServer}/api/certs/${hostname}`,
{
method: http.RequestMethod.GET,
header: { 'Accept': 'application/x-x509-ca-cert' }
}
);
if (response.responseCode === 200) {
// 假设响应是Base64编码的证书
let base64Cert = response.result as string;
return this.base64ToUint8Array(base64Cert);
}
return null;
} catch (error) {
console.error('下载证书失败:', error);
return null;
} finally {
httpRequest.destroy();
}
}
// 解析证书有效期
private async parseCertificateExpiry(certData: Uint8Array): Promise<Date> {
// 实际需要使用证书解析库
// 这里返回一个默认值(1年后)
let expiry = new Date();
expiry.setFullYear(expiry.getFullYear() + 1);
return expiry;
}
// Base64转Uint8Array
private base64ToUint8Array(base64: string): Uint8Array {
// 实际实现需要使用Base64解码API
return new Uint8Array(0);
}
}
// 使用示例
async function setupDynamicCerts(context: Context): Promise<SelfSignedHttpClient> {
let certManager = new SelfSignedCertManager();
let cache = new CertificateCache('/data/local/tmp/cert_cache');
await cache.init(context);
// 清理过期证书
await cache.cleanExpired();
let fetcher = new DynamicCertificateFetcher(cache, certManager);
// 获取内网服务证书
await fetcher.getCertificate('internal.company.local');
await fetcher.getCertificate('api.internal.local');
return new SelfSignedHttpClient(certManager);
}
场景三:企业CA证书链处理
企业通常有自己的CA服务器,需要处理完整的证书链。
typescript
// 企业CA证书配置
interface EnterpriseCAConfig {
// 根CA证书
rootCA: Uint8Array;
// 中间CA证书(可选)
intermediateCAs: Uint8Array[];
// 证书吊销列表(CRL)
crlUrls: string[];
// OCSP服务器地址
ocspServer?: string;
}
// 企业CA信任管理器
class EnterpriseCATrustManager {
private config: EnterpriseCAConfig;
private trustedCerts: Set<string> = new Set();
constructor(config: EnterpriseCAConfig) {
this.config = config;
}
// 初始化信任链
async initialize(): Promise<void> {
// 1. 加载根CA
let rootFingerprint = await this.calculateFingerprint(this.config.rootCA);
this.trustedCerts.add(rootFingerprint);
console.info('根CA已信任:', rootFingerprint.substring(0, 16) + '...');
// 2. 加载中间CA
for (let intermediate of this.config.intermediateCAs) {
let fingerprint = await this.calculateFingerprint(intermediate);
this.trustedCerts.add(fingerprint);
console.info('中间CA已信任:', fingerprint.substring(0, 16) + '...');
}
// 3. 下载CRL
await this.downloadCRLs();
}
// 校验服务器证书
async verifyServerCertificate(certChain: Uint8Array[]): Promise<boolean> {
if (certChain.length === 0) {
return false;
}
// 检查证书链是否以受信任的CA结尾
let rootCert = certChain[certChain.length - 1];
let rootFingerprint = await this.calculateFingerprint(rootCert);
if (!this.trustedCerts.has(rootFingerprint)) {
console.error('证书链不以受信任的CA结尾');
return false;
}
// 验证证书链完整性
for (let i = 0; i < certChain.length - 1; i++) {
let isValid = await this.verifyCertificateSignature(
certChain[i],
certChain[i + 1]
);
if (!isValid) {
console.error(`证书链第${i}层签名验证失败`);
return false;
}
}
// 检查服务器证书是否被吊销
let serverCert = certChain[0];
if (await this.isRevoked(serverCert)) {
console.error('服务器证书已被吊销');
return false;
}
return true;
}
// 计算证书指纹
private async calculateFingerprint(cert: Uint8Array): Promise<string> {
// 实际需要使用SHA-256计算
return 'simulated_fingerprint';
}
// 验证证书签名
private async verifyCertificateSignature(
cert: Uint8Array,
issuerCert: Uint8Array
): Promise<boolean> {
// 实际需要使用加密API验证签名
return true;
}
// 下载CRL
private async downloadCRLs(): Promise<void> {
for (let crlUrl of this.config.crlUrls) {
try {
let httpRequest = http.createHttp();
let response = await httpRequest.request(crlUrl, {
method: http.RequestMethod.GET
});
if (response.responseCode === 200) {
console.info('CRL下载成功:', crlUrl);
// 解析并存储CRL
}
httpRequest.destroy();
} catch (error) {
console.error('CRL下载失败:', crlUrl, error);
}
}
}
// 检查证书是否被吊销
private async isRevoked(cert: Uint8Array): Promise<boolean> {
// 检查CRL或OCSP
return false;
}
}
// 企业内网HTTP客户端
class EnterpriseHttpClient {
private trustManager: EnterpriseCATrustManager;
constructor(trustManager: EnterpriseCATrustManager) {
this.trustManager = trustManager;
}
async request(url: string, options: http.HttpRequestOptions): Promise<http.HttpResponse | null> {
let httpRequest = http.createHttp();
try {
// 配置企业CA信任
let response = await httpRequest.request(url, {
...options,
// HarmonyOS 6: 配置自定义信任管理器
// 注:实际API需要参考官方文档
});
return response;
} catch (error) {
console.error('请求失败:', error);
return null;
} finally {
httpRequest.destroy();
}
}
}
// 初始化企业CA
async function initEnterpriseCA(): Promise<EnterpriseHttpClient> {
// 加载企业CA证书(从应用资源)
let rootCA = await loadCertFromResource('/resources/certs/enterprise-root-ca.crt');
let intermediateCA = await loadCertFromResource('/resources/certs/enterprise-intermediate.crt');
let config: EnterpriseCAConfig = {
rootCA: rootCA,
intermediateCAs: [intermediateCA],
crlUrls: ['https://ca.company.com/crl/enterprise.crl'],
ocspServer: 'https://ca.company.com/ocsp'
};
let trustManager = new EnterpriseCATrustManager(config);
await trustManager.initialize();
return new EnterpriseHttpClient(trustManager);
}
// 辅助函数:从资源加载证书
async function loadCertFromResource(path: string): Promise<Uint8Array> {
let file = fs.openSync(path, fs.OpenMode.READ_ONLY);
let stat = fs.statSync(path);
let buffer = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file);
return new Uint8Array(buffer);
}
四、踩坑与注意事项
坑点一:证书格式不兼容
不同服务器可能使用不同证书格式,需要正确转换。
typescript
// 常见证书格式
enum CertFormat {
PEM = 'PEM', // 最常见,Base64编码
DER = 'DER', // 二进制格式
PKCS12 = 'P12', // 包含私钥和证书
PKCS7 = 'P7B' // 证书链格式
}
// 证书格式转换器
class CertFormatConverter {
// PEM转DER
pemToDer(pem: string): Uint8Array {
// 移除PEM头尾
let lines = pem.split('\n');
let base64 = lines
.filter(line => !line.startsWith('-----'))
.join('');
// Base64解码
return this.base64Decode(base64);
}
// DER转PEM
derToPem(der: Uint8Array, label: string = 'CERTIFICATE'): string {
let base64 = this.base64Encode(der);
let lines = [];
lines.push(`-----BEGIN ${label}-----`);
// 每64字符一行
for (let i = 0; i < base64.length; i += 64) {
lines.push(base64.substring(i, i + 64));
}
lines.push(`-----END ${label}-----`);
return lines.join('\n');
}
private base64Decode(base64: string): Uint8Array {
// 实际需要使用Base64解码API
return new Uint8Array(0);
}
private base64Encode(data: Uint8Array): string {
// 实际需要使用Base64编码API
return '';
}
}
坑点二:证书更新未同步
服务器证书更新后,客户端仍使用旧证书导致连接失败。
typescript
// 证书版本管理
class CertificateVersionManager {
private versions: Map<string, number> = new Map();
// 记录证书版本
recordVersion(hostname: string, version: number): void {
this.versions.set(hostname, version);
}
// 检查证书是否需要更新
async checkForUpdate(hostname: string, currentCert: Uint8Array): Promise<boolean> {
try {
// 从服务器获取最新证书版本
let latestVersion = await this.fetchLatestVersion(hostname);
let currentVersion = this.versions.get(hostname) || 0;
if (latestVersion > currentVersion) {
console.info(`证书需要更新: ${hostname} (${currentVersion} -> ${latestVersion})`);
return true;
}
return false;
} catch (error) {
console.error('检查证书更新失败:', error);
return false;
}
}
private async fetchLatestVersion(hostname: string): Promise<number> {
// 实际需要调用版本查询API
return 1;
}
}
坑点三:多环境证书混淆
开发、测试、生产环境使用不同证书,容易混淆。
typescript
// 环境感知的证书管理
class EnvironmentAwareCertManager {
private environment: 'dev' | 'test' | 'prod';
private certPaths: Map<string, Map<string, string>> = new Map();
constructor() {
// 初始化不同环境的证书路径
this.certPaths.set('dev', new Map([
['api.internal.local', '/resources/certs/dev/api.crt']
]));
this.certPaths.set('test', new Map([
['api.test.local', '/resources/certs/test/api.crt']
]));
this.certPaths.set('prod', new Map([
['api.company.local', '/resources/certs/prod/api.crt']
]));
// 检测当前环境
this.environment = this.detectEnvironment();
}
// 获取当前环境的证书路径
getCertPath(hostname: string): string | null {
let envCerts = this.certPaths.get(this.environment);
return envCerts?.get(hostname) || null;
}
// 环境检测
private detectEnvironment(): 'dev' | 'test' | 'prod' {
// 可以通过构建配置、环境变量等方式检测
// 这里简单返回dev
return 'dev';
}
}
五、HarmonyOS 6适配指南
5.1 自定义信任管理器
HarmonyOS 6提供了更灵活的证书信任配置。
typescript
import http from '@ohos.net.http';
// HarmonyOS 6: 自定义信任配置
let trustOptions: http.TrustManagerOptions = {
// 自定义证书来源
customCertificates: [
{
hostname: 'internal.company.local',
certData: certData1 // Uint8Array
},
{
hostname: 'api.internal.local',
certData: certData2
}
],
// 是否信任系统CA(默认true)
trustSystemCA: true,
// 证书校验回调
onVerify: (certChain: Uint8Array[], hostname: string) => {
// 自定义校验逻辑
console.info(`校验证书: ${hostname}`);
return true; // 返回true表示信任
}
};
// 应用信任配置
let httpRequest = http.createHttp();
let response = await httpRequest.request(
'https://internal.company.local/api/data',
{
method: http.RequestMethod.GET,
trustOptions: trustOptions // HarmonyOS 6新增
}
);
5.2 证书热更新
HarmonyOS 6支持运行时更新信任证书,无需重启应用。
typescript
// 证书热更新管理
class HotUpdateCertManager {
private trustManager: http.TrustManager;
constructor() {
this.trustManager = http.createTrustManager();
}
// 添加新证书
async addCertificate(hostname: string, certData: Uint8Array): Promise<void> {
await this.trustManager.addTrustedCertificate(hostname, certData);
console.info(`证书已添加: ${hostname}`);
}
// 移除证书
async removeCertificate(hostname: string): Promise<void> {
await this.trustManager.removeTrustedCertificate(hostname);
console.info(`证书已移除: ${hostname}`);
}
// 获取当前信任的所有证书
async getTrustedCertificates(): Promise<string[]> {
return await this.trustManager.listTrustedCertificates();
}
}
// 使用示例
async function updateCerts() {
let manager = new HotUpdateCertManager();
// 动态添加新服务器证书
let newCert = await fetchCertificate('new-server.company.local');
await manager.addCertificate('new-server.company.local', newCert);
// 查看当前信任列表
let trusted = await manager.getTrustedCertificates();
console.info('当前信任的证书:', trusted);
}
六、总结一下下
自签名证书是企业内网HTTPS的常见需求,正确处理既能保证安全又能满足业务需求。本文从三个场景展开:
应用内嵌入证书:将证书文件打包到应用,启动时加载。适合证书相对固定的场景,实现简单。
动态下载缓存:从证书服务器动态获取,支持证书更新。适合证书可能变化的场景,需要处理缓存和过期。
企业CA证书链:处理完整证书链,包括根CA、中间CA、CRL/OCSP。适合企业级部署,安全性最高。
三个常见坑点:证书格式不兼容、证书更新未同步、多环境证书混淆。遇到证书问题时,先排查这三个方面。
HarmonyOS 6提供了自定义信任管理器和证书热更新能力,让自签名证书的处理更加灵活。但切记:自签名证书虽然方便,安全责任也完全在自己,务必做好证书管理和更新机制。
下一篇文章,我们将进入WebSocket实时通信的世界,探讨长连接的最佳实践!