はじめに
protobuf-net.Grpc を使ってコードファーストに ASP.NET Core gRPC サービスを実装する場合でも、 ASP.NET Core の JWT Bearer 認証ミドルウェアを使うことができそうだ、 というところまで前回確認できた。
Authorization ヘッダーを詰めた Metadata を毎回送信するのは手間なので、 自動化したいところ。 gRPC にはインターセプターという仕組みが用意されているので、 Authorization ヘッダーを送信するインターセプターを作成すれば良さそうだ。
問題は、protobuf-net.Grpc を使ってコードファーストに実装する場合でも、 インターセプターが使えるのか。 試してみた。
Shared プロジェクトを作成
前回と同じ内容になるが、 クライアントとサーバーの両方で使う、 サービスコントラクトとデータコントラクトを定義する。 プロジェクトの種類は .NET Core クラスライブラリ。 ServiceContractAttribute を使うために System.ServiceModel.Primitives、 CallContext を使うために protobuf-net.Grpc のパッケージを追加しておく。
using System.Runtime.Serialization; using System.ServiceModel; using System.Threading.Tasks; using ProtoBuf.Grpc; namespace CodeFirstGrpc.Shared { [ServiceContract] public interface ITokenServie { ValueTask<TokenReply> TokenAsync( TokenRequest request); } [ServiceContract] public interface IGreetingService { ValueTask<HelloReply> HelloAsync(CallContext context = default); } [DataContract] public class TokenRequest { [DataMember(Order = 1)] public string UserName { get; set; } } [DataContract] public class TokenReply { [DataMember(Order = 1)] public string Token { get; set; } } [DataContract] public class HelloReply { [DataMember(Order = 1)] public string Message { get; set; } } }
Server プロジェクトを作成
これまた前回と同じになるが、 gRPC サービスプロジェクトを作成し、 コードファーストで gRPC サービスを実装する。
コードファーストでやるために、 protobuf-net.Grpc.AspNetCore パッケージを追加しておく。 Microsoft.AspNetCore.Authentication.JwtBearer パッケージも追加し、 JWT Bearer 認証を組み込む。
using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Threading.Tasks; using CodeFirstGrpc.Shared; using Grpc.Core; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; using ProtoBuf.Grpc; using ProtoBuf.Grpc.Server; namespace CodeFirstGrpc.Server { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddCodeFirstGrpc(); services.AddAuthorization(options => { // デフォルトの認証スキーマのポリシーを簡単なものに上書き options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy => { policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); policy.RequireClaim(ClaimTypes.Name); }); }); // JWT Bearer 認証を組み込む services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { // サンプルを簡略化するため検証機能を OFF にする // 本番でこんなことをしてはダメ options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, ValidateIssuer = false, ValidateActor = false, ValidateLifetime = true, IssuerSigningKey = SecurityKey }; }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); // パイプラインに認証と認可を組み込む app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<TokenService>(); endpoints.MapGrpcService<GreetingService>(); }); } internal static readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray()); } public class TokenService : ITokenServie { public ValueTask<TokenReply> TokenAsync(TokenRequest request) { var token = GenerateJwtToken(request.UserName); return new ValueTask<TokenReply>(new TokenReply { Token = token, }); } string GenerateJwtToken(string name) { if (string.IsNullOrEmpty(name)) { throw new InvalidOperationException("Name is not specified."); } var claims = new[] { new Claim(ClaimTypes.Name, name), }; var credentials = new SigningCredentials( Startup.SecurityKey, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( "ExampleServer", "ExampleClients", claims, expires: DateTime.Now.AddSeconds(60), signingCredentials: credentials); return JwtTokenHandler.WriteToken(token); } readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); } // 認証必須のサービス [Authorize] public class GreetingService : IGreetingService { public ValueTask<HelloReply> HelloAsync(CallContext context = default) { // 認証に成功していれば HttpContext からユーザーの情報を取得できるはず var httpContext = context.ServerCallContext.GetHttpContext(); var userName = httpContext.User.Identity.Name; return new ValueTask<HelloReply>(new HelloReply { Message = $"Hello, {userName}.", }); } } }
Client プロジェクトを作成
長い前置きが終わって、ここからが本番。
NET Core コンソールアプリのプロジェクトを作成し、 gRPC サービスを呼び出すためのクライアントを実装する。 コードファーストで実装した gRPC サービスを呼び出すために、 Grpc.Net.Client と protobuf-net.Grpc のパッケージを参照しておく。
using System; using System.Threading.Tasks; using CodeFirstGrpc.Shared; using Grpc.Core; using Grpc.Core.Interceptors; using Grpc.Net.Client; using ProtoBuf.Grpc.Client; using ProtoBuf.Grpc.Configuration; namespace CodeFirstGrpc.Client { class Program { static async Task Main(string[] args) { var channel = GrpcChannel.ForAddress("https://localhost:5001"); // JWT Bearer トークン無しで保護されたサービスを呼び出す try { var greetingClient1 = channel.CreateGrpcService<IGreetingService>(); _ = await greetingClient1.HelloAsync(); } catch (RpcException ex) { Console.WriteLine(ex.Message); } // JWT Bearer トークン取得 var tokenClient = channel.CreateGrpcService<ITokenServie>(); var tokenReply = await tokenClient.TokenAsync(new TokenRequest { UserName = "Kubo", }); // Metadata に常に Authorization を追加するインターセプターを利用する // Intercept の戻り値は CallInvoker なので、CreateGrpcService<T> は使えない。 // 代わりに ClientFactory の CreateClient<T>(CallInvoker) を使う。 var callInvoker = channel.Intercept(new AuthorizationInterceptor(tokenReply.Token)); var greetingClient2 = ClientFactory.Default.CreateClient<IGreetingService>(callInvoker); var helloRepoly = await greetingClient2.HelloAsync(); Console.WriteLine($"Message: {helloRepoly.Message}"); Console.ReadLine(); } } // Metadata に常に Authorization を追加するインターセプター class AuthorizationInterceptor : Interceptor { readonly string jwt; public AuthorizationInterceptor(string jwt) { this.jwt = jwt; } public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation) { AddAuthorizationMetadata(ref context); return continuation(request, context); } public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>( ClientInterceptorContext<TRequest, TResponse> context, AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation) { AddAuthorizationMetadata(ref context); return continuation(context); } public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>( TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation) { AddAuthorizationMetadata(ref context); return continuation(request, context); } public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>( ClientInterceptorContext<TRequest, TResponse> context, AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation) { AddAuthorizationMetadata(ref context); return continuation(context); } void AddAuthorizationMetadata<TRequest, TResponse>( ref ClientInterceptorContext<TRequest, TResponse> context) where TRequest : class where TResponse : class { var headers = context.Options.Headers; // ヘッダーのコレクションが無ければ作成する。 // 作成したヘッダーのコレクションを後続のインターセプターに渡すために、 // context も作り直す必要がある。 if (headers == null) { headers = new Metadata(); var options = context.Options.WithHeaders(headers); context = new ClientInterceptorContext<TRequest, TResponse>( context.Method, context.Host, options); } // Authorization ヘッダーを追加 headers.Add("Authorization", $"Bearer {jwt}"); } } }
Authorization ヘッダーを毎回自動で送信するためのインターセプターを作成した。
作成したインターセプターを組み込む場合、
今までお世話になってきた CreateGrpcService<T>
が使えないので注意。
代わりに ClientFactory.CreateClient<T>(CallInvoker)
を使えばいいみたい。
実行結果
Authorization ヘッダー無しで認証必須の gRPC サービスを呼び出すと、ちゃんとエラーになった。 インターセプターを組み込んだ場合は呼び出し成功。
おわりに
インターセプターを自作して組み込むことで、 クライアントからサーバーに任意のメタデータを自動で送信できそう。
WCF から gRPC への移行は、 protobuf-net.Grpc を使ってコードファーストに行えそうな目途がついた。