Используемый пример
Перед тем, как углубиться в тему, стоит описать пример, который используется в статье. Всё решение состоит из двух приложений: веб-сайта и 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/
Добавить комментарий