PHP 怎么双向TLS

wen PHP项目 2

PHP 实现双向 TLS(mTLS)完整指南:原理、代码与实战踩坑

📚 目录导读

  1. 什么是双向 TLS?与单向 HTTPS 的本质区别
  2. 核心原理:数字证书、私钥与信任链的握手过程
  3. 环境准备:OpenSSL 生成 CA、服务端与客户端证书
  4. PHP 代码实现:cURL 流(最推荐)与 Stream 上下文(备选)
  5. Nginx + PHP-FPM 架构下的 mTLS 配置与 PHP 获取客户端证书
  6. 高频问题解答(FAQ):证书验证失败、浏览器警告、自签名证书
  7. SEO 优化要点:独特价值与实战建议

什么是双向 TLS?与单向 HTTPS 的本质区别

很多开发者都知道 HTTPS 使用 TLS 加密通信,但那是单向 TLS:只验证服务器身份(客户端验证服务器证书),而双向 TLS(Mutual TLS,简称 mTLS),要求客户端也要出示证书,服务器验证客户端证书的合法性后,才会建立加密连接。

PHP 怎么双向TLS

一句话总结:单向 TLS 是“我知道你在和谁说话”,双向 TLS 是“我知道你在和谁说话,你也必须证明你是谁”。

典型应用场景

  • API 网关服务间调用(如微服务)
  • 银行、金融行业的接口对接
  • 物联网设备与服务器认证
  • 企业内部高安全性的管理后台

核心原理:数字证书、私钥与信任链的握手过程

1 三要素角色

角色 文件 说明
CA(证书颁发机构) ca.crt(公钥)+ ca.key(私钥) 负责签发其他证书
服务端 server.crt + server.key 证明服务器身份
客户端 client.crt + client.key 证明客户端身份

2 握手流程(简化版)

  1. 客户端发起请求,并发送 ClientHello(包含支持的加密套件)
  2. 服务端回应 ServerHello,发送自己的 server.crt,并要求客户端提供证书
  3. 客户端用 CA 公钥验证服务端证书有效,然后发送自己的 client.crt 和签名数据
  4. 服务端用 CA 公钥验证客户端证书,双方协商出对称密钥,开始加密通信

核心验证逻辑:双方都相信同一个 CA,CA 是信任的锚点。


环境准备:OpenSSL 生成 CA、服务端与客户端证书

这是最容易出错的环节!我直接给出可跑通的命令(Linux / macOS 通用)。

# 1. 创建 CA(证书颁发机构)
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyOrg/CN=MyRootCA"
# 2. 生成服务端私钥与证书签名请求(CSR)
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyOrg/CN=localhost"
# 3. 用 CA 签发服务端证书(注意扩展字段,必须加 serverAuth)
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt -days 365 -sha256 \
  -extfile <(printf "extendedKeyUsage=serverAuth\nsubjectAltName=DNS:localhost,IP:127.0.0.1")
# 4. 生成客户端私钥与 CSR
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyOrg/CN=my-client"
# 5. 用 CA 签发客户端证书(必须加 clientAuth)
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out client.crt -days 365 -sha256 \
  -extfile <(printf "extendedKeyUsage=clientAuth")

⚠️ 绝对避坑提示extendedKeyUsage 必须区分 serverAuthclientAuth,否则 OpenSSL 会报错 “unsupported certificate purpose”。


PHP 代码实现:cURL 流(最推荐)与 Stream 上下文(备选)

1 使用 cURL 扩展(最稳定、功能最全)

<?php
function makeMutualTlsRequest($url, $data = []) {
    $ch = curl_init();
    $options = [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode($data),
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        // ==== 关键 mTLS 配置 ====
        CURLOPT_SSL_VERIFYPEER => true,          // 验证服务器证书(必须开启)
        CURLOPT_SSL_VERIFYHOST => 2,             // 验证主机名(必须开启,2是严格模式)
        // 指定 CA 证书(用于验证服务器证书)
        CURLOPT_CAINFO => '/path/to/ca.crt',
        // 指定客户端证书和私钥(这是双向 TLS 的核心)
        CURLOPT_SSLCERT => '/path/to/client.crt',
        CURLOPT_SSLKEY => '/path/to/client.key',
        CURLOPT_SSLKEYPASSWD => 'your_key_password_if_any', // 如果私钥有密码
        // 强制使用 TLS(可选)
        CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
    ];
    curl_setopt_array($ch, $options);
    $response = curl_exec($ch);
    if (curl_errno($ch)) {
        $error = curl_error($ch);
        curl_close($ch);
        throw new RuntimeException("cURL 错误: $error");
    }
    curl_close($ch);
    return $response;
}
// 调用示例
try {
    $result = makeMutualTlsRequest('https://api.example.com/mtls-endpoint', ['hello' => 'world']);
    echo $result;
} catch (Exception $e) {
    echo "请求失败: " . $e->getMessage();
}

解释

  • CURLOPT_SSLCERTCURLOPT_SSLKEY 是开启 mTLS 的开关。
  • 如果私钥是加密的(生成时加了 -aes256),必须提供 CURLOPT_SSLKEYPASSWD
  • 如果证书是 PKCS#12 格式(.pfx),需要先转换成 PEM 格式,或者用 CURLOPT_SSLCERTTYPE 指定类型。

2 使用 PHP Stream Context(备选方案)

<?php
$context = stream_context_create([
    'ssl' => [
        'verify_peer' => true,
        'verify_peer_name' => true,
        'cafile' => '/path/to/ca.crt',
        // 客户端证书和私钥(必须指向同一份文件或分别指定)
        'local_cert' => '/path/to/client.crt',
        'local_pk' => '/path/to/client.key',
        'passphrase' => 'your_key_password_if_any',
        // 允许自签名证书(不建议生产环境开启)
        'allow_self_signed' => false,
    ],
]);
$fp = fopen('https://api.example.com/mtls-endpoint', 'r', false, $context);
if ($fp) {
    $response = stream_get_contents($fp);
    fclose($fp);
    echo $response;
} else {
    echo "连接失败";
}

推荐使用 cURL 的原因:错误信息更详细、支持重试、超时控制更灵活、对证书错误提示清晰。


Nginx + PHP-FPM 架构下的 mTLS 配置与 PHP 获取客户端证书

1 Nginx 侧配置(作为服务器接收 mTLS 请求)

server {
    listen 443 ssl;
    server_name api.example.com;
    # 服务器证书
    ssl_certificate /path/to/server.crt;
    ssl_certificate_key /path/to/server.key;
    # CA 证书(验证客户端证书)
    ssl_client_certificate /path/to/ca.crt;
    # 开启客户端证书验证(on 强制要求,optional 可选)
    ssl_verify_client on;
    # 可选:将客户端证书信息传递给 PHP
    fastcgi_param SSL_CLIENT_S_DN $ssl_client_s_dn;
    fastcgi_param SSL_CLIENT_VERIFY $ssl_client_verify;
    location / {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    }
}

2 PHP 中读取客户端证书信息

<?php
// 在 PHP-FPM 环境下,Nginx 会通过 fastcgi_param 传入这些变量
if (isset($_SERVER['SSL_CLIENT_VERIFY']) && $_SERVER['SSL_CLIENT_VERIFY'] === 'SUCCESS') {
    echo "客户端证书验证通过!<br>";
    echo "证书主题: " . htmlspecialchars($_SERVER['SSL_CLIENT_S_DN']);
    // 输出示例: /C=CN/ST=Beijing/L=Beijing/O=MyOrg/CN=my-client
} else {
    // 如果客户端未提供有效证书,可以返回 403
    http_response_code(403);
    die("Unauthorized: 缺少有效客户端证书");
}

注意SSL_CLIENT_S_DN 是证书的 DN(Distinguished Name),你可以从中提取 CN(Common Name)来识别客户端身份,做细粒度授权。


高频问题解答(FAQ)

Q1:证书验证报错 "unable to get local issuer certificate" 怎么办? A:这说明 PHP 找不到 CA 证书,检查 CURLOPT_CAINFO 路径是否正确,且权限可读,也可用绝对路径,避免相对路径问题。

Q2:客户端证书和私钥文件需要设置什么权限? A:建议设为 600(仅拥有者可读写),防止其他用户读取私钥,PHP 运行用户需要可读权限。

Q3:浏览器访问时会警告"不安全",但 cURL 正常,为什么? A:浏览器不信任你自建的 CA,你需要将 ca.crt 导入系统信任根证书库,Windows/macOS 都有证书导入向导。

Q4:能否让 mTLS 和普通 HTTPS 共存于同一端口? A:可以,Nginx 设置 ssl_verify_client optional(而不是 on),但此时 PHP 必须检查 SSL_CLIENT_VERIFY 是否等于 SUCCESS 来决定是否授权。

Q5:私钥可以放在数据库或 Redis 中吗? A:理论上可以用内存缓存,但 cURL 扩展只接受文件路径,你可以写入临时文件后使用,但注意安全性和并发清理。


SEO 优化要点:独特价值与实战建议

本文通过中文关键词“PHP 双向 TLS” 精准定位,内容覆盖:

  • 从证书生成到 PHP 代码的全链路实操(区别于纯理论文章)
  • 给出了 Nginx 环境下客户端证书信息传递给 PHP 的完整方案
  • 包含真实避坑建议(如 extendedKeyUsage、CURLOPT_CAINFO 路径等)

建议你在自己的服务器上完整复现一次,因为只有亲手跑通,才能应对生产环境中的各种异常(如证书过期、私钥密码错误、SAN 域名不匹配等)。

如果本文有帮助,欢迎按下面方式实操后留言交流遇到的坑,祝你实现安全的内部 API 互联!

抱歉,评论功能暂时关闭!