Как аутентифицировать клиента с помощью сертификата в ServiceStack?

Я изучаю использование 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));
    }
}

person r2_118    schedule 15.07.2014    source источник


Ответы (1)


Некоторые люди предлагали использовать фильтры запросов для проверки сертификата клиента, но это кажется очень неэффективным, поскольку каждый запрос будет проверять сертификат клиента. Производительность является очень высоким приоритетом.

REST не имеет состояния, поэтому, если вы не хотите проверять сертификат клиента при каждом запросе, вам нужно будет предоставить альтернативный токен аутентификации, чтобы показать, что действительное удостоверение уже было предоставлено.

Таким образом, вы можете избежать проверки сертификата при последующих запросах, если после аутентификации сертификата клиента вы предоставите клиенту файл 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 );
};

После того, как вы сделали первый запрос к серверу, ваш клиент получит файл cookie с идентификатором сеанса, и вы можете при желании удалить сертификат клиента от отправки, пока сеанс не станет недействительным.

Надеюсь, это поможет.

person Scott    schedule 15.07.2014
comment
Это очень помогает! Спасибо за очень обстоятельный ответ! - person r2_118; 15.07.2014