兼虎の辛辛魚

兼虎が天神に移転してきてから、自分の中で空前の兼虎ブームが巻き起こっている。赤坂のときは滅多に行けないから、いつも濃厚つけ麺を食べていたが、天神ならいつでも行ける。今こそ他のメニューを食べるチャンスと思い、また行ってきた。お目当は『辛辛魚』。

スープの濃度が凄くて、とてもラーメンとは思えない。つけ麺のつけ汁並だ。太麺との絡みは抜群。辛さ耐性に自信がなかったので辛さ控えめにしてみたが、それでも結構クル。唇がヒリつく辛さ。基本以上だと完食無理だったかも。クセになりそうな旨さだった。

兼虎

食べログ 兼虎

IdentityServer4 の認証を差し替える方法

はじめに

外部に公開する REST API の認証・認可に OpenID Connect を選択した場合、ASP.NET Core での実装には IdentityServer4 を使うことになると思う。IdentityServer4 は認証のバックエンドに ASP.NET Core Identity をサポートしているので、新規の開発ならそれを使えばいいかもしれない。

github.com

ただ、独自の認証機能を既に実装している場合、今更 ASP.NET Core Identity に移行するのは困難。そんなときは、IdentityServer4 のカスタム認証バックエンドを作ればいいはず。既に IdentityServer4.AspNetIdentity というお手本があるので、それを参考に挑戦してみた。

IdentityServer4 のホストを作成

IdentityServer4.AspNetIdentity のソースコードを見た感じ、認証をカスタマイズするには IProfileService と IResourceOwnerPasswordValidator を実装して、差し替えれば良さそうだった。

using IdentityModel;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Validation;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityServer4Host
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseUrls("http://localhost:8000");
    }

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients())
                .AddProfileService<MyProfileService>()
                .AddResourceOwnerValidator<MyResourceOwnerPasswordValidator>();

            services.AddScoped<MyUserManager>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseIdentityServer();

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello IdentityServer4!");
            });
        }
    }

    static class Config
    {
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("api1", "My API")
            };
        }

        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientId = "ro.client",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowedScopes =
                    {
                        "api1",
                    }
                }
            };
        }
    }

    // 独自のユーザー
    public class MyUser
    {
        public string Id { get; set; } = Guid.NewGuid().ToString();

        public string UserName { get; set; }

        public string Password { get; set; }
    }

    // 独自のユーザーを管理
    public class MyUserManager
    {
        static readonly List<MyUser> users = new List<MyUser>
        {
            new MyUser
            {
                UserName = "kagawa",
                Password = "password",
            }
        };

        // 実際に利用するときは非同期メソッドになるだろうから、
        // このサンプルでは必要ないけど非同期メソッドにしておく。

        public Task<MyUser> FindByNameAsync(string userName)
        {
            var user = users.FirstOrDefault(u => u.UserName == userName);
            return Task.FromResult(user);
        }

        public Task<MyUser> FindByIdAsync(string userId)
        {
            var user = users.FirstOrDefault(u => u.Id == userId);
            return Task.FromResult(user);
        }

        public Task<bool> CheckPasswordAsync(MyUser user, string password)
        {
            return Task.FromResult(user.Password == password);
        }

        public Task<IEnumerable<Claim>> GetClaimsAsync(MyUser user)
        {
            var claims = new List<Claim>();
            claims.Add(new Claim(JwtClaimTypes.PreferredUserName, user.UserName));
            claims.Add(new Claim(JwtClaimTypes.Name, user.UserName));
            return Task.FromResult<IEnumerable<Claim>>(claims);
        }
    }

    // ユーザーを取得するサービス
    class MyProfileService : IProfileService
    {
        readonly MyUserManager userManager;

        public MyProfileService(MyUserManager userManager)
        {
            this.userManager = userManager;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await userManager.FindByIdAsync(sub);
            var claims = await userManager.GetClaimsAsync(user);

            // ユーザーの情報をクレームで登録する
            context.IssuedClaims.AddRange(claims);
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await userManager.FindByIdAsync(sub);
            context.IsActive = user != null;
        }
    }

    // ユーザーのパスワードを検証するバリデーター
    class MyResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
    {
        readonly MyUserManager userManager;

        public MyResourceOwnerPasswordValidator(MyUserManager userManager)
        {
            this.userManager = userManager;
        }

        public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
        {
            var user = await userManager.FindByNameAsync(context.UserName);
            if (user != null)
            {
                var result = await userManager.CheckPasswordAsync(user, context.Password);
                if (result)
                {
                    context.Result = new GrantValidationResult(
                        user.Id,
                        OidcConstants.AuthenticationMethods.Password);
                    return;
                }
            }
            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
        }
    }
}

保護対象の Web API を作成

アクセスしてきたユーザーのクレームを返すだけの Web API を実装した。 IdentityServer4 チュートリアルのまんま。

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;

namespace SampleWebApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseUrls("http://localhost:8001");
    }

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            services.AddAuthentication();
            services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    options.Authority = "http://localhost:8000";
                    options.RequireHttpsMetadata = false;
                    options.ApiName = "api1";
                });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();

            app.UseMvc();
        }
    }

    [Authorize]
    [ApiController]
    [Route("identity")]
    public class IdentityController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            return new JsonResult(
                from c in User.Claims
                select new
                {
                    c.Type,
                    c.Value
                });
        }
    }
}

Web API を呼び出すクライアントを作成

OpenID Connect/OAuth クライアントには IdentityModel を使う。

www.nuget.org

アクセストークンを取得できたら、あとは HttpClient を使って Web API を呼び出す。

using IdentityModel.Client;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace ConsoleClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Title = "ConsoleClient";

            MainAsync().GetAwaiter().GetResult();

            Console.WriteLine("Press Enter Key.");
            Console.ReadLine();
        }

        static async Task MainAsync()
        {
            // メタデータからエンドポイントを取得する
            var disco = await DiscoveryClient.GetAsync("http://localhost:8000");
            if (disco.IsError)
            {
                Console.WriteLine(disco.Error);
                return;
            }

            var tokenClient = new TokenClient(
                address: disco.TokenEndpoint,
                clientId: "ro.client",
                clientSecret: "secret");

            var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(
                userName: "kagawa",
                password: "password",
                scope: "api1");

            if (tokenResponse.IsError)
            {
                Console.WriteLine(tokenResponse.Error);
                return;
            }

            Console.WriteLine(tokenResponse.Json);

            // SampleWebApi を呼び出す
            var client = new HttpClient();
            client.SetBearerToken(tokenResponse.AccessToken);

            var response = await client.GetAsync("http://localhost:8001/identity");
            if (!response.IsSuccessStatusCode)
            {
                Console.WriteLine(response.StatusCode);
            }
            else
            {
                var content = await response.Content.ReadAsStringAsync();
                Console.WriteLine(content);
            }
        }
    }
}

実行結果

アクセストークンの取得と、Web API の呼び出しに成功した。

f:id:griefworker:20181002112110p:plain

おわりに

IdentityServer4.AspNetIdentity を参考に、 IdentityServer4 で使う認証を独自のものに差し替えることができた。今回必要に迫られたので試してみたけど、できれば避けたいことにはかわりない。

ASP.NET Core MVC vs. WCF

はじめに

以前 WCF と gRPC のベンチマークを比較して、結果 gRPC の圧勝だった。

tnakamura.hatenablog.com

ASP.NET Core MVC(Web API) と WCF ではどっちが速いか気になったので試してみた。WCF のサービスは今のところ .NET Framework で動かすしかないので、公平のために ASP.NET Core MVC .NET Framework で動かした。

ASP.NET Core MVC で Web API を作成

gRPC のときと同様に、書籍の一覧を取得する API を用意。

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;

namespace AspNetCoreVsWcf.AspNetCore
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseUrls("http://localhost:8000");
    }

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMvc();
        }
    }

    public class Book
    {
        public string Id { get; set; }

        public string Title { get; set; }

        public string Description { get; set; }

        public string Author { get; set; }

        public int Price { get; set; }

        public string PublishedAt { get; set; }
    }

    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        static readonly Book[] books;

        static BooksController()
        {
            books = Enumerable.Range(0, 50)
                .Select(i => new Book()
                {
                    Id = Guid.NewGuid().ToString(),
                    Title = $"Book{i}",
                    Author = $"Author{i}",
                    Description = $"Description{i}",
                    PublishedAt = DateTime.Today.ToString(),
                    Price = 2000,
                })
                .ToArray();
        }

        [HttpGet]
        public ActionResult<IEnumerable<Book>> Get()
        {
            return books;
        }
    }
}

WCF のサービスを作成

WCF の方でも書籍の一覧を取得するオペレーションを用意した。WCF を使うとしたら TCP か名前付きパイプくらいなので、今回はバインディングTCP を選択。

using System;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace AspNetCoreVsWcf.Wcf
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new ServiceHost(typeof(BookService));
            host.AddServiceEndpoint(
                typeof(IBookService),
                new NetTcpBinding(),
                "net.tcp://localhost:8001/BookService");
            host.Open();

            Console.WriteLine("Enter で終了");
            Console.ReadLine();

            host.Close();
        }
    }

    [DataContract]
    public class Book
    {
        [DataMember]
        public string Id { get; set; }

        [DataMember]
        public string Title { get; set; }

        [DataMember]
        public string Description { get; set; }

        [DataMember]
        public string Author { get; set; }

        [DataMember]
        public int Price { get; set; }

        [DataMember]
        public string PublishedAt { get; set; }
    }

    [ServiceContract]
    public interface IBookService
    {
        [OperationContract]
        Book[] GetBooks();
    }

    public class BookService : IBookService
    {
        static readonly Book[] books;

        static BookService()
        {
            books = Enumerable.Range(0, 50)
                .Select(i => new Book()
                {
                    Id = Guid.NewGuid().ToString(),
                    Title = $"Book{i}",
                    Author = $"Author{i}",
                    Description = $"Description{i}",
                    PublishedAt = DateTime.Today.ToString(),
                    Price = 2000,
                })
                .ToArray();
        }

        public Book[] GetBooks()
        {
            return books;
        }
    }

}

ベンチマークを作成

BenchmarnDotNet を使ってベンチマークを作成。WCFASP.NET Core MVC で公平にするため、ASP.NET Core MVC 側は取得したレスポンスをデシリアライズするところまでを計測対象にした。

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Net.Http;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace AspNetCoreVsWcf.Benchmark
{
    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<AspNetCoreVsWcfBenchmark>();

            Console.ReadLine();
        }
    }

    [DataContract]
    public class Book
    {
        [DataMember]
        public string Id { get; set; }

        [DataMember]
        public string Title { get; set; }

        [DataMember]
        public string Description { get; set; }

        [DataMember]
        public string Author { get; set; }

        [DataMember]
        public int Price { get; set; }

        [DataMember]
        public string PublishedAt { get; set; }
    }

    [ServiceContract]
    public interface IBookService
    {
        [OperationContract]
        Book[] GetBooks();
    }

    public class AspNetCoreVsWcfBenchmark
    {
        IBookService tcpChannel;
        HttpClient httpClient;
        JsonSerializer jsonSerializer;

        [GlobalSetup]
        public void GlobalSetup()
        {
            tcpChannel = ChannelFactory<IBookService>.CreateChannel(
                new NetTcpBinding(),
                new EndpointAddress("net.tcp://localhost:8001/BookService"));
            httpClient = new HttpClient();
            jsonSerializer = JsonSerializer.CreateDefault();
        }

        [GlobalCleanup]
        public void GlobalCleanup()
        {
            ((IClientChannel)tcpChannel).Close();
            httpClient.Dispose();
        }

        [Benchmark]
        public Book[] WcfTcp()
        {
            return tcpChannel.GetBooks();
        }

        [Benchmark]
        public IEnumerable<Book> AspNetCore()
        {
            //var json = httpClient.GetStringAsync("http://localhost:8000/api/books")
            //    .GetAwaiter().GetResult(); ;
            //return JsonConvert.DeserializeObject<IEnumerable<Book>>(json);
            return GetBooksAsync().GetAwaiter().GetResult();
        }

        async Task<IEnumerable<Book>> GetBooksAsync()
        {
            var response = await httpClient.GetAsync("http://localhost:8000/api/books");
            var stream = await response.Content.ReadAsStreamAsync();
            using (var streamReader = new StreamReader(stream))
            using (var jsonReader = new JsonTextReader(streamReader))
            {
                return jsonSerializer.Deserialize<IEnumerable<Book>>(jsonReader);
            }
        }
    }
}

ベンチマーク結果

WCF/TCP の方が断然速かった。

f:id:griefworker:20181015133719p:plain

おわりに

ASP.NET Core MVC は HTTP2 ではないし、WCF の方が速いんだろうなとは予想していた。ただ、ここまで差が開くとは思わなかったな。素の ASP.NET Core ならもっと差は縮まりそうだけど、実際の開発では ASP.NET Core MVC を使うことがほとんどだから、やっても参考にはならないか。Kestrel と HttpClient が HTTP2 に対応するので、正式版が出たらベンチマークを取り直すかもしれない。

シリウス

BUMP OF CHECKENの『シリウス』がiTunesで配信されているのに気付いたのでポチッた。TVアニメ『 重神機パンドーラ』の主題歌に提供と発表されてからだいぶ経ったな。アニメは見てないけど、ロボットアニメの主題歌らしい、激しい戦闘シーンが浮かんでくるような曲だった。

シリウス

シリウス

『業務システム開発モダナイゼーションガイド』を読んだ

本業ではパッケージ開発に携わっていて、その開発プロセスは外部設計→内部設計→実装→テストという流れを、開発する機能ごとに回す小規模なウォーターフォールみたいなもの。SIerと協力会社に分かれてはいないし、本書のメインターゲットからは若干外れているけど、パッケージ開発は業務システム開発と似た部分が多いので、参考になるところがあるのではと思い購入した。

とりわけ内部設計について悩んでいて、どうすればいいかまったく手探りの状態。本書によれば、外部設計と方式設計がしっかりしていれば内部設計書は不要ですらあるとのこと。確かに、外部設計書はかなりしっかり書いているし、実装する機能はどれも決まったレイヤーアーキテクチャなので、内部設計書に書く内容が外部設計書と重複する場面は少なくない。原則、内部設計では、独立したドキュメントファイルではなく、コードと一体化して管理するのが良いということで、それならコードと乖離しにくいし、開発スピードも今より向上しそうだ。

Excelによるチェックリストもどうにかしたいと思っていたので、VSTS でテストを管理するというのは悪くなさそう。テスト管理では特に目立ったソリューションが見当たらないので、既に Visual Studio を使ってることだし、Microsoft の製品にどっぷり浸かってしまおうか。Excel でインポートできるので、移行は難しくないかもしれない。ただ、テスト項目が膨大なため、本当に管理できるかは未知数。

欲を言えば、内部設計についてもっとボリュームがほしかった。実案件を載せるわけにもいかないとは思うが、もう一歩、実践的な内容に踏み込んで欲しかった。今自分がちょうど困っているから、なおさらそう思ってしまうのだろう。自覚はある。ともあれ、本書に掲載されているものは、著者が実際に提案・実践した手法とのことなので、試してみる価値はありそうだ。

キッチングリーン

連日炭水化物が続いて体が肉や野菜を求めていたので、ヘルシーそうな定食を食べに別府にある『キッチングリーン』に行ってみた。夏に一度行こうとしたら運悪く臨時の店休日で無駄足を踏んでしまったので、リベンジだったりする。

ハンバーグ定食がイチオシみたいだったので注文。サラダや小鉢など野菜がたくさん採れて、ハンバーグ定食だけど健康的だった。罪悪感も薄れるってもんだ。

ソースはジャポネで、餡になっているから終始アツアツだった。そしてハンバーグ本体は肉肉しくて、餡状のソースがよく絡む。餡かけのハンバーグというものは初めてだったけど、なるほどこれも一つの形か。

女性だけのスタッフで切り盛りしている店で、メインターゲットは女性客だろう。ただ、店内には小上がりがあって、家族でも来れそう。事実、近所に住んでそうなファミリーが夕食食べてたし。男性1人だけだと、ちょっと肩身が狭かったな。

KOMUGI

『シティ情報ふくおか』っていう情報誌に掲載されてチェックしていた、別府にある『KOMUGI』に行ってみた。別府駅から思ってた以上に遠くて、涼しくなってきてよかったと心底思った。

今回食べたのは『トリニボ』。鶏と煮干しの出汁が効いたスープは味わい深くて絶品だった。麺はもちっとしていて、これまた旨い。かなりレベルの高い醤油ラーメンだと思う。福岡の非豚骨系では、自分の中でトップ10、いや、五指に入るな。

先月発売された『ラーメンWalker九州2018』でも表紙を飾っていて、人気に火がつきそうな予感がする。非豚骨系で良いと思う店がことごとく天神から遠くて、なかなか行く機会を作れないのが悩ましい。

関連ランキング:ラーメン | 茶山駅別府駅