Daxia Blog
Uncategorized | Rust | WebUI | FHIR | Javascript | KB

如何为Axum添加TLS支持

思路

Axum项目的讨论区Does Axum have HTTPS support?#1032中给出了两种方法。其中tls-rustls提供了一种利用Rustls的解决方案。

总体思路就是:主线程处理https请求,然后利用spawn方法启动新进程,监听http请求,然后将所有http请求都跳转到https请求上;

这就涉及到本文着重记录的几个关键技术点:

  1. 在进行请求跳转过程中都需要哪些操作;
  2. 增加TLS安全层是需要证书和密钥的,这些东西如何生成;

请求跳转

我们已经明确了,需要新开一个进程来处理http请求到https的跳转。在这个过程中,很明显的是消息体不需要处理,需要处理的是消息头中url地址变换:

  • schema - http协议变换为https协议;
  • port - 端口号要变;

端口信息

首先,声明一个结构体用于存储HTTPHTTPS的端口号。

#[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地址的变换,下面给出在RustUrl地址类型的各个组成部分命名:

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格式数字签名证书。

通过opensslreq子命令,可以根据生成用于获取数组证书的请求。

openssl req -new -key private.key -out cert.csr

数字证书由数字证书颁发机构CA来签发,下面我们通过opensslx509子命令来模拟这个过程,根据证书签名请求文件(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-----

About Daxia
我是一名独立开发者,国家工信部认证高级系统架构设计师,在健康信息化领域与许多组织合作。具备大型卫生信息化平台产品架构、设计和开发的能力,从事软件研发、服务咨询、解决方案、行业标准编著相关工作。
我对健康信息化非常感兴趣,尤其是与HL7和FHIR标准的健康互操作性。我是HL7中国委员会成员,从事FHIR培训讲师和FHIR测评现场指导。
我还是FHIR Chi的作者,这是一款用于FHIR测评的工具。