为 Azure 应用服务配置 TLS 相互身份验证Configure TLS mutual authentication for Azure App Service

通过为 Azure 应用服务应用启用不同类型的身份验证可以限制对网站的访问。You can restrict access to your Azure App Service app by enabling different types of authentication for it. 若要实现此目的,一种方法是通过 TLS/SSL 发送客户端请求时请求客户端证书,然后验证该证书。One way to do it is to request a client certificate when the client request is over TLS/SSL and validate the certificate. 此机制称为 TLS 相互身份验证或客户端证书身份验证。This mechanism is called TLS mutual authentication or client certificate authentication. 本文介绍如何将应用设置为使用客户端证书身份验证。This article shows how to set up your app to use client certificate authentication.

Note

如果通过 HTTP 而不是 HTTPS 访问站点,不会收到任何客户端证书。If you access your site over HTTP and not HTTPS, you will not receive any client certificate. 因此,如果应用程序需要客户端证书,则你不应允许通过 HTTP 对应用程序发出请求。So if your application requires client certificates, you should not allow requests to your application over HTTP.

启用客户端证书Enable client certificates

若要将应用设置为要求提供客户端证书,需要将应用的 clientCertEnabled 设置指定为 trueTo set up your app to require client certificates, you need to set the clientCertEnabled setting for your app to true. 若要设置该设置,请在 Azure PowerShell 中运行以下命令。To set the setting, run the following command in the Azure PowerShell.

az webapp update --set clientCertEnabled=true --name <app_name> --resource-group <group_name>

访问客户端证书Access client certificate

在应用服务中,请求的 SSL 终端是在前端负载均衡器上发生的。In App Service, SSL termination of the request happens at the frontend load balancer. 已启用客户端证书的情况下将请求转发到应用代码时,应用服务会注入包含客户端证书的 X-ARR-ClientCert 请求标头。When forwarding the request to your app code with client certificates enabled, App Service injects an X-ARR-ClientCert request header with the client certificate. 应用服务不会对此客户端证书执行任何操作,而只会将它转发到你的应用。App Service does not do anything with this client certificate other than forwarding it to your app. 应用代码负责验证客户端证书。Your app code is responsible for validating the client certificate.

对于 ASP.NET,可以通过 HttpRequest.ClientCertificate 属性提供客户端证书。For ASP.NET, the client certificate is available through the HttpRequest.ClientCertificate property.

对于其他应用程序堆栈(Node.js、PHP 等),可以通过 X-ARR-ClientCert 请求标头中的 base64 编码值在应用中提供客户端证书。For other application stacks (Node.js, PHP, etc.), the client cert is available in your app through a base64 encoded value in the X-ARR-ClientCert request header.

ASP.NET 示例ASP.NET sample

    using System;
    using System.Collections.Specialized;
    using System.Security.Cryptography.X509Certificates;
    using System.Web;

    namespace ClientCertificateUsageSample
    {
        public partial class cert : System.Web.UI.Page
        {
            public string certHeader = "";
            public string errorString = "";
            private X509Certificate2 certificate = null;
            public string certThumbprint = "";
            public string certSubject = "";
            public string certIssuer = "";
            public string certSignatureAlg = "";
            public string certIssueDate = "";
            public string certExpiryDate = "";
            public bool isValidCert = false;

            //
            // Read the certificate from the header into an X509Certificate2 object
            // Display properties of the certificate on the page
            //
            protected void Page_Load(object sender, EventArgs e)
            {
                NameValueCollection headers = base.Request.Headers;
                certHeader = headers["X-ARR-ClientCert"];
                if (!String.IsNullOrEmpty(certHeader))
                {
                    try
                    {
                        byte[] clientCertBytes = Convert.FromBase64String(certHeader);
                        certificate = new X509Certificate2(clientCertBytes);
                        certSubject = certificate.Subject;
                        certIssuer = certificate.Issuer;
                        certThumbprint = certificate.Thumbprint;
                        certSignatureAlg = certificate.SignatureAlgorithm.FriendlyName;
                        certIssueDate = certificate.NotBefore.ToShortDateString() + " " + certificate.NotBefore.ToShortTimeString();
                        certExpiryDate = certificate.NotAfter.ToShortDateString() + " " + certificate.NotAfter.ToShortTimeString();
                    }
                    catch (Exception ex)
                    {
                        errorString = ex.ToString();
                    }
                    finally 
                    {
                        isValidCert = IsValidClientCertificate();
                        if (!isValidCert) Response.StatusCode = 403;
                        else Response.StatusCode = 200;
                    }
                }
                else
                {
                    certHeader = "";
                }
            }

            //
            // This is a SAMPLE verification routine. Depending on your application logic and security requirements, 
            // you should modify this method
            //
            private bool IsValidClientCertificate()
            {
                // In this example we will only accept the certificate as a valid certificate if all the conditions below are met:
                // 1. The certificate is not expired and is active for the current time on server.
                // 2. The subject name of the certificate has the common name nildevecc
                // 3. The issuer name of the certificate has the common name nildevecc and organization name Microsoft Corp
                // 4. The thumbprint of the certificate is 30757A2E831977D8BD9C8496E4C99AB26CB9622B
                //
                // This example does NOT test that this certificate is chained to a Trusted Root Authority (or revoked) on the server 
                // and it allows for self signed certificates
                //

                if (certificate == null || !String.IsNullOrEmpty(errorString)) return false;

                // 1. Check time validity of certificate
                if (DateTime.Compare(DateTime.Now, certificate.NotBefore) < 0 || DateTime.Compare(DateTime.Now, certificate.NotAfter) > 0) return false;

                // 2. Check subject name of certificate
                bool foundSubject = false;
                string[] certSubjectData = certificate.Subject.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string s in certSubjectData)
                {
                    if (String.Compare(s.Trim(), "CN=nildevecc") == 0)
                    {
                        foundSubject = true;
                        break;
                    }
                }
                if (!foundSubject) return false;

                // 3. Check issuer name of certificate
                bool foundIssuerCN = false, foundIssuerO = false;
                string[] certIssuerData = certificate.Issuer.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string s in certIssuerData)
                {
                    if (String.Compare(s.Trim(), "CN=nildevecc") == 0)
                    {
                        foundIssuerCN = true;
                        if (foundIssuerO) break;
                    }

                    if (String.Compare(s.Trim(), "O=Microsoft Corp") == 0)
                    {
                        foundIssuerO = true;
                        if (foundIssuerCN) break;
                    }
                }

                if (!foundIssuerCN || !foundIssuerO) return false;

                // 4. Check thumprint of certificate
                if (String.Compare(certificate.Thumbprint.Trim().ToUpper(), "30757A2E831977D8BD9C8496E4C99AB26CB9622B") != 0) return false;

                return true;
            }
        }
    }

Node.js 示例Node.js sample

以下 Node.js 示例代码获取 X-ARR-ClientCert 标头,并使用 node-forge 将 base64 编码的 PEM 字符串转换为证书对象,然后验证该对象:The following Node.js sample code gets the X-ARR-ClientCert header and uses node-forge to convert the base64-encoded PEM string into a certificate object and validate it:

import { NextFunction, Request, Response } from 'express';
import { pki, md, asn1 } from 'node-forge';

export class AuthorizationHandler {
    public static authorizeClientCertificate(req: Request, res: Response, next: NextFunction): void {
        try {
            // Get header
            const header = req.get('X-ARR-ClientCert');
            if (!header) throw new Error('UNAUTHORIZED');

            // Convert from PEM to pki.CERT
            const pem = `-----BEGIN CERTIFICATE-----${header}-----END CERTIFICATE-----`;
            const incomingCert: pki.Certificate = pki.certificateFromPem(pem);

            // Validate certificate thumbprint
            const fingerPrint = md.sha1.create().update(asn1.toDer((pki as any).certificateToAsn1(incomingCert)).getBytes()).digest().toHex();
            if (fingerPrint.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            // Validate time validity
            const currentDate = new Date();
            if (currentDate < incomingCert.validity.notBefore || currentDate > incomingCert.validity.notAfter) throw new Error('UNAUTHORIZED');

            // Validate issuer
            if (incomingCert.issuer.hash.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            // Validate subject
            if (incomingCert.subject.hash.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            next();
        } catch (e) {
            if (e instanceof Error && e.message === 'UNAUTHORIZED') {
                res.status(401).send();
            } else {
                next(e);
            }
        }
    }
}