.NET Core で Shift-JIS を扱う

例えば .NET Core でテキストファイルを出力するとき、文字コードに Shift-JIS を指定するには System.Text.Encoding.CodePages が必要だった。

www.nuget.org

using System;
using System.IO;
using System.Text;

namespace EncodingSample
{
    class Program
    {
        static void Main(string[] args)
        {
            // エンコードプロバイダーの登録が必要
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

            var path = Path.Combine(".", "result.txt");
            using (var stream = File.OpenWrite(path))
            {
                // エンコードプロバイダーを登録していなかったら
                // ArgumentException が発生してしまう
                using (var writer = new StreamWriter(stream, Encoding.GetEncoding("shift_jis")))
                {
                    writer.WriteLine("Hello,World!");
                }
            }

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

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

以前、ASP.NET Core MVC で Basic 認証を行う記事を書いた。

tnakamura.hatenablog.com

プライベートな Web API なのでこれでいいかなと思っていたが、 大人の事情でそうはいかなくなり、 JWT(Json Web Token) を使った認証に変えることに。

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.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
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)
        {
            var host = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .UseApplicationInsights()
                .UseUrls(AppConfiguration.SiteUrl)
                .Build();

            host.Run();
        }
    }

    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

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

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseJwtBearerAuthentication(new JwtBearerOptions
            {
                // 着信したトークンの意図された受信者、またはトークンがアクセスを許可
                // するリソースを表す。
                // このパラメータで指定された値がトークンの aud パラメータと一致しない
                // 場合、トークンは別のリソースにアクセスするために使用されたため、
                // 拒否される。
                Audience = AppConfiguration.SiteUrl,
                
                // トークンによって定義されたユーザーが自動的にログインするかどうか
                AutomaticAuthenticate = true,

                TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    ValidateIssuer = true,
                    ValidIssuer = AppConfiguration.SiteUrl,
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(AppConfiguration.SecretKey)),
                }
            });

            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>()
            {
                // JwtBeaerAuthentication 用
                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: Fri, 04 Aug 2017 04:47:20 GMT
Content-Length: 0
Server: Kestrel
WWW-Authenticate: Bearer

次は JWT を取得。

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

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI0ZjkxMWM0Mi05YzNjLTQwYjMtYmNlNC0zMzU2NDA3MDI0ZWEiLCJzdWIiOiJ0bmFrYW11cmEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiI0ZjkxMWM0Mi05YzNjLTQwYjMtYmNlNC0zMzU2NDA3MDI0ZWEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidG5ha2FtdXJhIiwiZXhwIjoxNTAyNDI2OTg0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.HlDiTrEvEeSjf4a8oAsVAJS-h5BZNPxCf4hBLkNVR8E","expiration":"2017-08-11T04:49:44Z"}

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

$ curl -i -s -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI0ZjkxMWM0Mi05YzNjLTQwYjMtYmNlNC0zMzU2NDA3MDI0ZWEiLCJzdWIiOiJ0bmFrYW11cmEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiI0ZjkxMWM0Mi05YzNjLTQwYjMtYmNlNC0zMzU2NDA3MDI0ZWEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidG5ha2FtdXJhIiwiZXhwIjoxNTAyNDI2OTg0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.HlDiTrEvEeSjf4a8oAsVAJS-h5BZNPxCf4hBLkNVR8E' http://localhost:5000/api/me
HTTP/1.1 200 OK
Date: Fri, 04 Aug 2017 04:51:29 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

{"id":"4f911c42-9c3c-40b3-bce4-3356407024ea","userName":"tnakamura"}

JWT を使った認証のメドは立ったかな。

もくもく Xamarin

自分で使うための iOS アプリを Xamarin で開発している。 Swift や Objective-C で開発しないのは、 大人の事情であり、 宗教的理由でもある。

最初 Xamarin.Forms で始めたが、 実現したい UI のためには、 Xamarin.Forms だとカスタムレンダラーをいくつも作る必要があったので、 早い段階で Xamarin.iOS に切り替えた。 Xamarin.iOS なので、当然 iOS 開発の知識が必要。 そこは RubyMotion のときと同じ。 仮に Xamarin.Forms で開発したとしても、どのみち必要になると思うが。

今回は作ろうとしているアプリの要件に合わなかっただけで、Xamarin.Forms は良いものだった。 WPF で慣れているのもあり、 Storyboard や Interface Builder でポチポチやるよりも、 XAML を書く方が圧倒的に効率が良かった。 Xamarin.Forms は macOSWPF を 2017 年第 3 四半期にサポート予定みたいだし、 アプリを新規に作るときは、これからもXamarin.Formsでいけそうか最初に試すと思う。

Swift から Xamarin.iOS への移行は、 Objective-C から Swift に移行したときとあまり変わらない印象。 iOS のライブラリで有名どころは、 Xamarin にポートしたものかバインディングが NuGet で入手できたりする。 無ければ自分でバインディングを作成すればいい。 ただ、RubyMotion の時はシームレスに iOS のライブラリを使えたので、 そこだけが障壁だな。 Xamarin から Swift に戻るのも難しくなさそう。 async/await 除けば。

ファイルに記述した SQL を PostgreSQL の CLI で実行する方法

しょっちゅう調べているのでブログにメモしておく。 psql コマンドの -f オプションで、SQL が書かれたファイルを指定するだけだった。

psql -h <host_name> -d <db_name> -U <user_name> -f <file_path>

サムライジェラート

梅雨が明けて夏本番。連日猛暑日。もうアイスでも食べないとやってられないので、前から雑誌で見て気になっていた警固公園そばにある『サムライジェラート』に行ってみた。

ジェラートの種類が豊富でどれにするか迷った。搾りたて阿蘇小国ジャージーミルクを食べることは決めていたので、あと一つ。イタリア旅行で思い出深いクイーンピスタチオにしてみた。

クイーンピスタチオは風味が予想以上に強く出ていて、食べ終わった後もしばらく残っていた。超ピスタチオって感じ。余韻を長く楽しめた。イタリアで食べたピスタチオのジェラートよりも美味しいかも。美化されているであろう記憶より美味いとなるとよっぽどだな。

阿蘇ジャージーミルクは、ミルクの濃厚さを味わいつつも、余韻はキレが良くて、総じてすっきりとした甘さという印象になった。クイーンピスタチオと組み合わせたのは正解だったな。

ハッピーあまおうや純正バニラ、ミルクショコラやビターショコラなど、他にも気になるジェラートがたくさんあった。搾りたて阿蘇小国ジャージーミルクとクイーンピスタチオもまた食べたい。全種類制覇するのは大変そうだ。

関連ランキング:アイスクリーム | 西鉄福岡駅(天神)天神南駅天神駅

UINavigationController の rootViewController を変更する

UINavigationController の setViewControllers メソッドを使って、ごっそり入れ替えればいい。 以下サンプルコード。Xamarin.iOS だけど。

NavigationController.SetViewControllers(
    new [] { newRootViewController },
    animated);

博多 旨鮨 小野

お祝いで寿司を食べることになったので、木の葉モールにある『博多 旨鮨 小野』に行ってきた。木の葉モールはファミリー向けのショピングモールで、その中の飲食店も子供連れで入りやすくて重宝している。

お昼に行ったけど、お祝いの席でお得なランチメニューを注文するのは興が削がれる気がしたので、 ランチにしてはちょっとだけお高めの『極厳選握り寿司十一貫』を注文。

茶碗蒸しは海鮮の出汁が効いた優しい味で良かった。子供もパクパク食べてた。

f:id:griefworker:20170720221204p:plain

お吸い物。

f:id:griefworker:20170720221229p:plain

そしてメインの寿司。 どれも美味かった。

f:id:griefworker:20170720221341p:plain

イクラ苦手だったけど克服したかも。

f:id:griefworker:20170720222118p:plain

f:id:griefworker:20170720222148p:plain

f:id:griefworker:20170720222744p:plain

f:id:griefworker:20170720222805p:plain

小野グループなだけあって味は期待通りだった。 とにかく小さい子供を連れて入りやすいのがポイント高いので、 お祝いで寿司でもという話になったら、 当分はファーストチョイスだろうな。

博多旨鮨 小野

食べログ 博多旨鮨 小野