在某些 WebAPI 控制器中禁用 SSL 客户端证书?

24

编辑以供未来读者参考:不幸的是,获得赏金的答案无效;我现在无能为力。但请阅读下面的我的回答(通过测试)- 经确认可以使用最少的代码更改。

我们有一个完全基于ASP.NET WebAPI 2.2(没有MVC,前端是Angular)的Azure云服务(WebRole)。我们的一些控制器/REST端点通过SSL(客户端证书认证/双向认证)与第三方云服务通信,其余的控制器/端点与HTML5 / AngularJS前端也通过SSL进行通信(但是采用更传统的服务器认证SSL)。我们没有任何非SSL端点。我们已通过云服务启动任务启用了客户端SSL,如下所示:

IF NOT DEFINED APPCMD SET APPCMD=%SystemRoot%\system32\inetsrv\AppCmd.exe
%APPCMD% unlock config /section:system.webServer/security/access

问题:该设置是整个网站范围内有效,因此即使用户打开第一页(例如https://domain.com,返回的是angularJS的index.html),他们的浏览器也会要求提供客户端SSL证书。(下面是图片)

有没有办法:

  1. 将客户端SSL证书请求限制为仅适用于与第三方云服务通信的WebAPI控制器?

或者

  1. 跳过为我们的前端驱动WebAPI控制器进行客户端SSL身份验证?

我们服务器的web.config非常复杂,但相关代码片段如下:

<system.webServer>
  <security>
    <access sslFlags="SslNegotiateCert" />
  </security>
</system.webServer>

客户端尝试进行客户端 SSL 认证,截图显示其请求了一个普通的 WebAPI 端点(在任何浏览器中都会发生,包括 Chrome、Firefox 或 IE)。 enter image description here


你能展示一下你的结果serverWebConfig和application WebConfig吗? - teo van kot
服务器的web.config文件在上面,由于客户端应用程序是浏览器,因此应用程序的web.config文件不存在。 - DeepSpace101
我认为这个问题更适合在Server Fault上讨论。 - 500 - Internal Server Error
3个回答

17

很遗憾,获得奖励的cleftheris的答案并不起作用。它试图在HTTP服务器管道/处理过程中太晚地工作以获取客户端证书,但是这篇文章给了我一些思路。

解决方案基于web.config,调用“目录”的特殊处理(也适用于虚拟文件夹或WebAPI路由)。

这里是期望的逻辑:

https://www.server.com/acmeapi/** => SSL with Client Certs

https://www.server.com/** => SSL

以下是相应的配置:

<configuration>
  ...
  <system.webServer>
    <!-- This is for the rest of the site -->
    <security>
      <access sslFlags="Ssl" />
    </security>
  </system.webServer>

  <!--This is for the 3rd party API endpoint-->
  <location path="acmeapi">
    <system.webServer>
      <security>
        <access sslFlags="SslNegotiateCert"/>
      </security>
    </system.webServer>
  </location>
...
</configuration>

额外加分

以上代码将相应地设置SSL握手。现在,您仍然需要在代码中检查客户端SSL证书是否符合您的预期。操作步骤如下:

控制器代码:

[RoutePrefix("acmeapi")]
[SslClientCertActionFilter] // <== key part!
public class AcmeProviderController : ApiController
{
    [HttpGet]
    [Route("{userId}")]
    public async Task<OutputDto> GetInfo(Guid userId)
    {
        // do work ...
    }
}

以下是从上面实际执行SSL客户端验证的属性。可以用来装饰整个控制器或仅特定方法。

public class SslClientCertActionFilterAttribute : ActionFilterAttribute
{
    public List<string> AllowedThumbprints = new List<string>()
    {
        // Replace with the thumbprints the 3rd party
        // server will be presenting. You can make checks
        // more elaborate but always have thumbprint checking ...
        "0011223344556677889900112233445566778899",
        "1122334455667788990011223344556677889900" 
    };

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var request = actionContext.Request;

        if (!AuthorizeRequest(request))
        {
            throw new HttpResponseException(HttpStatusCode.Forbidden);
        }
    }

    private bool AuthorizeRequest(HttpRequestMessage request)
    {
        if (request==null)
            throw new ArgumentNullException("request");

        var clientCertificate = request.GetClientCertificate();

        if (clientCertificate == null || AllowedThumbprints == null || AllowedThumbprints.Count < 1)
        {
            return false;
        }

        foreach (var thumbprint in AllowedThumbprints)
        {
            if (clientCertificate.Thumbprint != null && clientCertificate.Thumbprint.Equals(thumbprint, StringComparison.InvariantCultureIgnoreCase))
            {
                return true;
            }
        }
        return false;
    }
}

这个方法非常有效。我没有找到其他任何可行的解决方案。谢谢。 - Saravanan

12
您可以在 web.config 级别简单地允许纯 http 流量,并在 Web Api 管道中编写自定义委托处理程序来处理它。您可以在这里找到一个客户端证书委派处理程序,以及这里。然后您可以使此处理程序“按路由”激活,就像在这个例子中找到的那样:
这是您的路由配置示例。
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "Route1",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Routes.MapHttpRoute(
            name: "Route2",
            routeTemplate: "api2/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional },
            constraints: null,
            handler: new CustomCertificateMessageHandler()  // per-route message handler
        );

        config.MessageHandlers.Add(new SomeOtherMessageHandler());  // global message handler
    }
}

请注意,如果您需要“按路由”委派处理程序,则不应将它们放在全局消息处理程序列表中。


请注意,这是我答案中选项#3的实现,因此它存在建立SSL会话两次的性能问题。 - daspek
@DeepSpace101看看这个codeplex工作项。列出的代码应该可以正常工作。 - cleftheris
给未来的读者:请阅读我的答案。它已经被证实可以工作,并且需要最少的代码更改。 - DeepSpace101

3
很遗憾,这不能在控制器级别进行配置。服务器根据HTTP请求的内容(通常是请求路径)来决定使用哪个控制器。SSL通过加密方式保护HTTP消息的内容,而请求路径是加密消息的一部分。在发送任何HTTP消息之前,需要设置SSL通道,这就是为什么SSL通道的配置(例如,服务器是否尝试协商客户端证书)不能依赖于任何HTTP消息的内容。
所以,以下是您的选择:
  1. 配置第二个Web角色,不进行客户端证书认证。由于这实际上是一个单独的服务,因此您需要另一个域名。因此,您需要将 https://domain.com 指向不进行客户端证书认证的Web角色,https://foo.domain.com 指向需要客户端证书认证的Web角色。

  2. 使用相同的Web角色,但设置第二个用于IIS监听的端口,并将其配置为不进行客户端证书认证。使用非标准端口很麻烦,因为您的一些客户端将不得不使用 https://domain.com:444(或除443以外的其他端口)。

  3. 完全禁用客户端证书认证。这可能无法正常工作,具体取决于服务如何访问客户端证书,但通常情况下,当您在 System.Web.HttpRequest 对象(或等效对象)上访问 ClientCertificate 属性时,它会根据需要请求并验证证书。这意味着它会透明地终止现有的SSL会话并建立新会话,此时会向客户端发出证书请求。这非常低效,因为首先建立SSL连接需要多次往返,而两次建立连接则更加痛苦。但根据您可用的选项、需要客户端证书的请求的性能要求以及是否会在保持连接存活期间获得大量连接重用,此选项可能是有意义的。

希望这有所帮助。

如果不使用控制器,我能否告诉IIS本身在哪些路径上强制执行(或跳过)客户端SSL?根据您的描述,这是正确的决策层。 - DeepSpace101
很不幸,你做不到。你只能使用IP和端口。当一个HTTP请求进来时,在服务器发现路径之前,SSL协商必须先进行。第三种选择是最接近的选项,可以根据路径/控制器做出不同的决策。但它的效率低下。 - daspek
@DeepSpace101 你可以尝试创建一个空的物理文件夹,其中包含自己的web.config文件,然后将安全控制器路由放在该路径下面。然后,你可以使用Appcmd Unlock Config命令仅为该位置解锁访问元素。 - cleftheris

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接