基于SSL/TSL证书实现HTTPS双向认证2-实践方案
本文介绍了以下内容:
- HTTPS双向认证的过程。
- 笔者所要解决的问题场景以及进行的前期准备(申请客户端证书)。
- 前端携带证书发请求的几种方案,以及方案的代码样例和优缺点等。
双向认证介绍
上篇文章已经介绍了HTTPS单向认证的原理和过程,详见基于SSL/TSL证书实现HTTPS双向认证1-原理介绍。
HTTPS中的单向认证即为服务端认证,而客户端认证是可选的。
单向认证的过程中,客户端从服务器端下载服务器端公钥证书进行验证,然后建立安全通信通道。
HTTPS在TLS标准中是提供了双向认证的能力的,双向认证是指客户端和服务器端都需要验证对方的身份,在建立Https连接的过程中,握手的流程比单向认证多了几步。
双向认证的过程中,客户端除了需要从服务器端下载服务器的公钥证书进行验证外,还需要把客户端的公钥证书上传到服务器端给服务器端进行验证,等双方都认证通过了,才开始建立安全通信通道进行数据传输。
问题场景描述
目前要在移动端新增功能,该功能的后端接口原本是只能内网访问,由于新功能只能在外网环境下使用,现需要将该API接口暴露在公网环境下,为了安全性考虑,需要给移动端增加客户端认证,由此笔者对双向认证展开调,并提出了几个实践方案以及其优缺点。
申请客户端证书
需要准备客户端证书,包括:
- cert.pem:公钥证书,
- private_key.pem:私钥证书,
- trust_cert_chain_server.pem:根证书。
客户端的工作需要以下步骤:
- 生成客户端证书和私钥,可以使用openssl命令或者其他工具生成,注意证书需要符合服务器端的要求。
- 把客户端证书上传到服务器端,服务器端需要把客户端证书添加到信任列表中。
- 在前端代码中添加证书验证的逻辑,通过浏览器的API实现,比如在请求服务器端API时,需要把客户端证书和私钥一起发送到服务器端,然后根据服务器端返回的证书进行验证。
- 如果证书验证通过,建立安全通信通道进行数据传输。
需要注意的是,在前端代码中添加证书验证的逻辑时,需要注意证书的安全性和有效期,以及证书的更新和维护等问题。
前端携带证书发请求
证书放到请求头
在发请求的时候,前端需要使用XMLHttpRequest或Fetch API来发送请求。
其中,XMLHttpRequest可以使用setRequestHeader方法来设置请求头,Fetch API可以使用headers属性来设置请求头。
在设置请求头的时候,需要添加Authorization字段,并把证书和私钥通过Base64编码后放在该字段中。同时,还需要添加Content-Type字段,并指定为application/json,表示请求体的格式为JSON。
- 前端发送请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 生成证书和私钥,并Base64编码
const cert = 'xxxxxx'; // 客户端证书
const key = 'xxxxxx'; // 客户端私钥
const authorization = `Basic ${btoa(`${cert}:${key}`)}`;
// 发送请求
const url = 'https://example.com/api';
const data = { name: 'John Doe' };
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': authorization
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error)); - 服务端需要在接收到请求时,从请求头中获取证书和私钥,并进行验证。可以使用Node.js中的tls模块来实现证书验证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const https = require('https');
const fs = require('fs');
const path = require('path');
const server = https.createServer({
key: fs.readFileSync(path.join(__dirname, 'server-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'server-cert.pem')),
requestCert: true,
rejectUnauthorized: true
}, (req, res) => {
const cert = req.socket.getPeerCertificate();
const authorized = req.socket.authorized;
const error = req.socket.authorizationError;
if (authorized) {
// 验证证书的有效性
// ...
} else {
console.error(error);
res.statusCode = 401;
res.end('Unauthorized');
}
});
server.listen(443);
注意:在实际使用中,需要根据服务器端的要求来设置Authorization字段的格式和内容。同时,还需要对证书和私钥进行保护,避免泄露和被恶意利用。
缺点:按照这种做法,证书会暴露在静态代码中,这种行为会泄露我们的证书和私钥,在前端静态代码中直接暴露证书和私钥是非常不安全的,容易被攻击者窃取。
浏览器客户端证书存储机制
我们可以考虑使用浏览器提供的客户端证书存储机制。这种机制可以让浏览器管理我们的证书和私钥,并在需要的时候自动调用。
把客户端证书导入到浏览器的证书存储中。这个过程因浏览器不同而异,一般来说,我们可以在浏览器的设置中找到证书管理工具,然后选择“导入证书”功能,把客户端证书导入到证书存储中。
在前端代码中添加证书验证的逻辑,通过浏览器的API实现。我们可以使用XMLHttpRequest或Fetch API来发送请求,并在请求中添加证书的标识。浏览器会自动调用证书存储中的证书和私钥,进行双向认证。
您说的很对,在前端静态代码中直接暴露证书和私钥是非常不安全的,容易被攻击者窃取。为了避免这种情况,我们可以考虑使用浏览器提供的客户端证书存储机制。这种机制可以让浏览器管理我们的证书和私钥,并在需要的时候自动调用。下面是具体的步骤:生成客户端证书和私钥,可以使用openssl命令或其他工具生成,注意证书需要符合服务器端的要求。
把客户端证书上传到服务器端,并在服务器端添加到信任列表中。
把客户端证书导入到浏览器的证书存储中。这个过程因浏览器不同而异,一般来说,我们可以在浏览器的设置中找到证书管理工具,然后选择“导入证书”功能,把客户端证书导入到证书存储中。
在前端代码中添加证书验证的逻辑,通过浏览器的API实现。我们可以使用XMLHttpRequest或Fetch API来发送请求,并在请求中添加证书的标识。浏览器会自动调用证书存储中的证书和私钥,进行双向认证。
以下是示例代码:
1 | // 发送请求 |
缺点:这种方法还需要让使用用户手动导入证书,会增加软件使用成本,这会影响用户体验。另外,如果每个用户都需要手动导入证书,那管理起来也会非常麻烦。
前端动态生成证书
在客户端动态生成证书和私钥,并在发送请求时把证书和私钥一起发送给服务器端。
在前端代码中使用crypto API动态生成客户端证书和私钥。可以使用以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const { privateKey, publicKey } = await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: { name: "SHA-256" },
},
true,
["sign", "verify"]
);
const cert = await crypto.subtle.exportKey("spki", publicKey);
const key = await crypto.subtle.exportKey("pkcs8", privateKey);
const certPEM = `-----BEGIN CERTIFICATE-----\n${btoa(String.fromCharCode(...new Uint8Array(cert)))}\n-----END CERTIFICATE-----`;
const keyPEM = `-----BEGIN PRIVATE KEY-----\n${btoa(String.fromCharCode(...new Uint8Array(key)))}\n-----END PRIVATE KEY-----`;在发送请求时把证书和私钥一起发送到服务器端。可以使用XMLHttpRequest或Fetch API来发送请求,并把证书和私钥放在请求头中。以下是示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 发送请求
const url = 'https://example.com/api';
const data = { name: 'John Doe' };
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Cert': certPEM,
'X-Client-Key': keyPEM
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));在服务器端进行证书的验证。服务器端需要把客户端证书添加到信任列表中,并在接收请求时验证证书的有效性。可以使用Node.js中的tls模块来实现证书验证。以下是示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
ca: fs.readFileSync('client-cert.pem'),
requestCert: true,
rejectUnauthorized: true
};
https.createServer(options, (req, res) => {
const certPEM = req.socket.getPeerCertificate().raw.toString('base64');
// 验证证书的有效性
// ...
}).listen(443);
问题:后端本来已经根据申请好的客户端证书写好了一套逻辑,如果前端改为用动态生成证书的方式,那么后端需要重新开发。
nginx代理
可以通过nginx来实现这个功能,在nginx中开启双向认证。
在nginx配置文件中添加以下内容,用于指定客户端证书和私钥的路径和密码:
1 | ssl_client_certificate /path/to/client/cert.pem; |
其中,/path/to/client/cert.pem
和/path/to/client/key.pem
分别为客户端证书和私钥的路径,/path/to/client/password.txt
为密钥库密码的文件路径。
在nginx配置文件中添加以下内容,用于启用双向认证:
1 | ssl_verify_client on; |
其中,ssl_verify_client on
表示启用双向认证,ssl_verify_depth 2
表示验证证书链的深度。
重启nginx服务,使配置生效。
当客户端发起请求时,nginx会要求客户端提供证书,如果客户端没有提供证书或者证书验证失败,则请求将被拒绝。同时,由于证书和私钥的路径和密码并没有写在代码中,因此也提高了证书和私钥的安全性。
问题:小程序是打包到 微信平台/飞书平台 发布的,开发人员没有办法在平台服务器中配置nginx的相关文件。
nginx+再找个域名做中转
业务接口后端为test.com,中转域名为auth.com。
- auth.com和test.com实现nginx的双向认证。
- 先从小程序代理到auth.com,这里部分接口做权限验证。通过后才可以调用业务接口。
小程序代理的样例代码:
1 | wx.request({ |