如何在ServiceStack中使用证书对客户端进行身份验证?

9
我正在探索使用ServiceStack作为WCF的替代方案。 我的要求之一是服务器和客户端必须相互使用证书进行身份验证。 客户端是一个服务,因此我不能使用任何涉及用户输入的身份验证类型。 此外,客户端需要能够在Linux上运行,因此Windows身份验证不可用。
我已经使用netsh.exe将我的服务器证书绑定到服务器端口,使用wireshark验证了客户端获取服务器证书并加密数据。 但是,我无法弄清如何配置服务器以要求客户端证书。
一些人建议使用请求过滤器来验证客户端证书,但这似乎非常低效,因为每个请求都会检查客户端证书。 性能是非常重要的。 创建自定义IAuthProvider似乎很有前途,但所有文档和示例都面向某些时候涉及用户交互而不是证书的身份验证类型。
关于此主题请参考以下链接:https://github.com/ServiceStack/ServiceStack/wiki/Authentication-and-authorization 在自托管的ServiceStack服务中是否可以使用证书来相互认证客户端和服务器?
以下是我测试服务的代码:
public class Host : AppHostHttpListenerBase
{
    public Host()
        : base("Self-hosted thing", typeof(PutValueService).Assembly)
    {
        //TODO - add custom IAuthProvider to validate the client certificate?
        this.RequestFilters.Add(ValidateRequest);

        //add protobuf plugin
        //https://github.com/ServiceStack/ServiceStack/wiki/Protobuf-format
        Plugins.Add(new ProtoBufFormat());

        //register protobuf
        base.ContentTypeFilters.Register(ContentType.ProtoBuf,
                (reqCtx, res, stream) => ProtoBuf.Serializer.NonGeneric.Serialize(stream, res),
                ProtoBuf.Serializer.NonGeneric.Deserialize);
    }

    public override void Configure(Funq.Container container)
    {}

    void ValidateRequest(IHttpRequest request, IHttpResponse response, object dto)
    {
        //TODO - get client certificate?
    }
}

[DataContract]
[Route("/putvalue", "POST")]
//dto
public class PutValueMessage : IReturnVoid
{
    [DataMember(Order=1)]
    public string StreamID { get; set; }

    [DataMember(Order=2)]
    public byte[] Data { get; set; }
}

//service
public class PutValueService : Service
{
    public void Any(PutValueMessage request)
    {
        //Comment out for performance testing

        Console.WriteLine(DateTime.Now);
        Console.WriteLine(request.StreamID);
        Console.WriteLine(Encoding.UTF8.GetString(request.Data));
    }
}
1个回答

11

一些人建议使用请求过滤器来验证客户端证书,但这似乎非常低效,因为每个请求都需要检查客户端证书。性能是非常重要的。

REST是无状态的,所以如果您不愿意在每个请求上检查客户端证书,您需要提供另一种身份验证令牌来展示已经提供了有效的身份验证。

因此,如果在认证客户端证书后,向客户端提供一个可以验证的会话Id cookie,那么您可以避免在后续请求中检查证书。

然而,我却想不出该如何配置服务器才能要求客户端证书。

客户端证书只能在原始的http请求对象上使用,这意味着您必须转换请求对象才能访问这个值。下面的代码用于将请求强制转换为一个ListenerRequest,它被自主托管的应用程序使用。

服务器处理:

请求过滤器将检查:

  • 首先检查一个有效的会话cookie,如果有效,则允许请求而无需进一步处理,因此不需要在后续请求上验证客户端证书。

  • 如果未找到有效的会话,则尝试检查请求是否包含客户端证书。如果存在,则基于某些标准进行匹配,并在接受后为客户端创建一个会话并返回一个cookie。

  • 如果未匹配到客户端证书,则抛出授权异常。

GlobalRequestFilters.Add((req, res, requestDto) => {

    // Check for the session cookie
    const string cookieName = "auth";
    var sessionCookie = req.GetCookieValue(cookieName);
    if(sessionCookie != null)
    {
        // Try authenticate using the session cookie
        var cache = req.GetCacheClient();
        var session = cache.Get<MySession>(sessionCookie);
        if(session != null && session.Expires > DateTime.Now)
        {
            // Session is valid permit the request
            return;
        }
    }

    // Fallback to checking the client certificate
    var originalRequest = req.OriginalRequest as ListenerRequest;
    if(originalRequest != null)
    {
        // Get the certificate from the request
        var certificate = originalRequest.HttpRequest.GetClientCertificate();

        /*
         * Check the certificate is valid
         * (Replace with your own checks here)
         * You can do this by checking a database of known certificate serial numbers or the public key etc.
         * 
         * If you need database access you can resolve it from the container
         * var db = HostContext.TryResolve<IDbConnection>();
         */

        bool isValid = certificate != null && certificate.SerialNumber == "XXXXXXXXXXXXXXXX";

        // Handle valid certificates
        if(isValid)
        {
            // Create a session for the user
            var sessionId = SessionExtensions.CreateRandomBase64Id();
            var expiration = DateTime.Now.AddHours(1);

            var session = new MySession {
                Id = sessionId,
                Name = certificate.SubjectName,
                ClientCertificateSerialNumber = certificate.SerialNumber,
                Expires = expiration
            };

            // Add the session to the cache
            var cache = req.GetCacheClient();
            cache.Add<MySession>(sessionId, session);

            // Set the session cookie
            res.SetCookie(cookieName, sessionId, expiration);

            // Permit the request
            return;
        }
    }

    // No valid session cookie or client certificate
    throw new HttpError(System.Net.HttpStatusCode.Unauthorized, "401", "A valid client certificate or session is required");
});

这里使用了一个名为 MySession 的自定义会话类,您可以根据需要替换为自己的会话对象。

public class MySession
{
    public string Id { get; set; }
    public DateTime Expires { get; set; }
    public string Name { get; set; }
    public string ClientCertificateSerialNumber { get; set; }
}

客户端处理:

客户端需要设置其客户端证书以与请求一起发送。

var client = new JsonServiceClient("https://servername:port/");
client.RequestFilter += (httpReq) => {
    var certificate = ... // Load the client certificate
    httpReq.ClientCertificates.Add( certificate );
};

一旦您与服务器进行了第一次请求,客户端将收到一个会话ID cookie,并且您可以选择性地防止客户端证书被发送,直到会话失效。

希望这能帮到您。


1
太有用了!非常感谢你的详细回复! - r2_118

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