期待するもの

新規登録のAPIを作成したので、次はログイン機能を作成したい。

ログインは、メールアドレスとパスワードを送り一致したら、tokenを取得する。

で、tokenを使って認証付きのAPIを叩けるようになりたい。

tokenにはjwtを使用する。

jwtってなに?

これはどこかで調べてまとめる(気が向いた時)。

JWT認証を作成する

パッケージのインストール

github.com/dgrijalva/jwt-goパッケージの情報が多かったので、それに従うことにする。長い物には巻かれるべきで間違いない。

go get github.com/dgrijalva/jwt-go

パッケージをインストールする。

Tokenを生成する

まずjwt.goというファイルを作成する。

今回はauthフォルダの中に作成した。

それでは書いていく。

まずは、jwtを生成するためのシークレットキーを作成する。

var jwtKey = []byte("your_secret_key")

ここでは任意の文字列(今回は"your_secret_key")をバイト列に変換し、jwtKeyに代入している。

バイト列というのがピンとこないので、

fmt.Printlnで書き出しみると、[121 111 117 114 95 115 101 99 114 101 116 95 107 101 121]となった。なんとなく理解した。

このようにして文字列をバイト列に変換することは、多くの場面でデータの操作や処理に利用されます。JWTの署名などのセキュリティ関連の操作では、シークレットキーをバイト列として扱うことが一般的です。

とのことでした。

次に、JWTのクレームを表す構造体であるClaimsを定義する。

これは任意のJSONデータっていう意味っぽい。

type Claims struct {
	Name string `json:"name"`
	jwt.StandardClaims
}

今回はnameを含めることにする。

と思ったけど、ユニークで必要がありそうなので、ユーザーIDにした。

下記のように変更する。

type Claims struct {
	UserID uint `json:"user_id"`
	jwt.StandardClaims
}

次に、Tokenを作成する関数を作成する。

コードの詳細はコメントアウトに書いた。

func generateToken(userId uint) (string, error) {

	// トークンの有効期限を設定(この場合は15分)
	expirationTime := time.Now().Add(15 * time.Minute)

	// トークンのClaimsを構築
	claims := &Claims{
		UserID: userId, // ユーザーIDをトークンに含める
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: expirationTime.Unix(), // トークンの有効期限を設定
		},
	}

	// 新しいJWTトークンを作成
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// トークンを署名して文字列に変換
	tokenString, err := token.SignedString(jwtKey)
	if err != nil {
		return "", err
	}

	// : JWTトークンを文字列として表現したもの&エラーが発生しなかったことを示すための値を返却する
	return tokenString, nil
}

これでTokenを作成することができた。

ログイン時にこのTokenを返却することにする。

Tokenを認証する

次に認証を行う関数を作成する。

func Authenticate(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Authorizationヘッダーからトークン文字列を取得します。
		tokenString := r.Header.Get("Authorization")

		// トークンが存在しない場合は、Unauthorizedエラーを返して処理を中断します。
		if tokenString == "" {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		// トークン文字列から"Bearer "を削除します。
		tokenString = strings.Replace(tokenString, "Bearer ", "", 1)

		// JWTトークンをパースし、クレーム(Claims)を含むトークンオブジェクトを取得します。
		token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
			return jwtKey, nil
		})

		// トークンのパースに失敗した場合は、Unauthorizedエラーを返して処理を中断します。
		if err != nil {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		// トークンのクレームを取得し、有効であるかを検証します。
		claims, ok := token.Claims.(*Claims)
		if !ok || !token.Valid {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		// 認証されたユーザーの情報をログに出力します。
		fmt.Printf("Authenticated user: %s\n", strconv.Itoa(int(claims.UserID)))

		// 次のハンドラ関数を呼び出します。
		next(w, r)
	}
}

ちなみにこの関数の関数名が大文字始まりなのは下記の通り。

Go言語では、関数や変数が大文字で始まる場合には他のパッケージから参照可能(エクスポート可能)となりますが、小文字で始まる場合には同じパッケージ内からのみアクセス可能となります。

で、このコントローラーを下記のように噛ませる。

	http.HandleFunc("/users", auth.Authenticate(FetchUsers))

これで認証が必要になった。

実際に叩くときは

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo4LCJleHAiOjE3MDMwODUxNzR9.bOJ6jYFLr3BneqtEhQdFb9b2y3lAubm3MpqXbzVQzQY

こんな感じでヘッダーを持たせる必要がある。

これでJWT認証ができるようになった。