如何为Axum添加TLS支持
思路
在Axum项目的讨论区Does Axum have HTTPS support?#1032中给出了两种方法。其中tls-rustls提供了一种利用Rustls
的解决方案。
总体思路就是:主线程处理https
请求,然后利用spawn
方法启动新进程,监听http
请求,然后将所有http
请求都跳转到https
请求上;
这就涉及到本文着重记录的几个关键技术点:
- 在进行请求跳转过程中都需要哪些操作;
- 增加TLS安全层是需要证书和密钥的,这些东西如何生成;
请求跳转
我们已经明确了,需要新开一个进程来处理http
请求到https
的跳转。在这个过程中,很明显的是消息体不需要处理,需要处理的是消息头中url地址变换:
- schema -
http
协议变换为https
协议; - port - 端口号要变;
端口信息
首先,声明一个结构体用于存储HTTP
和HTTPS
的端口号。
#[derive(Clone, Copy)]
struct Ports {
http: u16,
https: u16,
}
新建HTTP进程
在主进程中由tokio::spawn
启动新进程运行,监听http端口,任何调用http
的请求都跳转到https
请求。
#[tokio::main]
async fn main() {
let ports = Ports {
http: 7878,
https: 3000,
};
// 启动新进程,以监听HTTP端口,并将http请求转发到https端口上。
tokio::spawn(redirect_http_to_https(ports));
}
其中调用的函数就是在处理Url地址变换:
async fn redirect_http_to_https(ports: Ports) {
fn make_https(host: String, uri: Uri, ports: Ports) -> Result<Uri, BoxError> {
let mut parts = uri.into_parts();
parts.scheme = Some(axum::http::uri::Scheme::HTTPS);
if parts.path_and_query.is_none() {
parts.path_and_query = Some("/".parse().unwrap());
}
let https_host = host.replace(&ports.http.to_string(), &ports.https.to_string());
parts.authority = Some(https_host.parse()?);
Ok(Uri::from_parts(parts)?)
}
let redirect = move |Host(host): Host, uri: Uri| async move {
match make_https(host, uri, ports) {
// Redirect实现了IntoResponse
Ok(uri) => Ok(Redirect::permanent(&uri.to_string())),
Err(error) => {
tracing::warn!(%error, "failed to convert URI to HTTPS");
Err(StatusCode::BAD_REQUEST)
}
}
};
let addr = SocketAddr::from(([127, 0, 0, 1], ports.http));
tracing::debug!("listening http on {}", addr);
axum::Server.bind(&addr)
.serve(redirect.into_make_service())
.await
.unwrap();
}
主要过程就是Url
变换。
为了便于理解上面代码中对Url
地址的变换,下面给出在Rust
中Url
地址类型的各个组成部分命名:
abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1
|---------| |-|
| |
host port
|-| |-------------------------------||--------| |-------------------| |-----|
| | | | |
scheme authority path query fragment
主进程处理HTTPS协议
首先引入axum-server
这个Crate
,用于为Axum
提供TLS
支持。
cargo add axum-server
要想启动对TLS的支持,必须提供一个包含公钥的数字证书和私钥。RustlsConfig
提供了两种方法来加载数字证书和私钥:
- 文件形式(
PEM
格式文件 -Base64
编码) - 字节数组
如下示例中采用的是通过文件来加载的,这也是最常用的一种方式。如果数字证书和私钥是存储在数据库中的话,可以采用字节数组方式来加载。
#[tokio::main]
async fn main() {
// 在TLS配置信息中引入了数字证书(含有公钥)文件和私钥文件
let config = RustlsConfig::from_pem_file(
PathBuf::from("self_signed_certs/")
.join("cert.pem"),
PathBuf::from("self_signed_certs/")
.join("key.pem"),
)
.await
.unwrap();
let app = Router::new().route("/", get(handler));
// run https server
let addr = SocketAddr::from(([127, 0, 0, 1], ports.https));
tracing::debug!("listening https on {}", addr);
// 利用axum-server提供的绑定方法bind_rustls来启动TLS加密通讯层
axum_server::bind_rustls(addr, config)
.serve(app.into_make_service())
.await
.unwrap();
}
下面的小节来描述如何利用openssl
命令来生成自签名数字证书和私钥。
生成TLS所需的证书和密钥
正常来讲,数字证书一般都是要去CA去购买的。所以,在此处我们是使用自签名数字证书来代替的。正是网站是不建议用自签名数字证书的,浏览器访问时会触发安全保护,拦截访问请求的(需要你把自签名数字证书加入到信任区后才能继续访问)。
数字证书和密钥的生成动作不是一步到位的,是按照如下流程来生成的。这个流程也正好是模拟的向CA申请的过程。
利用OpenSSL来生成密钥对
在我们需要为HTTP服务器增加TLS支持时,配置中需要提供数字证书(X509格式)和私钥文件。这些证书文件存在两种格式:
- PEM - Base64编码格式
- DER - 二进制编码格式
我们一般都是通过openssl
命令来生成密钥对。在不指定格式的情况下,生成的文件都默认是PEM
格式的,这是正是我们所需的格式。
生成RSA私钥
首先生成一个私钥文件,现在为了安全性默认的密钥长度都是2048比特的。
openssl genpkey -algorithm RSA -out private.key
默认生成PEM
格式的、默认长度为2048
比特的私钥文件。
还有一种写法:
openssl genrsa -out private.key 2048
该命令如果不指定密钥长度,则默认长度为1024
比特的私钥文件。
该命令已经被更通用、更强大的
genpkey
替代。
使用cat
命令可以查看生成的PEM格式密钥文件,内容如下:
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrg3UXc+/UNHab
7at8sLLV+VNWAVJkfhXQfoaK9GHsZdhpiqDN4TpRAufCWbgtT2ZZe0GmsR5qUa2L
JEQ9QCiypNgPuzqtlTUvnDguDw+aHXHdJY1TKOqkTsyR7mbwZOv8h2S9wOV3Tqyp
262jhrbWFETvrJLpBvrG/UkHmMPn3aaq7WTY0Eb4g8V0l0e5Pn3+ksNLNS8NuxnY
XcpQd0fuJH74OF39pso6kWUtlHEpBE7GGiJjNNMUnLe00SJdr9RG8/cnI7Ny93zR
APwZQ9RzenZlexvID0D6/7ezLdxgC62UbfgwvCZfrVcrmkbubimkLmbB/0ESqnc5
gbK+fIxLAgMBAAECggEAB/69w5Uc9ehoKwaeOtbFz2Gq99Rh8dtKywhK30lbEzkA
KJNUwaQhIsue4Blsk3gjDkLHoayZ/VzHWTgckjpea01x4PHaX2MYMpr2p3b+RDHh
L+zjNKAwp+4gbSEO3V/m/EOXd5z5VWFufRCHfJr4BOy29fE2e/ReVk5STC9b2oAQ
BescB3TxAHK1/NGSz3NjOFp9Lq9StMBwgQmE+wvIGf52en0lWz7B78YHQ/i/Acm2
leZNSNbcMXCW04B+Mk93cxxOkBFVHg2ghwgsBJao9aQdMo1Yfcg4kvXQ14iaVk8E
BH8Ct+REKSbhrF4nP/EElpx8USr5GtPFjVbGj+VkEQKBgQDeWfpkDuQgpb0WZQeb
xa2os3QTD1DCRcoQAmqBaj4I8aJPJXl8yjvdv8xG/jMQCLL0WUudliGa1nzspTsg
LzwtpzmAQjvUIUAxx4K0kexp6iKeD4Dcy6/PR0D8tFSqwCLJIk7Mn/02yz61zyzA
9F3jCqd3vQDRdOZpHggr/8Zz+QKBgQDFd/zPzceTZPvxN1FblojQ9SshGBGPDDlV
duwpF8SBHvxSxUkoZURyF2O+EeICeYBVArqkt0aVprid4vqsjq3TfaS0XqvxIrot
Zsjax7l/IBZ/NClrjh65FQAxSaRUKAzl09Omq+W6wKxnRM/D+os+1nD8HvJYrDB0
6sLbaFILYwKBgQC009QwX3PjoSZf+hXv9xm94wN5JLF27S7hSjW3veFpgKmRWFau
yAFb28mTytNYLNrsoVo/dIMr21eJVO76w69bKW44AqWJZOlCEpEXrLzpfgJdLRqN
dhEqrzYIzdc1bkBb9szC2TIix7PeQ/iPi6x3IokDdfAeZ8Wio0veDIsmcQKBgCn3
XhsruIZyp7iT8Q5t55XBBPF7k/+6sVSgeYAiNPjcBPPySaH0I5+7qM70LfwopkVq
1w+6g526WPH1EyEpEXgo86ABnrkYkEil13nniFykDSPrigH5qj3bYLcY13urJ1KQ
uiJp1eDiocEwtpcH4B/yowBx8z81GPdADTlj6kvRAoGAY3Nyc9est3hQiRvU6z6V
ooQsFyZP5pz4MsSbMrTEuczdsOS7CyJHe1oErI9ILNEsXkAwzcnDWBulUW4FjG4a
2Zkq4OYENeE8Jrj1Y3TO6vJj1VqrrB8yBM5g3JE9jEU36JV8PB/SOLjd6dV3hqSX
EHepUJb5dMDC3lnEoAmrKJ8=
-----END PRIVATE KEY-----
获取X509数字证书
TLS协议还需要标准的X509
格式数字签名证书。
通过openssl
的req
子命令,可以根据生成用于获取数组证书的请求。
openssl req -new -key private.key -out cert.csr
数字证书由数字证书颁发机构CA来签发,下面我们通过openssl
的x509
子命令来模拟这个过程,根据证书签名请求文件(CSR)
来生成数字证书。
openssl x509 -req -in cert.csr -signkey private.key -days 365 -out cert.crt
注意,-signkey
参数用于自签名过程,输出的数字证书是自签名证书,一般用于测试。-days 365
参数表明该数字证书有效期为一年。
同样,使用cat
命令查看生成的PEM格式密钥文件,内容如下:
-----BEGIN CERTIFICATE-----
MIIDbjCCAlYCCQDQepMqz/+IhjANBgkqhkiG9w0BAQsFADB5MQswCQYDVQQGEwJD
TjEQMA4GA1UECAwHQmVpamluZzEQMA4GA1UEBwwHQmVpamluZzENMAsGA1UECgwE
QmlnUTELMAkGA1UECwwCQlExCzAJBgNVBAMMAkJRMR0wGwYJKoZIhvcNAQkBFg55
ZXlhbmJvQG1lLmNvbTAeFw0yMzA5MTAwOTMxMTFaFw0yNDA5MDkwOTMxMTFaMHkx
CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n
MQ0wCwYDVQQKDARCaWdRMQswCQYDVQQLDAJCUTELMAkGA1UEAwwCQlExHTAbBgkq
hkiG9w0BCQEWDnlleWFuYm9AbWUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAq4N1F3Pv1DR2m+2rfLCy1flTVgFSZH4V0H6GivRh7GXYaYqgzeE6
UQLnwlm4LU9mWXtBprEealGtiyREPUAosqTYD7s6rZU1L5w4Lg8Pmh1x3SWNUyjq
pE7Mke5m8GTr/IdkvcDld06sqduto4a21hRE76yS6Qb6xv1JB5jD592mqu1k2NBG
+IPFdJdHuT59/pLDSzUvDbsZ2F3KUHdH7iR++Dhd/abKOpFlLZRxKQROxhoiYzTT
FJy3tNEiXa/URvP3JyOzcvd80QD8GUPUc3p2ZXsbyA9A+v+3sy3cYAutlG34MLwm
X61XK5pG7m4ppC5mwf9BEqp3OYGyvnyMSwIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
AQBTefk/6fRaAXh1sdXM71MK+5VXsWZRdNG9OMwBc3uUHhtRjN9QlPmL6vOzQ3xA
ht6fjRQXaDSsiMLadAFWzraAMszb0Ublwdw7AEEMWVs7VOnrA26vEZoI/206ARZs
9p4A1d+aABbpiIXU4h6lBXytlJzxGUzgJo5naHQeBLV2P+R7rL0avzZrXFFrnzA8
VpAvI/PmI7efSNFLpmUnnuxnPM2nJ2i8hQ2/E9lI5wcMTZKdRzQAGzdETJCGrzam
cYXvDYMlWueVT4Kv/jlznuUp6pRrkahp6nWVHFTOEJ/IcCf05K98N/O+AXO6NDWI
XOZ1EgpxsM12VxWl0Hh4v7kW
-----END CERTIFICATE-----
到此为止,TLS协议需要的两个文件都生成好了。
题外话
获取公钥
数字证书中其实包含的就是公钥,只不过是经过签名认证过的公钥。
如果在某种情况下,我们不需要数字证书,只需要公钥的话,如何来获取公钥呢?
使用如下命令获取公钥:
openssl rsa -in private.key -pubout -out public.key
同样,我们使用cat
命令来查看生成的PEM格式公钥文件,内容如下:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq4N1F3Pv1DR2m+2rfLCy
1flTVgFSZH4V0H6GivRh7GXYaYqgzeE6UQLnwlm4LU9mWXtBprEealGtiyREPUAo
sqTYD7s6rZU1L5w4Lg8Pmh1x3SWNUyjqpE7Mke5m8GTr/IdkvcDld06sqduto4a2
1hRE76yS6Qb6xv1JB5jD592mqu1k2NBG+IPFdJdHuT59/pLDSzUvDbsZ2F3KUHdH
7iR++Dhd/abKOpFlLZRxKQROxhoiYzTTFJy3tNEiXa/URvP3JyOzcvd80QD8GUPU
c3p2ZXsbyA9A+v+3sy3cYAutlG34MLwmX61XK5pG7m4ppC5mwf9BEqp3OYGyvnyM
SwIDAQAB
-----END PUBLIC KEY-----