はじめに
protobuf-net.Grpc を使ってコードファーストに ASP.NET Core gRPC サービスを実装する場合でも、
ASP.NET Core の JWT Bearer 認証ミドルウェアを使うことができそうだ、
というところまで前回確認できた。
tnakamura.hatenablog.com
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);
});
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
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)
{
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");
try
{
var greetingClient1 = channel.CreateGrpcService<IGreetingService>();
_ = await greetingClient1.HelloAsync();
}
catch (RpcException ex)
{
Console.WriteLine(ex.Message);
}
var tokenClient = channel.CreateGrpcService<ITokenServie>();
var tokenReply = await tokenClient.TokenAsync(new TokenRequest
{
UserName = "Kubo",
});
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();
}
}
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;
if (headers == null)
{
headers = new Metadata();
var options = context.Options.WithHeaders(headers);
context = new ClientInterceptorContext<TRequest, TResponse>(
context.Method,
context.Host,
options);
}
headers.Add("Authorization", $"Bearer {jwt}");
}
}
}
Authorization ヘッダーを毎回自動で送信するためのインターセプターを作成した。
作成したインターセプターを組み込む場合、
今までお世話になってきた CreateGrpcService<T>
が使えないので注意。
代わりに ClientFactory.CreateClient<T>(CallInvoker)
を使えばいいみたい。
実行結果
Authorization ヘッダー無しで認証必須の gRPC サービスを呼び出すと、ちゃんとエラーになった。
インターセプターを組み込んだ場合は呼び出し成功。
おわりに
インターセプターを自作して組み込むことで、
クライアントからサーバーに任意のメタデータを自動で送信できそう。
WCF から gRPC への移行は、
protobuf-net.Grpc を使ってコードファーストに行えそうな目途がついた。