Аутентификация в .NET Core gRpc с помощью JWT

    В этой статье я расскажу об особенностях аутентификации API в gRpc сервисах с помощью JWT. Я предполагаю, что вы знакомы с JWT и заголовками HTTP, с их использованием в .NET Core WebAPI, поэтому не буду обсуждать эти детали. Когда я пытался реализовать аутентификацию в gRpc, я столкнулся с тем, что большинство примеров написаны с использованием консольных приложений. Это слишком далеко от реальности, в которой, на мой взгляд, живут разработчики. Например, я не хочу создавать канал каждый раз, когда я хочу вызвать метод сервиса. Еще я не хочу заботиться об отправке токена и пользовательской информации с каждым запросом. Вместо этого я хочу иметь инфраструктурный уровень, который будет заботиться обо всём этом за меня. Если эта тема вам интересна, то под катом будет больше. Все примеры в статье справедливы для .NET Core 3.1.

    Используемый пример


    Перед тем, как углубиться в тему, стоит описать пример, который используется в статье. Всё решение состоит из двух приложений: веб-сайта и gRpc сервиса (далее API). Оба написаны на .NET Core 3.1. Пользователь может залогиниться и посмотреть некоторые данные, если он авторизован для этого. Веб-сайт не сохраняет данные пользователя и в процессе аутентификации полагается на API. Чтобы общаться с gRpc сервисом, веб-сайту необходимо иметь валидный токен JWT, но этот токен ни как не относится к аутентификации пользователя в приложении. Веб-приложение используетс куки на своей стороне. Чтобы API знал, какой именно пользователь делает запрос к сервису, информация об этом отправляется вместе с токеном JWT, но не в самом токене, а дополнительным HTTP заголовком. На рисунке ниже показана примерная схепа системы, о которой я только что рассказал:


    Здесь я должен отметить, что когда я делал этот пример, у меня не было цели реализовать наиболее правильный способ аутентификации для API. Если хотите увидеть какие-то best practices, то посмотрите спецификацию OpenID Connect. Хотя, иногда мне кажется, что самое правильное решение может ыть избыточно по сравнению с тем, что может решить проблему и сэкономить время и деньги.

    Включение аутенификации с помощью JWT в gRpc сервисе


    Конфигурация службы gRpc не отличается от обычной конфигурации, которая требуется .NET Core API. Дополнительным плюсом является то, что она не отличается для HTTP и HTTPS протоколов. Коротко, вам нужно добавить стандартные службы аутентификации и авторизации, а также middlewere в файле Startup.cs. Место куда вы добавляете middleware важно: его нужно добавить точно между маршрутизацией и едпоинтами (некоторый код пропущен):

    public void Configure(...) {
        app.UseRouting();
        
        app.UseAuthentication();
        app.UseAuthorization();
        
        app.UseEndpoints(...
    }
    

    А вот место где регистрируются службы не так важно, просто добавьте в метод ConfigureServices(). Но тут необходимо настроить проверку токена JWT. Это можно определить прямо тут, но я рекомендую вытащить это в отдельный класс. Таким образом, код может выглядеть следующим образом:

    public void ConfigureServices(...) {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(o => {
                var validator = new JwtTokenValidator(...);
                o.SecurityTokenValidators.Add(validator);
            });
        services.AddAuthorization();
    }
    

    Класс JwtTokenValidator — это тот, где вы будете определять логику проверки. Надо создать класс TokenValidationParameters с правильными настройками и он сделает всю остальную работу по проверке JWT. Как бонус, вы можете добавить дополнительный уровень безопасности здесь. Он может понадобиться, потому что JWT — это широко известный формат. Если у вас есть JWT, вы можете перейти на jwt.io и посмотреть некоторую информацию. Я предпочитаю добавить дополнительное шифрование в JWT, что усложняет расшифровку. Вот как может выглядеть валидатор:

    public class JwtTokenValidator : ISecurityTokenValidator
    {
        public bool CanReadToken(string securityToken) => true;
        
        public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
        {
            var handler = new JwtSecurityTokenHandler();
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = "your string",
                ValidAudience = "your string",
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your secrete code"))
            };
            
            var claimsPrincipal = handler.ValidateToken(token, tokenValidationParameters, out validatedToken);
            return claimsPrincipal;
        }
        
        public bool CanValidateToken { get; } = true;
        public int MaximumTokenSizeInBytes { get; set; } = int.MaxValue;
    }
    

    И это всё, что нужно на стороне API. История настройки клиента немного длиннее и немного отличается в зависимости выбранного протокола HTTP или HTTPS.

    Отправка HTTP заголовков с каждым запросом к gRpc сервису


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

    var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var client = new Greeter.GreeterClient(channel);
    var response = await client.SayHelloAsync(
        new HelloRequest { Name = "World" });
    Console.WriteLine(response.Message);
    

    Чтобы использовать это в реальном проекте, нам нужно еще иметь централизованную конфигурацию и DI, которые практически не рассматриваютсяю. Вот что вам нужно сделать. Для начала нам нужно добавить необходимые пакеты NuGet в наш проект.

    dotnet add package Grpc.Net.Client
    dotnet add package Google.Protobuf
    dotnet add package Grpc.Tools
    dotnet add package Grpc.Net.ClientFactory

    Пакет Grpc.Tools поможет создавать прототипы при сборке проекта, а Grpc.Net.ClientFactory поможет настроить DI.

    Работая с gRpc, если вам нужно внедрить свою обработку где-то по середине цепочки запрос-ответ, вам нужно использовать классы унаследованные от Interceptor, который является частью gRpc.Core. Если вам нужно получить доступ к HttpContext.User.Identity внутри ваших сервисов, вы можете добавить интерфейс IHttpContextAccessor в ваш сервис (для этого требуется дополнительная регистрация в сервисах). Вам необходимо добавить следующее в ваш файл Startup.cs.

    services.AddTransient<AuthHeadersInterceptor>();
    services.AddHttpContextAccessor();
    
    var httpClientBuilder = services.AddGrpcClient<MygRpcService.MygRpcServiceClient>(o => { o.Address = new Uri("grpc-endpoint-url"); });
    httpClientBuilder.AddInterceptor<AuthHeadersInterceptor>();              
    httpClientBuilder.ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);
    

    Класс AuthHeadersInterceptor — это наш собственный класс, производный от класса Interceptor. Он использует IHttpContextAccessor и регистрация .AddHttpContextAccessor () позволяет сделать это.

    Особенности конфигурации для HTTP


    Вы можете заметить следующую конфигурацию:

    httpClientBuilder.ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);
    

    Она необходима для работы через HTTP, но этого недостаточно. Вам также необходимо исключить эту строку из метода Configure ().

    app.UseHttpsRedirection();
    

    И ещё вам нужно потанцевать установить специальный сеттинг перед созданием любого канала gRpc. Это может быть выполнено только один раз во время запуска приложения. Поэтому я добавил его почти в ту же позицию, что и удаленная строка, упомянутая выше. Это должно быть вызвано только для HTTP.

    AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
    

    Особенности конфигурации для HTTPS


    Есть некоторые сложности работы с SSL в Windows и Linux. Может случиться так, что вы разрабатываете на компьютере Windows и развертываете в Docker/Kubernetes с использованием образов на основе Linux. В таком случае конфигурация не является такой простой, как описывается во многих постах. Я опишу эту конфигурацию в другой статье, а тут я затрону только код.

    Нам нужно изменить конфигурацию канала gRpc, чтобы использовать учетные данные SSL. Если вы деплоите в Docker и делаете Linux-based имеджи, вам также может понадобиться настроить HttpClient для разрешения невалидных сертификатов. HttpClient создается для каждого канала.

    httpClientBuilder.ConfigureChannel(o =>
    {
        // add SSL credentials
        o.Credentials = new SslCredentials();
        // allow invalid/untrusted certificates
        var httpClientHandler = new HttpClientHandler
        {
            ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
        };
        var httpClient = new HttpClient(httpClientHandler);
        o.HttpClient = httpClient;
    });
    

    Добавление HTTP заголовков


    Заголовки добавляются в классе перехватчика (наследнике от Interceptor). gRpc использует концепцию метаданных, которые отправляются вместе с запросами в качестве заголовков. Класс перехватчика должен добавить метаданные для контекста вызова.

    public class AuthHeadersInterceptor : Interceptor
    {
        public AuthHeadersInterceptor(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }
        
        public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
        {
            var metadata = new Metadata
            {
                {HttpHeaderNames.Authorization, $"Bearer <JWT_TOKEN>"}
            };
            var userIdentity = _httpContextAccessor.HttpContext.User.Identity;
            if (userIdentity.IsAuthenticated)
            {
                metadata.Add(HttpHeaderNames.User, userIdentity.Name);
            }
            var callOption = context.Options.WithHeaders(metadata);
            context = new ClientInterceptorContext<TRequest, TResponse>(context.Method, context.Host, callOption);
            
            return base.AsyncUnaryCall(request, context, continuation);
        }
    }
    

    Для сценария, когда вы просто вызываете сервис gRpc, вам нужно переопределить только метод AsyncUnaryCall. Конечно, токен JWT может быть сохранен в конфигурационных файлах.

    И это всё. Позже я добавлю ссылку на код с простым примером описанного варианта использования. Если у вас есть дополнительные вопросы, пожалуйста, напишите мне. Я постараюсь ответить.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое