期待するもの
新規登録の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
認証ができるようになった。