まったり技術ブログ

Technology is power.

【knock】JSON Web Token(JWT)を使ってみる【セキュリティ編】

f:id:motikan2010:20170421183524p:plain

前回の続きです。
JSON Web Token(JWT)を使ってみる【実装編】 - まったり技術ブログ
今回はJWTのセキュリティにふれてみます。

JWTは危険なのか

認証成功時に発行されるJSONは軽く見たかんじ、少し長い乱数のセッションIDのように見えますがそうではありません。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8

規則性があり、下記の記事のように危険がふくまれているそうです。

auth0.com

christina04.hatenablog.com

発行されるJSONはこのような形式になっています。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0
EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8

が発行された場合に、まずは「.」で区切る。
個々の値をBase64でデコードします。これだけです。

base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 {“typ”:“JWT”,“alg”:“HS256”}
eyJleHAiOjE0OTI4NDY3MTUsInN1YiI6NX0 {“exp”:1492816836,“sub”:5}
EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8 (署名バイナリデータ)

ユーザによって値が改ざんされる危険性

ここで重要なのが「“sub”:5」の"5"という数値がユーザidということです。
Webアプリケーション側では、この数値でユーザを識別されており、この値を他ユーザの値に改ざんして送信することによって、なりすましを行うことが可能となっています。
本来は「EzBo2BZa・・・」の値が検証トークンとなっており、値が改ざんされたことを検出できるが、algの指定にnoneが用いられた時に検証されないとのことです。
つまり、「{“typ”:“JWT”,“alg”:“none”} 」の場合に、なりすましが行われてしまう
そのことがJWTのセキュリティ上の懸念となっている。

ここではknockのみに焦点を当てて、改ざんが検出されるか、またはされないかの確認をしていきます。

knockはどうなのか

試しに「"sub":5」を「sub":6」に改ざんしてリクエストを送信してみます。

アルゴリズムを「HS256」

まずは、knockのデフォルトアルゴリズムで確認します。

ユーザid:5で認証

$ curl -X "POST" "http://nuconuco.com:3000/user_token" \
-H "Content-Type: application/json" \
-d '{"auth": {"email": "user1@example.com", "password": "passwd1"}}'

{"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6NX0.L_inYpObtUsQE_lEP_Kk2FNgP8888ppMICykuGa7AVQ"}%
Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 {“typ”:“JWT”,“alg”:“HS256”}
eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6NX0 {“exp”:1492847990,“sub”:5}

「"sub":6」に改ざんして送信

Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 {“typ”:“JWT”,“alg”:“HS256”}
eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6Nn0= {“exp”:1492847990,“sub”:6}
$ curl -X "GET" "http://example.jp:3000/private-posts" -v \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6Nn0=.L_inYpObtUsQE_lEP_Kk2FNgP8888ppMICykuGa7AVQ" \
-H "Content-Type: application/json"


HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html
Cache-Control: no-cache
X-Request-Id: dc65a24d-e7c6-4cd7-abb2-0eaa27680f0a
X-Runtime: 0.003603
Transfer-Encoding: chunked

改ざんが検知されて、「HTTP/1.1 401 Unauthorized」となっている。

アルゴリズムを「none」

algにnoneを指定すれば、署名であるトークンが発行されずに改ざんができるのかを確認します。

設定ファイルの編集

$ vim config/initializers/knock.rb

# config.token_signature_algorithm = 'HS256'
config.token_signature_algorithm = 'none' # 32行付近に追記

これで「{typ: “JWT”, alg: “none”}」となってくれるはずです。

ユーザid:5で認証

$ curl -X "POST" "http://nuconuco.com:3000/user_token" \
-H "Content-Type: application/json" \
-d '{"auth": {"email": "user1@example.com", "password": "passwd1"}}'

{"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6NX0."}%
Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0 {“typ”:“JWT”,“alg”:“none”}
eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6NX0 {“exp”:1492848635,“sub”:5}

予想通り「{“typ”:“JWT”,“alg”:“none”}」になり、認証トークンも発行されていないことが分かります 。 この状態で「"sub":5」を改ざんするとなりすましが可能であるか確認してみます。

「"sub":6」に改ざんして送信

Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0 {“typ”:“JWT”,“alg”:“none”}
eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6Nn0= {“exp”:1492848635,“sub”:6}
$ curl -X "GET" "http://example.jp:3000/private-posts" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6Nn0=." \
-H "Content-Type: application/json"

HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html
Cache-Control: no-cache
X-Request-Id: 55fda114-d9c3-435f-aa49-22816180bd97
X-Runtime: 0.004454
Transfer-Encoding: chunked

結果は「HTTP/1.1 401 Unauthorized」
なんと「"alg":“none"」を指定しているがエラーになった。
knockでは「"alg”:“none"」に指定しても、認証トークンが検証されることが確認できた。
でも何故なのか。


knockのソースを見て原因を調べてみる。

なぜknockでは「"alg":“none"」で検証が行われたのか

JWTの実装を確認してみる。

ruby-jwtの仕様を確認

knockは内部でruby-jwtを利用しているので、まずはruby-jwtのREADMEを見てみます。
github.com

を見てみると、

decoded_token = JWT.decode token, nil, false

JSONのでコード時つまりトークンの検証時に、第3引数に検証有無を指定する必要があるらしく、上記のようにfalseが指定されていると検証が行われません。

knockの実装を確認

knock/auth_token.rb at 7fb00e36b8a1db188d2258eb28dbc56441385302 · nsarno/knock · GitHub

# 10行付近
@payload, _ = JWT.decode token, decode_key, true, options.merge(verify_options)

knockだとtrueが指定されており、検証が必須となっている。 そのため「"alg":“none"」となっていても検証が行われていたわけです。

knockでトークン検証なしにしてみる

ソースコードを少し変更してみて、トークンの検証が行われないようにしてみます。
試しに第3引数を"false"に変更して、動作を確認してみる。

$ vim vendor/bundler/ruby/2.3.0/gems/knock-2.1.1/app/model/knock/auth_token.rb

@payload, _ = JWT.decode token, decode_key, false, options.merge(verify_options) # 変更

再度改ざんしたリクエストを送信してみる。認証者をレスポンスで返すようにする。

$ vim app/controllers/private_posts_controller.rb

class PrivatePostsController < ApplicationController
  include JSONAPI::ActsAsResourceController
  before_action :authenticate_user

  def index
    render :json => current_user
  end

end
curl -X "GET" "http://example.jp:3000/private-posts" -v \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6Nn0=." \
-H "Content-Type: application/json"

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
ETag: W/"f7e6dd29a636a3d858fab2dd8c67e57d"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: e4fdf9a4-49e7-4bff-be60-ae8351dc4483
X-Runtime: 0.004916
Transfer-Encoding: chunked

{"id":6,"password_digest":"$2a$10$2IL4VyZ2m2ojfjrWpMxiDOTL6Ctu43cm2o6423z7xCET71HtZVSRC","name":"User2","email":"user2@example.com","created_at":"2017-04-20T18:46:10.245Z","updated_at":"2017-04-20T18:46:10.245Z"}%

改ざんしたユーザidの情報を取得できていることが確認できる。

結論

knockの場合だと、変にknock自体のソースコードを変更しない限り、トークンの検証は必ず行われると考えられます。
今回は必然的に改ざんの検出が行われるとなりましたが、他のJWTライブラリではどうなのか。設定次第では検証を行わせないようなライブラリがあるのかなどを探していきます。

【knock】JSON Web Token(JWT)を使ってみる【実装編】

f:id:motikan2010:20170421183808p:plain

github.com

JSON Web Tokenの説明は下記の記事を参照。 qiita.com

事前準備

新規アプリを生成

$ rails new railsJWT --api

Gemfileに追記

必要なライブラリをGemfileに追記します。

$ vim Gemfile

# 下記を追記
gem "faker"
gem "bcrypt"
gem "jsonapi-resources"
gem "knock"

$ bundle install

GitHub - cerebris/jsonapi-resources: A resource-focused Rails library for developing JSON API compliant servers.

モデルの作成

Postモデル

「タイトル」「内容」「公開/非公開の指定」のカラムを保持したPostモデルを作成します。

$ rails g model Post title:string body:text type:string
$ touch app/models/private_post.rb
$ touch app/models/public_post.rb
$ vim app/models/private_post.rb

class PrivatePost < Post
end

$ vim app/models/public_post.rb

class PublicPost < Post
end
$ vim app/models/post.rb

class Post < ApplicationRecord
  validates :body, presence: true
  validates :title, presence: true
  validates :type, presence: true

  POST_TYPES = %w(PublicPost PrivatePost)
  validates :type, :inclusion => { :in => POST_TYPES }
end

Userモデル

次に「パスワード」「名前」「メールアドレス」のカラムを保持したUserモデルを作成します。

$ rails g model user password_digest:string name:string email:string
$ vim app/models/user.rb

class User < ActiveRecord::Base
  has_secure_password

  validates :name, presence: true
  validates :email, presence: true
end
$ rails db:migrate

テストデータの追加

$ vim db/seeds.rb

Post.destroy_all
User.destroy_all

# ユーザを作成
User.create!({
  name: 'User1',
  email: 'user1@example.com',
  password: 'passwd1',
  password_confirmation: 'passwd1'
})

User.create!({
  name: 'User2',
  email: 'user2@example.com',
  password: 'passwd2',
  password_confirmation: 'passwd2'
})

3.times do
  # 公開記事を作成
  PublicPost.create!(
    title: Faker::Lorem.sentence,
    body: Faker::Lorem.paragraphs.join(' ')
  )

  # 非公開記事を作成
  PrivatePost.create!(
    title: Faker::Lorem.sentence,
    body: Faker::Lorem.paragraphs.join(' ')
  )
end
$ rails db:seed

コントローラの作成

PublicPostsコントローラ

$ vim app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  include Knock::Authenticable # 追記
end
$ rails g controller PublicPosts
$ vim vim app/controllers/public_posts_controller.rb

class PublicPostsController < ApplicationController
  include JSONAPI::ActsAsResourceController # 追記
end
$ rails generate jsonapi:resource public_posts
$ vim app/resources/public_post_resource.rb

class PublicPostResource < JSONAPI::Resource
  immutable
  attributes :title, :body
end

ルーティング設定

$ vim config/routes.rb

jsonapi_resources :public_posts # 追記

PrivatePostsコントローラ

$ rails generate knock:install
$ rails generate knock:token_controller user

before_action :authenticate_user」を追記することによって、認証が必要なコントローラにすることができます。

$ rails g controller PrivatePosts
$ vim app/controllers/private_posts_controller.rb

class PrivatePostsController < ApplicationController
  include JSONAPI::ActsAsResourceController # 追記
  before_action :authenticate_user # 追記
end
$ rails generate jsonapi:resource private_posts
$ vim app/resources/private_post_resource.rb

class PrivatePostResource < JSONAPI::Resource
  immutable
  attributes :title, :body
end

ルーティング設定

$ vim config/routes.rb

jsonapi_resources :private_posts

動作確認

リクエスト

“/public-posts"にアクセス

認証を行わずにアクセスすることが可能です。

$ curl -X "GET" "http://example.jp:3000/public-posts"

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/vnd.api+json
ETag: W/"ae93de1833f5e081219472e78b408c0a"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: a0b4df38-9374-4801-97c3-714599305b00
X-Runtime: 0.010128
Transfer-Encoding: chunked

{"data":[{"id":"1","type":"public-posts","links":{"self":"http://example.jp:3000/public-posts/1"},"attributes":{"title":"Necessitatibus et sit alias.","body":"Numquam...(中略)..."}}]}%

“/private-posts"にアクセス

レスポンスで「HTTP/1.1 401 Unauthorized」と返ってきており、認証が必要ということが分かります。

$ curl -X "GET" "http://example.jp:3000/private-posts"

HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html
Cache-Control: no-cache
X-Request-Id: 7cde37bd-abdd-421f-a6dc-5667e8cce0d0
X-Runtime: 0.002747
Transfer-Encoding: chunked

認証を行う

“/private-posts"に対してアクセスを行うためには、認証後に発行されるトークンをリクエストに含める必要があります。

トークンを取得する認証リクエスト

$ curl -X "POST" "http://nuconuco.com:3000/user_token" \
> -H "Content-Type: application/json" \
> -d '{"auth": {"email": "user1@example.com", "password": "passwd1"}}'

{"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8"}%

JSON形式で返ってきている「eyJ0eXAiOiJKV1QiL・・・」が認証トークンです。

トークンを使用してアクセス

Authorizationヘッダの値に取得したトークンを指定します。

$ curl -X "GET" "http://example.jp:3000/private-posts" \
> -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8" \
> -H "Content-Type: application/json"

GET /private-posts HTTP/1.1
Host: example.jp:3000
User-Agent: curl/7.43.0
Accept: */*
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8
Content-Type: application/json

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/vnd.api+json
ETag: W/"df8eaf13cb9cd4dd8f47df9f4ec65bb3"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 5050c781-bbfb-43a6-a7f7-2b887682d3a0
X-Runtime: 0.022478
Transfer-Encoding: chunked

{"data":[{"id":"2","type":"private-posts","links":{"self":"http://example.jp:3000/private-posts/2"},"attributes":{"title":"Qui voluptas nemo tenetur.","body":"Nemo...(中略)..."}}]}%

正常に"/private-posts"にアクセスすることができています。

これでJSON Web Tokenの実装が完了となります。

ボタンを重ねない『クリックジャッキング』の話

f:id:motikan2010:20170407185617g:plain
Trap Site

2017年度に入りましたが、昔ながらのクリックジャッキングの話
『クリックジャッキング』ってなんぞやという方は下の記事が大変解りやすいかと。
blog.tokumaru.org

クリックジャッキングを成立させることは難しいのか

クリックジャッキングのことで同期の方と話した時に、おとりとなるボタンを押下させることがそもそも難しいということで、リスクは低いと言っていた。
f:id:motikan2010:20170407190439p:plain
(参考:http://www.ipa.go.jp/files/000026479.pdf)
確かに、いろんなサイトでのクリックジャッキングの説明を見てみるとおとりのボタンやリンクに『ここをクリック』だったり『おすすめ情報!!』になっており、見るからに怪しく、騙されてクリックする人なんていなさそう。
f:id:motikan2010:20170407185323p:plain

自然にボタンがページに紛れ込んでいても特定の箇所をクリックさせることがそもそも難しい気がする・・・。
f:id:motikan2010:20170407185509p:plain
そう考えてみると、攻撃を成立させることは難しそう。

どこをクリックしてもターゲットとなるボタンを押下させる

常にマウスカーソルにターゲットとなるサイト(iframe内)が来るようにJavaScriptで制御してみる。 http://biboroku.watanabehiroki.net/markup/javascript/jquery-sample-2

あとは、ターゲットとなるサイト内のボタンが左上に来るように調整する。 http://blog12345.seesaa.net/article/281787492.html

ターゲットとなるiframeは下記のように記述
<div id="target" style="width:180px;height:50px;margin:0px;opacity:0.5;overflow:hidden;">
  <iframe width="300px" height="650px" scrolling="no" frameborder="0"
    style="margin:-350px 0 0 -25px;overflow:hidden;" src="./target.html"></iframe>
</div>
divがマウスカーソルに追跡するように記述
$('html').mousemove(function(e){
    $('#target').css({
      top:e.pageY-25,
      left:e.pageX-50
      });
  });

デモサイト

Trap Site

透過50%です。透過100%にしたらまず気づかれることはなさそう。
f:id:motikan2010:20170407185617g:plain
これでどこをクリックしてもiframe内のボタンをクリックしたことになります。

【Go言語】パスワードをハッシュ化(bcrypt)

f:id:motikan2010:20170213221701p:plain
・bcrypt.GenerateFromPassword
・bcrypt.CompareHashAndPassword
を使ってみる話です。

Go言語を使ったWeb開発で認証機能を実装したくて調べてみたらこんなリポジトリが見つかった。

github.com

よく見てみるとパスワード格納はライブラリに含まれておらず、サンプルコードでも無残にパスワードが平文で格納されているではありませんか・・・。

Go言語でのパスワードハッシュに関して調べてみると下記の記事が見つかった。

hachibeechan.hateblo.jp

ハッシュ化のライブラリには主に2つ用意されているらしい。
・scrypt
・bcrypt

本記事では、bcryptライブラリを使ってハッシュ化する方法を紹介します。

bcrypt - GoDoc

パスワードのハッシュ化

hash, err := bcrypt.GenerateFromPassword([]byte("パスワード"), bcrypt.DefaultCost)

// Byteで返されるので文字列に変換して表示
fmt.Println(string(hash))

// 毎回値の異なるハッシュ値が取得できる(ちなみに"password"のハッシュ値)
// $2a$10$iuJaubQvGTawiwa6UFa08uvOGwFaa25Wz29llEKEFHyPT3w262Qw6
// $2a$10$HFZ4bmj98bEePKO3gNsbZO3XsgXORvjFhexZV6HADm46/CuaE6M/m
// $2a$10$BSzyPPKOOs0YwC1h6UoD2eNFAyWYVfS.hmZQuQLLTRyC/Z.z3fzsy

パスワード文字列とハッシュ値を比較

認証部分は下記のように記述します。

err = bcrypt.CompareHashAndPassword([]byte("ハッシュ値"), []byte("パスワード"))
// 一致している場合はerrにnilが返されます。一致していない場合はエラーが返されます。

コードにまとめるとこのような感じ

下記のコードでは「Success」が表示されます。

package main

import (
    "fmt"

    "golang.org/x/crypto/bcrypt"
)

func main() {
    storePass := "password"
    loadPass := "password"

    hash, err := bcrypt.GenerateFromPassword([]byte(storePass), bcrypt.DefaultCost)
    if err != nil {
        return
    }
    hash_str := string(hash)

    err = bcrypt.CompareHashAndPassword([]byte(hash_str), []byte(loadPass))
    if err != nil {
        fmt.Println("Failure")
    } else {
        fmt.Println("Success")
    }

}

認証のデモ

今回はパスワード認証機能が動作確認をしたいだけなので、データベースなどは用意せずにログインIDとパスワードを格納することができるUser構造体を使って動作確認を行う。

package main

import (
    "fmt"
    "time"

    "golang.org/x/crypto/bcrypt"
)

type User struct {
    LoginId  string
    Password string
}

type Users []*User

var users Users

func register(login_id, pass string) {
    /*
      bcrypt.MinCost = 4
      bcrypt.MaxCost = 31
      bcrypt.DefaultCost = 10
   */
    hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
    if err != nil {
        return
    }
    users = append(users, &User{LoginId: login_id, Password: string(hash)})
}

func login(login_id, password string) {
    var hash_str = ""
    start := time.Now()
    for _, user := range users {
        if login_id == user.LoginId {
            hash_str = user.Password
            break
        }
    }
    err := bcrypt.CompareHashAndPassword([]byte(hash_str), []byte(password))
    end := time.Now()
    fmt.Printf("%fs\t", (end.Sub(start)).Seconds())
    if err != nil {
        fmt.Print("Failure")
    } else {
        fmt.Print("Success")
    }
    fmt.Printf("\t%s/%s\n", login_id, password)
}

func main() {
    users = Users{}
  // 登録
    register("user1", "password1")
    register("user2", "password2")
    register("user3", "password3")
    register("user4", "password4")
    register("user5", "password5")
  fmt.Println()

  // 認証
    login("user1", "password1")
    login("user2", "password2")
    login("user3", "password3")
    login("user4", "password4")
    login("user5", "password5")
    login("user6", "password1")
    login("user1", "")
    login("user3", "password1")
    login("user3", "password2")
    login("user3", "password3")
    login("user3", "password4")

}
出力
0.106827s    Success user1 / password1
0.096496s   Success user2 / password2
0.099798s   Success user3 / password3
0.099758s   Success user4 / password4
0.100966s   Success user5 / password5
0.000001s   Failure user6 / password1
0.125239s   Failure user1 /
0.097884s   Failure user3 / password1
0.097816s   Failure user3 / password2
0.097296s   Success user3 / password3
0.096882s   Failure user3 / password4

処理時間を見てみると、ログインIDに「user6」を指定した時だけ処理時間が異様に短いことが分かる。
存在しないユーザであり「CompareHashAndPassword」の処理がとてつもなく短くなるからである。
レスポンス時間からユーザの有無を知られるなんて気分が悪いので、このように"hash_str"を初期化してみる。

var hash_str = "$2a$10$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

こうすれば常に「CompareHashAndPassword」の処理時間が同等になる。

0.097851s Success user1 / password1
0.095720s  Success user2 / password2
0.097851s  Success user3 / password3
0.097147s  Success user4 / password4
0.104292s  Success user5 / password5
0.099853s  Failure user6 / password1
0.098755s  Failure user1 /
0.094102s  Failure user3 / password1
0.093256s  Failure user3 / password2
0.100131s  Success user3 / password3
0.097537s  Failure user3 / password4

これでレスポンス時間からユーザの有無を知られることはなくなるかと思う。

Webフレームワーク『Gin』を使ってみる

f:id:motikan2010:20170211223100p:plain
Go言語でWebアプリ開発をしていみたいと思っていましたので、調べてみたらいろいろあるらしい。

概観からGoのWebFrameworkを選ぶ(2016/02) - Qiita その中で速度が速く、人気もある『Gin』を手始めにさわってみることにします。

github.com

作成するものは「SQLiteを使ったTODOリストアプリ」です。
こちらの記事を参考にして作成しました。

Go言語製WAF GinでWebアプリを作ってみる【準備編】 | eureka tech blog

作成順序としては
 ビュー → コントローラ → モデル
です。

下記のコマンドでGinをインストールすることができます。

$ go get gopkg.in/gin-gonic/gin.v1

HTMLテンプレートを呼び出す

まずは「/」にアクセスしたら、「index.tmpl」を呼び出し、出力するだけのコードを書いていきます。

$ vim main.go
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("views/*")

    router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.tmpl", gin.H{
                    "title": "Hello Gin!",
                })
    })

    router.Run(":8080")
}

HTMLテンプレート作成

main.goで

gin.H{
    "title": "Hello Gin!",
}

と定義しているので、テンプレート側で「{{ .title }}」と記述してレンダリングさせることができます。

$ mkdir views
$ vim views/index.tmpl
<!DOCTYPE html>
<html>
    <head>
        <title>{{ .title }}</title>
    </head>
    <body>
        <h3>{{ .title }}</h3>
    </body>
</html>

ここまでできたら起動させてアクセスしてみます。

動作確認

$ go run main.go

http://127.0.0.1:8080」にアクセスしてみます。
f:id:motikan2010:20170211215514p:plain

コントローラを作る

ビューとモデル(DB)を橋渡しをするコントローラを作成していきます。

タスク一覧を渡すコントローラ作成

今はタスク一覧はDBから持ってくるのではなく、コントローラ内でをタスク配列として用意し、
そのタスク一覧をテンプレート側に渡す処理を書いていきます。

$ mkdir controllers
$ vim controllers/task.go
package task

import "strconv"

// idとテキストを保持する構造体
type Task struct {
    ID   int
    Text string
}

func NewTask() Task {
    return Task{}
}

// タスク構造体一覧を返す
func (c Task) GetAll() interface{} {

    // テストデータとして5つタスクを作成
    tasks := make([]*Task, 5)
    for i := 1; i <= 5; i++ {
        tasks[i-1] = &Task{ID: i, Text: "Task Text " + strconv.Itoa(i)}
    }

    return tasks
}

コントローラの呼び出し

$ vim main.go
//・・・

router.GET("/", func(c *gin.Context) {
    controller := task.NewTask()
    tasks := controller.GetAll()

    c.HTML(http.StatusOK, "index.tmpl", gin.H{
        "title": "TODO List",
        "tasks": tasks,    // 追記 テンプレートにタスクを渡す
    })
})

//・・・

テンプレートを修正

タスク一覧をリストとして表示するforを記述していきます。

$ vim views/index.tmpl
//・・・

<body>
<h3>{{ .title }}</h3>
    <ul>
    {{ range $index, $task := .tasks }}
        <li>{{ $task.ID }}: {{ $task.Text }} </li>
    {{ end }}
    </ul>
</body>

//・・・

f:id:motikan2010:20170211215616p:plain

モデルを作成して、DBにタスクを保存する

タスク内容をDB内に保存できるようにします。
今回DBはSQLiteを使っていきます。

Modelを作成

タスクを登録するためにCreate関数を用意しています。
文字列の引数を受け取り、その文字列をDBに挿入するような動作を行います。

$ mkdir models
$ vim models/task.go
package task

import (
    "github.com/jinzhu/gorm"
    _ "github.com/mattn/go-sqlite3"
)

var db *gorm.DB

func init() {
    var err error

    db, err = gorm.Open("sqlite3", "task.db")

    db.DropTableIfExists(&Task{})
    db.CreateTable(&Task{})

    if err != nil {
        panic(err)
    }
}

type Task struct {
    ID   int    `gorm:"primary_key"`
    Text string `gorm:"size:140"`
}

type Tasks []Task

type TaskRepository struct {
}

func NewTaskRepository() TaskRepository {
    return TaskRepository{}
}

// データベースに一行登録する
func (m TaskRepository) Create(text string) {
    var task = Task{Text: text}
    db.NewRecord(task)
    db.Create(&task)
    db.Save(&task)
}

コントローラ − Create関数を追加

こちらでもCreate関数を定義しています。
モデル内に定義されているCreate関数に対して、ユーザが送信したタスク文字列を渡しています。

$ vim controllers/task.go
import (
    "strconv"

    task "../models"
)

//・・・

func (c Task) Create(text string) {
    repo := task.NewTaskRepository()
    repo.Create(text)
}

POSTデータの受け取り

「text := c.PostForm(“text”)」でユーザが送信したパラメータを取得することができます。

$ vim main.go
func main() {

    //・・・

    router.POST("/", func(c *gin.Context) {
        text := c.PostForm("text")
        ctrl := task.NewTask()
        ctrl.Create(text)

        c.Redirect(http.StatusMovedPermanently, "/")
    })

登録フォームの作成

アプリケーションにタスク文字列を送信するためにフォームを追加します。

$ vim views/index.tmpl
//・・・

<ul>
<form action="/" method="post">
  <input type="text" name="text"></input>
  <input type="submit" value="送信">
</form>
{{ range $index, $task := .tasks }}
    <li>{{ $task.ID }}: {{ $task.Text }} </li>
{{ end }}
</ul>

//・・・

タスクを登録

$ go run main.go

f:id:motikan2010:20170211215637p:plain

登録されたタスクの確認

$ sqlite3 task.db

sqlite> .tables
tasks

sqlite> select * from tasks;
1|Test task

データベースに登録したタスクを出力

モデル − GetAll関数を追加

GetAll関数はDBに登録されているタスクを全て返します。

$ vim models/task.go
func (m TaskRepository) GetAll() Tasks {
    var tasks = Tasks{}
    db.Find(&tasks)

    return tasks
}

コントローラ − GetAll関数を修正

$ vim controllers/task.go
func (c Task) GetAll() interface{} {
    repo := task.NewTaskRepository()
    tasks := repo.GetAll()

    return tasks
}

f:id:motikan2010:20170211215706p:plain

idを指定してタスクを取得

モデル - GetByID関数を追加

GetByID関数はタスクIDを引数として受け取り、該当するタスクを返します。

$ vim models/task.go
func (m TaskRepository) GetByID(id int) Tasks {
    var tasks = Tasks{}
    db.Find(&tasks, id)
    return tasks
}

コントローラ - Get関数を追加

$ vim controllers/task.go
func (c Task) Get(n int) interface{} {
    repo := task.NewTaskRepository()
    tasks := repo.GetByID(n)

    return tasks
}

 Getを呼び出す

func main() {

    //・・・

    router.GET("/:id", func(c *gin.Context) {
            var id, _ = strconv.Atoi(c.Param("id"))
            ctrl := task.NewTask()
            tasks := ctrl.Get(id)

            c.HTML(http.StatusOK, "index.tmpl", gin.H{
                "tasks": tasks,
            })
        })

動作確認

http://127.0.0.1:8080/2」にアクセスすると、idが2のタスクが表示されます。 f:id:motikan2010:20170211215739p:plain