🔞

Go言語でJWT認証をやってみた!

2024/01/12に公開

読んでほしい人

  • JWT認証なるものに興味がある人
  • Go言語で試しに作ってみたい!

補足情報

docker-composeを使用して、私はローカル環境でPostgreSQLを使っています。

こちらの記事を参考にしてください。
https://zenn.dev/joo_hashi/articles/3702238384488f

JWTとは何か?

公式サイト
JSON Web Token(JWT)とは:特殊な形式の認証トークン

JWTを製品に使用することで、二者間で安全な通信を実現できます。 データはデジタル署名で検証され、HTTP経由で送信される場合は暗号化によりデータの安全性が保持されます

記事の内容

データベースに、usersテーブルを作成しますが、JWT TOKENはDBには保存しません。

トークンは、一般的にはクライアントサイド(例えば、ユーザーのブラウザ)で保存されます。これは、トークンがユーザーの認証状態を表すため、ユーザーがログイン状態を維持するために必要な情報だからです。トークンは通常、ブラウザのローカルストレージやセッションストレージ、またはクッキーに保存されます。

サーバーサイドでトークンを保存する必要がないのは、トークン自体がユーザーの認証情報を含んでいるためです。例えば、JWT(JSON Web Token)の場合、トークンはユーザーのIDや有効期限などの情報を含んでおり、これらの情報はトークン自体に署名されています。したがって、サーバーはトークンを受け取るたびにその署名を検証し、トークンが改ざんされていないこと、そして有効期限が切れていないことを確認します。これにより、サーバーはデータベースにアクセスすることなくユーザーの認証状態を確認できます。

ただし、一部の認証システムでは、サーバーサイドでトークンを追跡するためにトークンを保存することがあります。これは、例えば、トークンを無効化するために使用されます。しかし、これはステートフルな認証システムであり、ステートレスな認証システム(例えば、JWT)とは異なります。

という仕組みあるそうです😅
難しいですね〜。認証とか認可のお話になると難しいお話になってきて、疲れます💦

環境構築

  1. 雛形を作る
go mod init jwt-auth
  1. main.goに新規登録とログインのfunctionを作成
package main

import (
	"log"

	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
	"gorm.io/driver/postgres"
	"github.com/joho/godotenv"
	_ "github.com/lib/pq"
	"golang.org/x/crypto/bcrypt"

	"net/http"
	"os"
	"time"
)

type User struct {
	gorm.Model
	Username string `gorm:"type:varchar(100);unique_index" json:"username"`
	Password string `gorm:"type:varchar(100)" json:"password"`
}

var DB *gorm.DB

func main() {
	godotenv.Load()
    var err error
    dsn := os.Getenv("DATABASE_URL")
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("failed to connect database: %v", err)
    }

    sqlDB, err := DB.DB()
    if err != nil {
        log.Fatalf("failed to get database: %v", err)
    }
    defer sqlDB.Close()

    DB.AutoMigrate(&User{})

    r := gin.Default()

    r.POST("/register", register)
    r.POST("/login", login)

    r.Run()
}

func register(c *gin.Context) {
	var user User
	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Error while hashing password"})
		return
	}
	user.Password = string(hashedPassword)

	if err := DB.Save(&user).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Error while registering user"})
		return
	}
	c.JSON(http.StatusOK, user)
}

func login(c *gin.Context) {
	var user User
	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	var foundUser User
	if err := DB.Where("username = ?", user.Username).First(&foundUser).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Error while logging in"})
		return
	}

	if err := bcrypt.CompareHashAndPassword([]byte(foundUser.Password), []byte(user.Password)); err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"})
		return
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"username": user.Username,
		"exp":      time.Now().Add(time.Hour * 24).Unix(),
	})

	jwtSecret := os.Getenv("JWT_SECRET")
	if jwtSecret == "" {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "JWT Secret not found"})
		return
	}

	tokenString, err := token.SignedString([]byte(jwtSecret))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Error while generating token"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"token": tokenString})
}
  1. PostgreSQLが起動している状態で、ローカルサーバーを起動する
go run main.go

新規登録をする

新規登録のURLにPOSTしてデータベースにハッシュ化したパスワードとユーザー名が登録されていればOK

PostgreSQLのusersテーブルの中身

postgres=# select * from users;
 id |          created_at           |          updated_at           | deleted_at | username |                           password  
                         
----+-------------------------------+-------------------------------+------------+----------+-------------------------------------
-------------------------
  1 | 2024-01-12 02:10:21.253184+00 | 2024-01-12 02:10:21.253184+00 |            | Jboy422  | $2a$10$p9BiApJj/pAHx42enMeLSOTHxOCzS
BwevPmmvFatamK8USB.XTwjG
(1 row)

ログインをする

エンドポイントにHTTPリクエストを送って、JWT TOKENが表示されれば認証が成功していることになります。

最後に

今回は、データーベースとJWTのパッケージを使って、Go言語でJWT認証をやってみました!
ご興味ある方は、是非是非やってみてください。本当はもっと複雑なロジックになるんでしょうね。リモートのDBを使いたかったのですが、接続できなくて、ローカルで使うことにしました😇

今回使用したソースコードはこちらにあります
https://github.com/sakurakotubaki/GoJWT

Discussion