Аутентификация в .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 может быть сохранен в конфигурационных файлах.

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

ссылка на оригинал статьи https://habr.com/ru/post/499588/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *