Go言語でJWT認証をやってみた!
読んでほしい人
- JWT認証なるものに興味がある人
- Go言語で試しに作ってみたい!
補足情報
docker-composeを使用して、私はローカル環境でPostgreSQLを使っています。
こちらの記事を参考にしてください。
JWTとは何か?
公式サイト
JSON Web Token(JWT)とは:特殊な形式の認証トークン
JWTを製品に使用することで、二者間で安全な通信を実現できます。 データはデジタル署名で検証され、HTTP経由で送信される場合は暗号化によりデータの安全性が保持されます
記事の内容
データベースに、users
テーブルを作成しますが、JWT TOKENはDBには保存しません。
トークンは、一般的にはクライアントサイド(例えば、ユーザーのブラウザ)で保存されます。これは、トークンがユーザーの認証状態を表すため、ユーザーがログイン状態を維持するために必要な情報だからです。トークンは通常、ブラウザのローカルストレージやセッションストレージ、またはクッキーに保存されます。
サーバーサイドでトークンを保存する必要がないのは、トークン自体がユーザーの認証情報を含んでいるためです。例えば、JWT(JSON Web Token)の場合、トークンはユーザーのIDや有効期限などの情報を含んでおり、これらの情報はトークン自体に署名されています。したがって、サーバーはトークンを受け取るたびにその署名を検証し、トークンが改ざんされていないこと、そして有効期限が切れていないことを確認します。これにより、サーバーはデータベースにアクセスすることなくユーザーの認証状態を確認できます。
ただし、一部の認証システムでは、サーバーサイドでトークンを追跡するためにトークンを保存することがあります。これは、例えば、トークンを無効化するために使用されます。しかし、これはステートフルな認証システムであり、ステートレスな認証システム(例えば、JWT)とは異なります。
という仕組みあるそうです😅
難しいですね〜。認証とか認可のお話になると難しいお話になってきて、疲れます💦
環境構築
- 雛形を作る
go mod init jwt-auth
- 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})
}
- 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を使いたかったのですが、接続できなくて、ローカルで使うことにしました😇
今回使用したソースコードはこちらにあります
Discussion