search
LoginSignup
298

posted at

updated at

Go言語で理解するJWT認証 実装ハンズオン

go-jwt.png

僕がWEBアプリケーションを作る時、認証周りをどう実装するか毎回悩むのですが、JWT認証が非常に実装しやすかったのでハンズオン形式で紹介していきます。

##Go言語

サーバーサイドは Go言語でいきます。Googleが開発したプログラミング言語です。フロントエンド畑で育った僕が、なぜサーバーサイドにGo言語を選ぶかというと、主に下記の理由

  • 構文 & 型システムがシンプル。動的言語しかやってこなかった僕でもすんなり受け入れてくれた。
  • 並列処理が書きやすい。JavaScriptで苦戦した非同期処理が難なく書ける。
  • 標準パッケージのサポートが強力。コードのフォーマットも自動でやってくれる。

とにかく開発者に無駄なことをさせないという気迫がすごい。ゆえに書いてて、読んでて気持ち良い。フロントエンドでずっとやってきた僕のような人間が速習でサーバサイドを学びたいと思ったらGo言語を推します。シンプルゆえにJWTがどのように動いているかも分かりやすいので、今回はGo言語を使ったハンズオンにしました。

##JWT

JWT(Json Web Token)と書いてジョットと読むらしい。このトークンを使ってクライアント&サーバー間で認証できる。特徴としては、

  • 発行者が鍵を使ってJSONを署名し、トークンとして使う。
  • JSONなので任意の情報を含めることができる(ユーザー名など)。
  • 発行者は鍵を使ってトークンの検証を行う為、改竄を検知できる。

JWTは以下のフォーマットに従った文字列です。

{base64エンコードしたheader}.{base64エンコードしたclaims}.{署名}

実際にこちらのサイトでJWTのエンコード、デコードが試せる。

https://jwt.io/

実際の使い方はGo言語で実装しながら確認していきましょう。

##認証なしAPIをGo言語で実装

まずは認証なしのAPIサーバを作ります。ポート番号8080で立てます。
APIには軽量なウェブツールキット gorilla/mux を使って実装します。これはルーティングとかセッションとかを net/http よりは柔軟に使える感じのもの。

そしてmain.goを作り下記を実装

main.go
package main

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/gorilla/mux"
)

type post struct {
	Title string `json:"title"`
	Tag   string `json:"tag"`
	URL   string `json:"url"`
}

func main() {
    r := mux.NewRouter()
    // localhost:8080/publicでpublicハンドラーを実行
	r.Handle("/public", public)
    
    //サーバー起動
	if err := http.ListenAndServe(":8080", r); err != nil {
		log.Fatal("ListenAndServe:", err)
	}
}

var public = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	post := &post{
		Title: "VueCLIからVue.js入門①【VueCLIで出てくるファイルを概要図で理解】",
		Tag:   "Vue.js",
		URL:   "https://qiita.com/po3rin/items/3968f825f3c86f9c4e21",
	}
	json.NewEncoder(w).Encode(post)
})

muxで簡単にルーティングの設定ができています。json.NewEncoder(w).Encode(user)http.ResponseWriter にjsonにエンコードしたuserを書き込んでいます。これでJSONを返してます。

この main.go の1ファイルでJSONを返すAPIサーバの完成です。実際に叩いてみると記事のJSONが返ってきます。

$ curl localhost:8080/public
{"title":"VueCLIからVue.js入門①【VueCLIで出てくるファイルを概要図で理解】","tag":"Vue.js","url":"https://qiita.com/po3rin/items/3968f825f3c86f9c4e21"}

##JWT認証をつける

ここからが本番。JWT認証つけていきます。

###Go言語でJWT認証を追加する

main.go と同じディレクトリに auth/auth.go を作成し自作のauthパッケージを作ります。

auth.go
package auth

import (
	"net/http"
	"os"
	"time"

	jwtmiddleware "github.com/auth0/go-jwt-middleware"
	jwt "github.com/form3tech-oss/jwt-go"
)

// GetTokenHandler get token
var GetTokenHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

    // headerのセット
	token := jwt.New(jwt.SigningMethodHS256)

    // claimsのセット
	claims := token.Claims.(jwt.MapClaims)
	claims["admin"] = true
	claims["sub"] = "54546557354"
	claims["name"] = "taro"
	claims["iat"] = time.Now().Unix()
	claims["exp"] = time.Now().Add(time.Hour * 24).Unix()

    // 電子署名
	tokenString, _ := token.SignedString([]byte(os.Getenv("SIGNINGKEY")))

    // JWTを返却
	w.Write([]byte(tokenString))
})

// JwtMiddleware check token
var JwtMiddleware = jwtmiddleware.New(jwtmiddleware.Options{
	ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
		return []byte(os.Getenv("SIGNINGKEY")), nil
	},
	SigningMethod: jwt.SigningMethodHS256,
})

そして環境変数に"SIGNINGKEY"をセットします。これはJWTに電子署名する為のシークレットキーです。絶対に外部に漏れないようにしなければいけません。僕はdirenvを使ってるので、そこでセットします。もちろん.bashrcとかでも大丈夫です。めんどくさい人は直接指定しちゃって下さい。

export SIGNINGKEY=<こちらで自由に設定するシークレットキー>

main.goのGetTokenHandlerでclaimsや署名アルゴリズム(今回はHS256)を含めた上で署名したJWTが発行できます。JWTは以下のフォーマットに従った文字列ということを思い出してください。

{base64エンコードしたheader}.{base64エンコードしたclaims}.{電子署名}

これらの情報を含んだJWTを作成しています。header、claims、署名をそれぞれ簡単に仕様を紹介します。

###Header

ヘッダ情報には、署名アルゴリズムの種類やメタ情報が組み込まれます。今回はtoken := jwt.New(jwt.SigningMethodHS256)でJWTとHS256を指定しています。

項目 説明
alg 署名アルゴリズム。(HS256など)
typ トークのタイプ。(JWTが推奨)

###Claims

claimsにはユーザー属性情報を埋め込んでいます。claimsのには標準クレーム群が定義されています。全部は紹介しませんが下記のようなものが存在します。こちらからTokenに埋め込みたい情報を追加します。

クレーム名 説明
sub ユーザーの一意識別子
name フルネーム
profile プロフィールページの URL
picture プロフィール画像の URL
website ウェブサイトもしくはブログの URL
email メールアドレス
gender 性別
birthdate 誕生日
zoneinfo 地域情報
phone_number 電話番号
address 住所
updated_at 情報最終更新時刻

今回はハードコーディングしてますが、実際にはデータベースからもってきたデータをJWTに仕込んだりします。

###電子署名
トークンの検証に利用します。これがある為にトークンの改竄ができなくなっています。今回は下記の部分で、シークレットキーを使い署名しています。
tokenString, _ := token.SignedString([]byte(os.Getenv("SIGNINGKEY")))


##APIをJWT検証Middlewareで守る
上のコードで自作のauthパッケージができました。これをmain.goで読み込んで使います。main.goの中でfromでauthパッケージを読み込んでください

そして main.go のトップレベルにprivateハンドラーを追加します。

var private = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	post := &post{
		Title: "VGolangとGoogle Cloud Vision APIで画像から文字認識するCLIを速攻でつくる",
		Tag:   "Go",
		URL:   "https://qiita.com/po3rin/items/bf439424e38757c1e69b",
	}
	json.NewEncoder(w).Encode(post)
})

そして main.goのr.Handle("/public", public)の下に二行追加します。

r.Handle("/private", auth.JwtMiddleware.Handler(private))
r.Handle("/auth", auth.GetTokenHandler)

ここでは/privateのみJwtMiddlewareでラップしています。つまり/privateを叩くにはJWTによる認証が必要になります。

これでサーバ側の実装は終わりました。実際にAPIを叩きましょう。

$ curl localhost:8080/public
{"title":"VueCLIからVue.js入門①【VueCLIで出てくるファイルを概要図で理解】","tag":"Vue.js","url":"https://qiita.com/po3rin/items/3968f825f3c86f9c4e21"}

$ curl localhost:8080/private
Required authorization token not found

/private だけ認証トークンが必要だと言われてしまいます。ではtokenを所得して/privateを叩いてみましょう。まずは/auth を叩いてJWTを手に入れましょう。

$ curl localhost:8080/auth
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTI4Mjk5NTE3LCJpYXQiOiIyMDE4LTA2LTA2VDAwOjM4OjM3LjQxMDM1OTc4MyswOTowMCIsIm5hbWUiOiJ0YXJvIiwic3ViIjoiNTQ1NDY1NTczNTQifQ.48QRN_eXXzwXv1daqUmOIusnHAZH21tabmiJ5wFrAWE

JWTが返ってきました。エンコードされていますが、先ほど紹介した{header}.{claims}.{署名}の形になっています。
実際にこちらのサイトでJWTをデコードしてみましょう!
https://jwt.io/

スクリーンショット 2018-06-06 00.39.03.png

先ほどheaderやclaimsに込めた情報を確認できます。
このトークンを使って/privateが叩けます。AuthorizationヘッダにBearerスキームを設定して送信してサーバ側で検証します。

Authorization: Bearer {JWT token}

実際にはこんな感じ

$ curl localhost:8080/private -H "Authorization:Bearer <先ほど返ってきたJWT>"
{"title":"VGolangとGoogle Cloud Vision APIで画像から文字認識するCLIを速攻でつくる","tag":"Go","url":"https://qiita.com/po3rin/items/bf439424e38757c1e69b"}

先ほどリクエストできなかった/privateからデータが返ってきました。これでJWTによるセキュアな API ができました。

以上です。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
298