呉朝明

夕食を食べに、舞鶴1丁目の『呉朝明』まで行ってみた。 お目当ては1日40杯限定のラーメン。

ラーメンだけだと食べ足りなさそうなので、焼きワンタンも注文してみた。 ワンタンの皮は薄いからか、 焼くと結構芳ばしくなる。 具から出るスープがしっかり閉じ込められていて美味い。 お酒に合いそうだ。 飲めないけど。

ラーメンはこれぞ屋台のラーメンという感じ。 豚骨ラーメンの中ではあっさりした方だと思う。 醤油タレがしっかり効いていて、 どこか懐かしい味。 地元の屋台のラーメンを思い出した。

余談だけど、ビールサーバー裏のゲージに入っている猫が気になった。 1〜2匹じゃない。 沢山いた。 あれは何だったんだろう…。

関連ランキング:居酒屋 | 赤坂駅天神駅西鉄福岡駅(天神)

Water site. OTTO

アクロス福岡の向かいにある『Water site. OTTO』でランチ。最初、ナポリタンを食べようかと思っていたけど、カレーが気になってしまったので、W カレーセットに変更した。チキンカレーとグリーンカレーを一度に楽しめる欲張りな1皿。

グリーンカレーは甘さと辛さのバランスが絶妙で、癖になりそうな味だった。 チキンカレーもこれまた美味で、どちらも甲乙つけ難い。ほんと、Wカレーにしておいてよかった。一皿で2種類楽しめるというのが非常に良い。平日のランチでカレーが食べたくなったとき、カツカレー以外ならこの店が今のところ有力だな。

関連ランキング:カフェ・喫茶(その他) | 中洲川端駅天神駅天神南駅

『WEB+DB PRESS Vol.100』を読んだ

WEB+DB PRESS Vol.100』を読んだので、感想をメモしておく。

特集1 : 作って学ぶ人工知能

word2vec は名前だけは知っていたけど、調べることができずにいたので、本特集はタイムリーだった。深層学習で学習したモデルを使って、「フランス-パリ+日本」で「東京」と表示させるサンプルは面白い。word2vec でいろいろ遊んでみたいが、モデルの学習にとてつもない時間がかかるのが壁だな。自分の財力ではなかなかキビシイ。

特集2 : 対応必須!完全HTTPS

すでに運用している Web サービスを完全 HTTPS 化するためのノウハウが載っていた。すべて自分たちでコントロールできるならいいけど、コントロールが及ばない外部リソースに依存してるときはツライ。そんな場合でもなんとかする方法も紹介してあって至れり尽くせり。幸い自分はこれから Web サービスをリリースする立場なので、最初から完全 HTTPS 化を進めることにしよう。

特集3 : メルカリ開発ノウハウ大公開

SRE チームを作ったり、QA-SET チームを作ったりと、メルカリの施策はどれも日本の IT 企業の中では先進的に思えて、見習いたいことが多い。というか、ここまでしっかりやっているのか。開発スピードと品質どちらも妥協せずに追求し続けるという会社の姿勢が見てとれる。

100号記念選書 : TOPエンジニアを支える1冊

定番の技術書はだいぶ読んできたつもりだったが、まだ読んでいない本が結構あった。本特集で存在を初めて知った本もいくつか。 特集で紹介された本の中で興味を持ったものは Amazon の欲しいものリストに登録しておこう。特に SICP はいい加減そろそろ読まねば。

100号記念エッセイ : あのときの自分へ

アルゴリズムやデータ構造といった、流行に左右されない基礎的な力を磨きつつも、新しく出てきた技術に対して投資するべきかどうか判断する目も養っていかなければいけないなと思った。理想を言えば、新しい技術を生み出す側になりたいものだが。「未来を予測する最善の方法は、未来を開発することだ」とアラン・ケイが言ったように。

WEB+DB PRESS Vol.100

WEB+DB PRESS Vol.100

いきなりステーキ

『いきなりステーキ』が天神西通りにオープンしていたので行ってみた。 てっきり立ち食いだけだと思っていたけど、少しだけ椅子の席もあったのは意外。

サラダなどのサイドメニューは席で注文できるが、ステーキは席の番号札を持って調理カウンターで注文しなければいけなかった。面倒なシステムだな。 なんとか『ワイルドステーキ 300g』を注文。

表面だけ焼かれた赤身肉を、 鉄板の余熱で自分好みに焼くスタイルで、これまた意外。 赤身肉は分厚いがなかなか柔らかかった。 脂身が苦手なので、端のでっかい脂身は余計。 コスパは微妙。 システムが分かりにくくて、言うほどいきなりではなかったな。

いきなりステーキ 福岡天神店

食べログ いきなりステーキ 福岡天神店

ASP.NET Core 2.0 で JWT を使った認証を実装する

先日、ASP.NET Core MVC で JWT を使った認証を実装する記事を書いた。

tnakamura.hatenablog.com

しかし、ASP.NET Core 2.0 では認証まわりがガラッと変わってしまったので、 上の記事にある方法は使えなくなってしまった。 /(^o^)\ナンテコッタイ。

なので、ASP.NET Core 2.0 用のサンプルを書いてみた。

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
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 Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;

namespace JwtSample
{
    // アプリケーションの構成
    // 本来は application.json か環境変数に持たせるべきだが、
    // 今回は簡略化のために static クラスにしておく。
    public static class AppConfiguration
    {
        public static string SiteUrl { get; } = "http://localhost:5000";

        // JWT の署名で使う秘密鍵
        public static string SecretKey { get; } = Guid.NewGuid().ToString();
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseUrls(AppConfiguration.SiteUrl)
                .Build();
    }

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

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            // 認証の設定は ConfigureServices の中で行うようになった
            services.AddAuthentication(options =>
            {
                // JwtBearer 認証しか使わないので、
                // JwtBearer をデフォルトにする
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                // 着信したトークンの意図された受信者、またはトークンがアクセスを許可
                // するリソースを表す。
                // このパラメータで指定された値がトークンの aud パラメータと一致しない
                // 場合、トークンは別のリソースにアクセスするために使用されたため、
                // 拒否される。
                options.Audience = AppConfiguration.SiteUrl;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    ValidateIssuer = true,
                    ValidIssuer = AppConfiguration.SiteUrl,
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(AppConfiguration.SecretKey))
                };
            });

        }

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

            // 認証を使う場合は UseJwtBearerAuthentication ではなく
            // UseAuthentication を呼び出す
            app.UseAuthentication();

            app.UseMvc();
        }
    }

    // ユーザー
    public class User
    {
        public string Id { get; set; }

        public string UserName { get; set; }

        public string Password { get; set; }
    }

    // パスワードを除いたユーザー情報を格納する
    public class UserViewModel
    {
        public string Id { get; set; }

        public string UserName { get; set; }
    }

    // トークンを取得するために渡された認証情報を格納する
    public class TokenInputModel
    {
        [Required]
        public string UserName { get; set; }

        [Required]
        public string Password { get; set; }
    }

    // 生成したトークンと有効期限を格納する
    public class TokenViewModel
    {
        public string Token { get; set; }

        public DateTime Expiration { get; set; }
    }

    [Route("api")]
    public class HomeController : Controller
    {
        // テスト用ユーザー
        static List<User> _testUsers = new List<User>()
        {
            new User()
            {
                Id = Guid.NewGuid().ToString(),
                UserName = "tnakamura",
                Password = "test1234",
            }
        };

        [HttpPost("token")]
        public IActionResult Token([FromBody]TokenInputModel inputModel)
        {
            if (ModelState.IsValid)
            {
                var user = _testUsers.FirstOrDefault(u => u.UserName == inputModel.UserName);
                if (user != null && user.Password == inputModel.Password)
                {
                    var token = CreateJwtSecurityToken(user);
                    return Ok(new TokenViewModel
                    {
                        Token = new JwtSecurityTokenHandler().WriteToken(token),
                        Expiration = token.ValidTo,
                    });
                }
            }
            return BadRequest();
        }

        JwtSecurityToken CreateJwtSecurityToken(User user)
        {
            // JWT に含めるクレーム
            var claims = new List<Claim>()
            {
                // JwtBearerAuthentication 用
                new Claim(JwtRegisteredClaimNames.Jti, user.Id),
                new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                // User.Identity プロパティ用
                new Claim(ClaimTypes.Sid, user.Id),
                new Claim(ClaimTypes.Name, user.UserName),
            };

            var token = new JwtSecurityToken(
                issuer: AppConfiguration.SiteUrl,
                audience: AppConfiguration.SiteUrl,
                claims: claims,
                expires: DateTime.UtcNow.AddDays(7),
                signingCredentials: new SigningCredentials(
                    new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AppConfiguration.SecretKey)),
                    SecurityAlgorithms.HmacSha256
                )
            );

            return token;
        }

        [Authorize]
        [HttpGet("me")]
        public IActionResult Me()
        {
            var id = User.Claims.First(c => c.Type == ClaimTypes.Sid).Value;
            var user = _testUsers.First(u => u.Id == id);
            return Ok(new UserViewModel
            {
                Id = user.Id,
                UserName = user.UserName,
            });
        }
    }
}

今回も curl を使って動作を確認してみる。 まずは JWT を取得せずに、認証が必要な Web API を呼び出してみると、 ちゃんと 401 が返ってくる。

$ curl -i -s http://localhost:5000/api/me
HTTP/1.1 401 Unauthorized
Date: Wed, 30 Aug 2017 07:41:55 GMT
Server: Kestrel
Content-Length: 0
WWW-Authenticate: Bearer

アクセストークンを取得。

$ curl -i -s -H 'Content-Type: application/json' --data '{"userName":"tnakamura","password":"test1234"}' http://localhost:5000/api/token
HTTP/1.1 200 OK
Date: Wed, 30 Aug 2017 07:44:14 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwMjBkNzI1Yi03NjNlLTQ0YjMtYWM5ZS00ZjI2YzQwMTE2MDgiLCJzdWIiOiJ0bmFrYW11cmEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiIwMjBkNzI1Yi03NjNlLTQ0YjMtYWM5ZS00ZjI2YzQwMTE2MDgiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidG5ha2FtdXJhIiwiZXhwIjoxNTA0NjgzODU1LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.dyii4SK204KDYvc-fSc_OVZ36-FAyTaa4dtxradCmQI","expiration":"2017-09-06T07:44:15Z"}

取得したアクセストークンを Authorization ヘッダーに指定して、認証が必要な Web API を呼び出すと成功する。

$ curl -i -s -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwMjBkNzI1Yi03NjNlLTQ0YjMtYWM5ZS00ZjI2YzQwMTE2MDgiLCJzdWIiOiJ0bmFrYW11cmEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiIwMjBkNzI1Yi03NjNlLTQ0YjMtYWM5ZS00ZjI2YzQwMTE2MDgiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidG5ha2FtdXJhIiwiZXhwIjoxNTA0NjgzODU1LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.dyii4SK204KDYvc-fSc_OVZ36-FAyTaa4dtxradCmQI' http://localhost:5000/api/me
HTTP/1.1 200 OK
Date: Wed, 30 Aug 2017 07:45:23 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

{"id":"020d725b-763e-44b3-ac9e-4f26c4011608","userName":"tnakamura"}

元気一杯

呉服町にある『元気一杯』に行ってきた。この店は心理的障壁が高くて、なかなか行けずにいたんだけど、ようやくといった感じ。店内は撮影禁止なので、写真は店の外観だけ。

注文したのは『ラーメン(700円)』。 本家のコラーゲンスープはお腹にたまるスープで、まるで豚骨味のコラーゲンを飲んでるみたいだ。コラーゲン飲んだことないけど。 麺はツルツルとしていて珍しいかも。 スープのとろみが凄いのでツルツルの麺でもよく絡む。 むしろ普通の麺だと絡みすぎて凄いことになりそうだ。

まず他所ではお目にかかれないスープで、福岡市にあるラーメン屋の中でも一線を画している。 姉妹店の『博多元気一番!!』ができるまでは、 ここでのみ味わうことができる、まさに 唯一無二の存在だったわけだ。 ケータイ・スマホ・カメラお断り、 辛子高菜を先に食べたら退場、 子連れもNG。 それでも客が絶えないのも納得の一杯だった。

関連ランキング:ラーメン | 呉服町駅千代県庁口駅中洲川端駅

蜂楽饅頭のコバルトアイス

今年の夏はまだかき氷を食べていなかったので、 西新商店街にある『蜂楽饅頭 福岡西新本店』まで行ってみた。

お目当は『コバルトアイス』(300円)。 見た目にも爽やかで美しい。 氷はフワッフワで口どけの良さは文句無し。 国産純粋蜂蜜と練乳で作られている綺麗な青い色の蜜が、 まんべんなくたっぷりとかかっていて、 最後まで美味しくいただけた。 青いからといって食欲が減退するなんてこともない。 優しいような懐かしいような、 そんな味に舌鼓を打った。 値段も良心的だし、これは毎年食べたくなる。

関連ランキング:たい焼き・大判焼き | 西新駅藤崎駅