はじめに
今回は、Go言語のWebフレームワーク「Gin」を用いてログイン認証を実装していこうと思います。
本記事では、エンドポイントの構築、JWT(JSON Web Token)による認証、MySQLデータベースとの連携方法について詳しく説明します。
まずは基本的なセットアップから始め、順を追って実装していきましょう!
環境
・MacBook Air M1
使用技術
- VScode
- Go
- バージョンは1.23.2を使用
-
Gin
- Go言語のWebフレームワーク
- MySQL
- 認証情報を MySQL データベースに保存しようと思います
-
mysql
コマンドが使える状態にしておいてください
- Docker
- Dockerを使ってMySQLを用意します
-
docker
コマンドが使える状態にしておいてください
-
Postman
- エンドポイントの検証に使用します
手順
始める前に、作成するAPIについて簡単に触れておきます。
1. 「サインアップ」エンドポイント
-
/api/register
- ユーザー登録のパス
- JWTを不要にし公開したままにする
2. 「ログイン」エンドポイント
-
/api/login
- ログインに利用するパス
- 「ユーザー名」と「パスワード」を受け取り、JWT を生成して返却
3. 「ログイン後にしか呼び出しできない」エンドポイント
-
/api/admin/user
- JWT が必須
この3つのエンドポイントを作成していきます。
それでは始めてきましょう!
セットアップ
# 作業用のディレクトリを作成(go-authで進めていきます)
$ mkdir go-auth
# 作成したディレクトリに移動後、VScodeを起動
$ cd go-auth
$ code .
まずはGoモジュールの初期化を行うために、以下のコマンドを叩いてください。
# 作成するモジュール名は「go-auth」でいきます
$ go mod init go-auth
次に必要なモジュールのインストールを行います。
# Gin フレームワーク
$ go get -u github.com/gin-gonic/gin
# ORM ライブラリ
$ go get -u gorm.io/gorm
# JWT の認証と生成に使用するパッケージ
$ go get -u github.com/dgrijalva/jwt-go
# .env ファイルから環境変数を読み込む
$ go get -u github.com/joho/godotenv
# パスワード暗号化
$ go get -u golang.org/x/crypto
インストールが終えたら、main関数を作成し最初のエンドポイントを作っていきます!
コーディング開始
main関数の作成
main.go
ファイルを作成したら、以下のコードを記述してください。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
public := router.Group("/api")
public.POST("/register", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": "register endpoint",
})
})
router.Run(":8080")
}
では、以下のコマンドをVScode上のターミナルで叩いて、実行してみましょう。
$ go run main.go
# 「Listening and serving HTTP on :8080」と出力されればOKです
ブラウザや、curl
コマンドでアクセスしてJSONが返ってきたらOKです。
$ curl -X POST http://localhost:8080/api/register
{"data":"register endpoint"}%
「サインアップ」エンドポイントの作成
登録処理の追加を行うため、controllers
ディレクトリを作成します。
この中にController処理を実装するためにauth.go
ファイルを作成してください。
それではauth.go
ファイルにコードを書いていきましょう。
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
)
func Register(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": "register endpoint.",
})
}
次に、Register関数が/register
エンドポイントと結びつくように、main.go
ファイルを修正していきます。
package main
import (
// controllersパッケージのインポート
"go-auth/controllers"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
public := router.Group("/api")
// Register関数の呼び出し
public.POST("/register", controllers.Register)
router.Run(":8080")
}
再度、Goを起動させ、curlコマンドあるいはブラウザでJSONを出力されるか確認しましょう。
$ go run main.go
$ curl -X POST http://localhost:8080/api/register
{"data":"register endpoint."}%
「サインアップ」エンドポイントのバリデーションチェック
今回は、サインアップに「ユーザー名」+「パスワード」のみ入力します。
その入力情報をチェックするためにGinのbindingというチェック機能を使用します。
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
)
type RegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context) {
var input RegisterInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": "validated!",
})
}
それではgoを起動させ、動作するか確認してきます。
まずは、「パスワード」のみの入力で検証していきましょう。
この検証をするにあたって Postman
を使用します。
入力内容は以下の通りです。
- メソッドを
POST
を指定し、http://localhost:8080/api/register
とする -
Body
のrow
にチェックを入れ、JSON
を選択しましょう - 以下のJSONが返ってくるか確認してください
次はユーザー名のみも同様に{"username":"<ユーザー名>"}だけを入れて、エラーがJSONで返ってくるか確かめます。
最後に「ユーザー名」と「パスワード」の両方を入れましょう。
画像の通りのJSONが返ってきたらOKです!
認証情報を MySQL データベースに保存
Dockerを起動させ、MySQLを用意します。
以下のコマンドを叩いてDockerを起動させましょう。
$ docker run --rm -p 3307:3306 -e MYSQL_ROOT_PASSWORD=dev -e MYSQL_USER=dev -e MYSQL_PASSWORD=dev -e MYSQL_DATABASE=dev -d mysql/mysql-server:5.7
簡単にコマンドの説明をすると、
-
docker run
:Dockerコンテナの実行を - 環境変数の設定は以下の通りです
-
-e MYSQL_USER=dev
:devというユーザーの作成 -
-e MYSQL_PASSWORD=dev
:devユーザーのパスワードをdevに設定 -
-e MYSQL_DATABASE=dev
:DB名をdevに設定 -
-d
:バックグラウンドでコンテナが実行されるため、ターミナルを占有しません
-
起動させた後は、MySQLに接続していきます。以下のコマンドで接続を確認してください。
$ mysql -h 127.0.0.1 -P 3307 -u dev -pdev dev
データベースへの接続処理と Model を格納するためのパッケージとコードを作成します。
それでは、models
ディレクトリを作成し、その中にsetup.go
ファイルを作成してコードを書いていきましょう。
package models
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDataBase() {
err := godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file")
}
dbUser := os.Getenv("DB_USER")
dbPass := os.Getenv("DB_PASS")
dbName := os.Getenv("DB_NAME")
dbHost := os.Getenv("DB_HOST")
dbPort := os.Getenv("DB_PORT")
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbUser, dbPass, dbHost, dbPort, dbName)
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Could not connect to the database", err)
}
// ここで適切なエンティティに対してマイグレーションを実行します。
DB.AutoMigrate(&User{})
}
また、.env
ファイルとUser
構造体の二つを用意する必要があるので作成していきましょう。
ルートディレクトリに.env
ファイルを作成し以下のコードを書いてください。
DB_DRIVER=mysql
DB_USER=dev
DB_PASS=dev
DB_NAME=dev
DB_HOST=localhost
DB_PORT=3307
次に、models
ディレクトリにuser.go
ファイルを作成しUser
Modelを作成しましょう。
package models
import "gorm.io/gorm"
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"password"`
}
また、main.go
ファイルにDB接続する処理を追加していきます。
package main
import (
"go-auth/controllers"
"go-auth/models"
"github.com/gin-gonic/gin"
)
func main() {
models.ConnectDataBase()
router := gin.Default()
public := router.Group("/api")
public.POST("/register", controllers.Register)
router.Run(":8080")
}
処理がかけたのでGoを起動させましょう。
その前に、go mod tidy
を叩いて依存関係の整理をしておきましょう。
go mod tidy
はGoモジュールの依存関係を整えるためのコマンドです。
依存関係を追加した後や削除した後には、適宜このコマンドを実行しましょう!
# 使用するパッケージの追加処理
$ go mod tidy
$ go run main.go
起動できたら、以下のコマンドでMySQLに接続し、usersテーブルを表示させてみましょう。
$ mysql -h 127.0.0.1 -P 3307 -u dev -pdev dev
mysql> show tables;
+---------------+
| Tables_in_dev |
+---------------+
| users |
+---------------+
DBのマイグレーション処理が正しく動作し users
テーブルが追加されたのことが確認できます。
ユーザー情報の登録
サインアップ実装の最終段階をしていきましょう。
users
テーブルへの登録ロジックを実装していきます。
package models
import (
"strings"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"password"`
}
// User オブジェクトをデータベースに保存する
func (u *User) Save() (*User, error) {
err := DB.Create(u).Error
if err != nil {
return nil, err
}
return u, nil
}
// Userオブジェクトが保存される前に実行する
func (u *User) BeforeSave(*gorm.DB) error {
// パスワードをハッシュ化する
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
// ユーザーネームを小文字に変換する
u.Username = strings.ToLower(u.Username)
return nil
}
// パスワードを空文字にして出力準備をする
func (u *User) PrepareOutput() *User {
u.Password = ""
return u
}
auth.go
ファイルも修正していきます。
package controllers
import (
"go-auth/models"
"net/http"
"github.com/gin-gonic/gin"
)
type RegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context) {
var input RegisterInput
// リクエストのJSONデータをRegisterInput構造体にバインドする
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// ユーザーオブジェクトを作成し、データベースに保存する
user := &models.User{Username: input.Username, Password: input.Password}
user, err := user.Save()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": user.PrepareOutput(),
})
}
Saveメソッドも戻り値を正しく受け取るために、user
変数をポインタ型 (*models.User)
に変更しました。
Goを起動させ、検証していきましょう!
$ go run main.go
Postman
を使用してPOSTリクエストを送ってみましょう。以下の画像の通りに進めてください。
ステータスコード200
が返ってきたことを確認した後、MySQLに接続しusers
テーブルにデータが追加されているか確認していきます。
# MySQLに接続
$ mysql -h 127.0.0.1 -P 3307 -u dev -pdev dev
# SELECT文でusersテーブルのデータを確認
mysql> select * from users;
+----+-------------------------+-------------------------+------------+----------+--------------------------------------------------------------+
| id | created_at | updated_at | deleted_at | username | password |
+----+-------------------------+-------------------------+------------+----------+--------------------------------------------------------------+
| 4 | 2024-11-07 21:33:43.172 | 2024-11-07 21:33:43.172 | NULL | fujifuji | $2a$10$IZpxktFgsKDLyDOkK/m1te8.DLAPh/XuFcBf5BO0ci4XRgKbJ.Ak6 |
+----+-------------------------+-------------------------+------------+----------+--------------------------------------------------------------+
データが登録され、パスワードがハッシュ化されていればOKです!
「ログイン」エンドポイントの作成
それでは、ログイン用のエンドポイントを作成していきましょう。
「ユーザー名」と「パスワード」を受け取り、DBにあるusersテーブルと照合を行う処理を書いていきます。
main.go
ファイルから編集していきます。
package main
import (
"go-auth/controllers"
"go-auth/models"
"github.com/gin-gonic/gin"
)
func main() {
models.ConnectDataBase()
router := gin.Default()
public := router.Group("/api")
public.POST("/register", controllers.Register)
// /loginエンドポイントの追加
public.POST("/login", controllers.Login)
router.Run(":8080")
}
auth.go
ファイルにcontrollers.Login
メソッドの作成をしていきます。
package controllers
import (
"go-auth/models"
"net/http"
"github.com/gin-gonic/gin"
)
type RegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context) {
var input RegisterInput
// リクエストのJSONデータをRegisterInput構造体にバインドする
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// ユーザーオブジェクトを作成し、データベースに保存する
user := &models.User{Username: input.Username, Password: input.Password}
user, err := user.Save()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 成功した場合、ユーザー情報をレスポンスとして返す
c.JSON(http.StatusOK, gin.H{
"data": user.PrepareOutput(),
})
}
type LoginInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Login(c *gin.Context) {
var input LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
token, err := models.GenerateToken(input.Username, input.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
})
}
Controller
であるLogin
関数から呼び出している GenerateToken
関数を user.go
ファイルに追記します。
package models
import (
"go-auth/utils/token"
"strings"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"password"`
}
// User オブジェクトをデータベースに保存する
func (u *User) Save() (*User, error) {
err := DB.Create(u).Error
if err != nil {
return nil, err
}
return u, nil
}
// Userオブジェクトが保存される前に実行する
func (u *User) BeforeSave(*gorm.DB) error {
// パスワードをハッシュ化する
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
// ユーザーネームを小文字に変換する
u.Username = strings.ToLower(u.Username)
return nil
}
// パスワードを空文字にして出力準備をする
func (u *User) PrepareOutput() *User {
u.Password = ""
return u
}
func GenerateToken(username string, password string) (string, error) {
var user User
err := DB.Where("username = ?", username).First(&user).Error
if err != nil {
return "", err
}
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if err != nil {
return "", err
}
token, err := token.GenerateToken(user.ID)
if err != nil {
return "", err
}
return token, nil
}
token.go
ファイルを作成し、このファイルにトークンの操作する関数を作成していきます。
$ mkdir -p ./utils/token
$ touch ./utils/token/token.go
以下の3つの関数を外部から呼び出せるようにします。
-
GenerateToken
関数 -
TokenValid
関数 -
ExtractTokenId
関数
TokenValid
関数と ExtractTokenId
関数の2つに関しては後ほど利用します。
それではtoken.go
ファイルを編集していきましょう。
package token
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
)
// 指定されたユーザーIDに基づいてJWTトークンを生成する
func GenerateToken(id uint) (string, error) {
tokenLifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))
if err != nil {
return "", err
}
claims := jwt.MapClaims{}
claims["authorized"] = true
claims["user_id"] = id
claims["exp"] = time.Now().Add(time.Hour * time.Duration(tokenLifespan)).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(os.Getenv("API_SECRET")))
}
func extractTokenString(c *gin.Context) string {
bearToken := c.Request.Header.Get("Authorization")
strArr := strings.Split(bearToken, " ")
if len(strArr) == 2 {
return strArr[1]
}
return ""
}
func parseToken(tokenString string) (*jwt.Token, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("There was an error")
}
return []byte(os.Getenv("API_SECRET")), nil
})
if err != nil {
return nil, err
}
return token, nil
}
// トークンが有効かどうかを検証
func TokenValid(c *gin.Context) error {
tokenString := extractTokenString(c)
token, err := parseToken(tokenString)
if err != nil {
return err
}
if !token.Valid {
return fmt.Errorf("Invalid token")
}
return nil
}
// トークンからユーザーIDを取得
func ExtractTokenId(c *gin.Context) (uint, error) {
tokenString := extractTokenString(c)
token, err := parseToken(tokenString)
if err != nil {
return 0, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if ok && token.Valid {
userId, ok := claims["user_id"].(float64)
if !ok {
return 0, nil
}
return uint(userId), nil
}
return 0, nil
}
そして、コードに登場したTOKEN_HOUR_LIFESPAN
とAPI_SECRET
の2つの環境変数を.env
ファイルに追加していきます。
DB_DRIVER=mysql
DB_USER=dev
DB_PASS=dev
DB_NAME=dev
DB_HOST=localhost
DB_PORT=3307
// トークンの有効期限
TOKEN_HOUR_LIFESPAN=1
// JSONの署名に使用する秘密鍵
API_SECRET=go-auth-dev
では、Goを起動させてみましょう!
# 使用するパッケージの追加処理(忘れないように👍)
$ go mod tidy
$ go run main.go
Postman
でリクエストを投げて動作確認していきましょう。
まずは、ユーザー名のみ正しいパターン
以下のJSONが返ってくるか確認しましょう。
"error": "crypto/bcrypt: hashedPassword is not the hash of the given password"
"error": "record not found"
ラストに、ユーザー名・パスワードともに正しいパターン
ログインに成功した際に、トークンが生成されるようになりました。
認証用ミドルウェアの作成
Ginにはミドルウェアという機能があり、これを使用してController
にリクエストが到達する前に処理を行うようにしていきます。
middlerware
ディレクトリを作成し、その中にmiddlerwares.go
ファイルを作成し、以下のように書いてください。
package middlewares
import (
"go-auth/utils/token"
"net/http"
"github.com/gin-gonic/gin"
)
// JwtAuthMiddleware はJWT認証を行うミドルウェアを返します
func JwtAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := token.TokenValid(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
c.Abort()
return
}
c.Next()
}
}
main.go
ファイルに新しい/api/admin/user
エンドポイントの追加とこれに対するミドルウェアの設定を行います。
package main
import (
"go-auth/controllers"
middlewares "go-auth/middlerwares"
"go-auth/models"
"github.com/gin-gonic/gin"
)
func main() {
models.ConnectDataBase()
router := gin.Default()
public := router.Group("/api")
public.POST("/register", controllers.Register)
public.POST("/login", controllers.Login)
protected := router.Group("/api/admin")
// JWT認証ミドルウェアを適用
protected.Use(middlewares.JwtAuthMiddleware())
// 認証されたユーザー情報を取得するルートを定義
protected.GET("/user", controllers.CurrentUser)
router.Run(":8080")
}
そして新たに追加したCurrentUser
関数をauth.go
ファイルに書いていきます。
package controllers
import (
"go-auth/models"
"go-auth/utils/token"
"net/http"
"github.com/gin-gonic/gin"
)
type RegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context) {
var input RegisterInput
// リクエストのJSONデータをRegisterInput構造体にバインドする
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// ユーザーオブジェクトを作成し、データベースに保存する
user := &models.User{Username: input.Username, Password: input.Password}
user, err := user.Save()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 成功した場合、ユーザー情報をレスポンスとして返す
c.JSON(http.StatusOK, gin.H{
"data": user.PrepareOutput(),
})
}
type LoginInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Login(c *gin.Context) {
var input LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
token, err := models.GenerateToken(input.Username, input.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
})
}
func CurrentUser(c *gin.Context) {
// トークンからユーザーIDを抽出する
userId, err := token.ExtractTokenId(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
var user models.User
// ユーザーIDに基づいてユーザー情報をデータベースから取得する
err = models.DB.First(&user, userId).Error
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": user.PrepareOutput(),
})
}
それでは、Go起動させて検証していきましょう!
ますは/api/login
エンドポイントに対してログインを行い、トークンを取得していきます。
取得したトークンは後で使用するのでPostman
で別タブを開いておきましょう。
次に、/api/admin/user
エドポイントにGETリクエストを送ってみましょう。
まずはトークンを使用しないパターンを検証していきます。Postmanの設定は以下の画像の通りです。
- メソッドを
GET
にする -
Headers
タブを開いてKey
にAuthorization
、Value
は空で大丈夫です
エラーが返ってきたのでOKです!
今度はValue
にトークンを設定してリクエストを送ってみましょう。
-
Value
にBearer <トークン>
を入力する-
Bearer
の後は半角スペースを入れてください
-
さいごに
今回はGo言語のGinを使い、簡単なログイン認証システムを作成しました。
エンドポイントの構築やJWTによる認証、MySQLデータベースとの連携方法を通して、Webアプリケーションの基本的な認証の仕組みについて学べたら幸いです。
参考