ginでユーザーの認証を行う
はじめに
この記事ではユーザーの認証処理のうちのフロントエンド側からAPIをリクエストされた時にバックエンド側でどのようにしてフロントエンド側にいるユーザーを認証するのかということを説明します。
この認証ではjwtトークンを使用します。jwtトークン発行に関しての記事はこちら
目次
- そもそもjwtトークンって何?
- jwtトークンを使用したAPIリクエスト時のユーザーの認証プロセスの概要
- 実際のコード
そもそもjwtトークンって何?
JWTとは何か?
- jwtトークンとは一言で表すとセキュアな情報交換を実現するためのデジタルトークンです。
このjwtトークンを使用することでクライアント側で今どのユーザーがログインしているかの判断を行うことができたり、そのユーザーのAPIアクセスの認可を行うことができたりします。
また、jwtトークンは3つの部分から構成されます。- ヘッダー:使用するアルゴリズムとトークンのタイプ(通常はjwt)を含みます。
- ペイロード:トークンに含まれるクレーム(情報)を含みます。例えばユーザーIDや有効期限など。
- 署名:ヘッダーとペイロードを秘密鍵で署名したものです。これによってjwtトークン全体が暗号化されてトークンの改ざんを防げます。
実際のjwtトークンは以下のようになっています。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IlVzZXIxQGdtYWlsLmNvbSIsImV4cCI6MTczODEzOTI1Niwic3ViIjo4OX0.nllm4EckwZ8mOKlvxoo8aNb67PhYzlFFVbHoGXt3dDk
このトークンが先ほど伝えたヘッダーとペイロードと署名の3つの部分に分かれています。
jwtトークン発行のプロセスの概要
-
今回はjwtトークンを使用したAPIリクエスト時のユーザーの認証プロセスについてのみ解説します。本来はその前にjwtトークンを発行する必要があります。jwtトークン発行に関する記事はこちらから参照していただけます。
実際のコード
クライアント側からAPIのリクエストが送信される
- クライアント側からのAPI送信では主に下記のコードで処理を行います。
fetch('http://localhost:8080/Portfolio/getAllPosts', { credentials: 'include' })
.then(res => res.json())
.then(data => {
setPortfolio(data.portfolio);
})
.catch(err => console.error(err));
-
HttpリクエストにCookieを含める
- 以下のコードでHttpリクエストにCookieを含めています。デフォルトでHttpリクエストにCookieが含まれるわけではないのでこの記述が必要になります。
{ credentials: 'include' }
Cookieを取得する
- Cookieの復号化では主に以下の処理を行います。
- Cookieからjwtトークンを取得する
- 秘密鍵を用いてjwtトークンを復号化する
portfolioRouterWithAuth := r.Group("/Portfolio", middlewares.AuthMiddleware(authService))
portfolioRouterWithAuth.GET("/getAllPosts", portfolioController.GetAllPosts)
func AuthMiddleware(authService services.IAuthService) gin.HandlerFunc {
return func(ctx *gin.Context) {
tokenString, err := ctx.Cookie("jwt-token")
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// トークンの検証とユーザーの取得
user, err := authService.GetUserFromToken(tokenString)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
return
}
ctx.Set("user", user)
ctx.Next()
}
}
-
Cookieからjwtトークンを取得する
- まずはmain.goでリクエストが特定のルートにアクセスを行う前に認証を行います。
- 以下のコードでPortfolioより下のルータに対して認証処理を行います。
portfolioRouterWithAuth := r.Group("/Portfolio", middlewares.AuthMiddleware(authService))
そしてAuthMiddleware関数では以下のコードでCokkieに含まれているjwt-tokenを取り出しています。
tokenString, err := ctx.Cookie("jwt-token")
秘密鍵を用いてjwtトークンを復号化する
- 以下の2つの関数を使用してjwt-tokenの復号化を行います。
func AuthMiddleware(authService services.IAuthService) gin.HandlerFunc {
return func(ctx *gin.Context) {
tokenString, err := ctx.Cookie("jwt-token")
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// トークンの検証とユーザーの取得
user, err := authService.GetUserFromToken(tokenString)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
return
}
ctx.Set("user", user)
ctx.Next()
}
}
func (s *AuthService) GetUserFromToken(tokenString string) (*models.User, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("SECRET_KEY")), nil
})
if err != nil {
return nil, err
}
var user *models.User
if claims, ok := token.Claims.(jwt.MapClaims); ok {
if float64(time.Now().Unix()) > claims["exp"].(float64) {
return nil, jwt.ErrTokenExpired
}
user, err = s.repository.FindUserByEmail(claims["email"].(string))
if err != nil {
return nil, err
}
}
return user, nil
}
-
取得したjwt-tokenをGetUserFromToken関数に渡す
- 以下のコードで処理を行います。これによって取得したjwt-tokenからそのユーザーを割り出せます。
auth_middleware.gouser, err := authService.GetUserFromToken(tokenString)
-
jwt-tokenを秘密鍵で復号化する
- 以下のコードで復号化を行います。ここでは署名方法を確認して秘密鍵を取得しています。今回の場合、署名方式はSigningMethodHMACであり、秘密鍵はSECRET_KEYです。
auth_service.gotoken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(os.Getenv("SECRET_KEY")), nil })
秘密鍵は何でもよいですが一般的には文字列で複雑で長い文字列であることが推奨されます。
この時点ではまだ復号化をしていません。秘密鍵を取得しただけです。
復号化はjwt-tokenからメールアドレスを取得するときに同時に行います。
復号化されたjwt-tokenからメールアドレスを取得してユーザーを取得する
- この処理には以下の関数を使用します。(さっきのGetUserFromToken関数と同じもの)
func (s *AuthService) GetUserFromToken(tokenString string) (*models.User, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("SECRET_KEY")), nil
})
if err != nil {
return nil, err
}
var user *models.User
if claims, ok := token.Claims.(jwt.MapClaims); ok {
if float64(time.Now().Unix()) > claims["exp"].(float64) {
return nil, jwt.ErrTokenExpired
}
user, err = s.repository.FindUserByEmail(claims["email"].(string))
if err != nil {
return nil, err
}
}
return user, nil
}
-
jwt-tokenのペイロードを解析する
- 以下のコードで処理を行います。ペイロードとは簡単に言ってしまえば、jwt-tokenの実際のデータもことです。
- okがtrueの場合、トークンのペイロードが正しく取得されています。
if claims, ok := token.Claims.(jwt.MapClaims); ok {
-
有効期限の確認
- 以下のコードでペイロードの中から有効期限の値を取得して現在の時間と比較します。もし、有効期限が切れていたら処理を中断します。
if float64(time.Now().Unix()) > claims["exp"].(float64) {
return nil, jwt.ErrTokenExpired
}
-
ユーザーの取得
- 以下のコードでこの処理を行います。ペイロードの中からemailを取得してその取得したメールアドレスに基づいてデータベースの中からユーザーを検索して返します。
- ユーザーが見つからなかった場合やエラーが発生した場合は、そのエラーを返します。
user, err = s.repository.FindUserByEmail(claims["email"].(string))
if err != nil {
return nil, err
}
ctxにユーザー情報を保存して次の処理へつなげる
- この処理には以下の関数を使用します。(先ほど出てきたAuthMiddleware関数と同じ関数です)
func AuthMiddleware(authService services.IAuthService) gin.HandlerFunc {
return func(ctx *gin.Context) {
tokenString, err := ctx.Cookie("jwt-token")
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// トークンの検証とユーザーの取得
user, err := authService.GetUserFromToken(tokenString)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
return
}
ctx.Set("user", user)
ctx.Next()
}
}
-
コンテキストにユーザー情報を保存する
- 以下のコードでこの処理を行います。コンテキスト(ctx)に「user」というキーでユーザー情報(user)を保存します。
- 一度コンテキストに保存されたデータは、同じリクエストの他のミドルウェアやハンドラ関数でアクセスすることができます。これにより、リクエスト全体を通じてデータを簡単に共有できるようになります。
ctx.Set("user", user)
-
次の処理へつなげる
- 以下のコードでこの処理を行います。ctx.Next()が呼び出されると、現在のミドルウェアの処理が終了し、リクエストの処理が次のミドルウェアまたは最終的なルートハンドラに渡されます。
- 逆にこの処理がないと次のハンドラ関数へ移行できません。
auth_middleware.goctx.Next()
まとめ
今回はjwtトークンによるユーザーの認証に関しての記事でした。別の記事でjwt-tokenの発行方法に関してもまとめているので是非ご一読ください。