المصادقة في gRpc .NET .NET مع JWT

في هذه المقالة سأتحدث عن ميزات مصادقة API في خدمات gRpc باستخدام JWT. أفترض أنك على دراية برؤوس JWT و HTTP التي تستخدمها في .NET Core WebAPI ، لذلك لن أناقش هذه التفاصيل. عندما حاولت تنفيذ المصادقة في gRpc ، صادفت حقيقة أن معظم الأمثلة مكتوبة باستخدام تطبيقات وحدة التحكم. هذا بعيد جدًا عن الواقع الذي يعيش فيه المطورون في رأيي. على سبيل المثال ، لا أريد إنشاء قناة في كل مرة أريد فيها الاتصال بطريقة الخدمة. لا أريد أيضًا القلق بشأن إرسال رمز مميز ومعلومات المستخدم مع كل طلب. بدلاً من ذلك ، أريد أن أحصل على مستوى البنية التحتية الذي سيهتم بكل هذا بالنسبة لي. إذا كان هذا الموضوع مثيرًا للاهتمام بالنسبة لك ، فسيكون هناك المزيد تحت الخفض. جميع الأمثلة في هذه المقالة صالحة لـ .NET Core 3.1.

مثال مستعمل


قبل الخوض في الموضوع ، يجدر وصف المثال المستخدم في المقالة. يتكون الحل بأكمله من تطبيقين: موقع ويب وخدمة gRpc (فيما يلي API). كلاهما مكتوب في .NET Core 3.1. يمكن للمستخدم تسجيل الدخول ورؤية بعض البيانات إذا كان مصرحًا له بذلك. لا يقوم موقع الويب بتخزين بيانات المستخدم ويعتمد على API في عملية المصادقة. للتواصل مع خدمة gRpc ، يحتاج موقع الويب إلى رمز JWT صالح ، ولكن هذا الرمز المميز لا يرتبط بمصادقة المستخدم في التطبيق. يستخدم تطبيق الويب ملفات تعريف الارتباط على جانبه. لكي تتمكن واجهة برمجة التطبيقات من معرفة أي مستخدم يقوم بالطلب على الخدمة ، يتم إرسال معلومات حول هذا مع الرمز المميز JWT ، ولكن ليس في الرمز المميز نفسه ، ولكن مع رأس HTTP إضافي. يوضح الشكل أدناه مثالاً لمخطط النظام الذي تحدثت عنه للتو:


هنا يجب أن أشير إلى أنه عندما قمت بهذا المثال ، لم يكن لدي هدف تنفيذ طريقة المصادقة الأكثر صحة لواجهة برمجة التطبيقات. إذا كنت ترغب في رؤية بعض أفضل الممارسات ، فراجع مواصفات OpenID Connect . على الرغم من أنه يبدو لي أحيانًا أن الحل الأكثر صحة يمكن أن يكون فائضًا مقارنة بما يمكن حل المشكلة وتوفير الوقت والمال.

تمكين مصادقة JWT في خدمة gRpc


لا يختلف تكوين خدمة gRpc عن التكوين المعتاد الذي تتطلبه .NET Core API. ميزة إضافية هي أنه لا يختلف عن بروتوكولات HTTP و HTTPS. لفترة وجيزة ، تحتاج إلى إضافة خدمات المصادقة والتفويض القياسية ، وكذلك middlewere في ملف Startup.cs . المكان الذي تضيف فيه الوسيطة مهم: تحتاج إلى إضافته بالضبط بين التوجيه والنقاط ( بعض التعليمات البرمجية مفقودة ):

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;
}

وهذا كل ما يحتاجه جانب واجهة برمجة التطبيقات. سجل إعداد العميل أطول قليلاً ومختلف قليلاً اعتمادًا على بروتوكول 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 الضرورية إلى مشروعنا. ستساعدك حزمة Grpc.Tools في إنشاء نماذج أولية عند إنشاء مشروع ، وستساعدك Grpc.Net.ClientFactory في إعداد DI. عند العمل مع gRpc ، إذا كنت بحاجة إلى تنفيذ المعالجة الخاصة بك في مكان ما في منتصف سلسلة استجابة الطلب ، فأنت بحاجة إلى استخدام الفئات الموروثة من Interceptor ، والتي تعد جزءًا من gRpc.Core . إذا كنت بحاجة إلى الوصول إلى HttpContext.User.Identity داخل خدماتك ، يمكنك إضافة واجهة IHttpContextAccessor

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



لخدمتك (وهذا يتطلب تسجيل إضافي في الخدمات). تحتاج إلى إضافة ما يلي إلى ملف 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 ، فقد تحتاج أيضًا إلى تكوين 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


تتم إضافة الرؤوس في فئة المعترض (الخلف من المعترض ). يستخدم 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 في ملفات التكوين.

وكل شيء. سأضيف لاحقًا رابطًا إلى الشفرة مع مثال بسيط عن حالة الاستخدام الموضحة. إذا كان لديك المزيد من الأسئلة ، يرجى الكتابة لي. سأحاول الإجابة.

All Articles