Go 言語用 Web フレームワーク "Gin" でログイン認証を実装

はじめに

前回までで、Gin フレークワームを使った本当に簡単なプログラムを組むことができました。

今回はログイン認証機構を実装していきます。

JSON Web Token とは?

JSON Web Token ( JWT ) は、RFC 7519 で定義された標準規格です。 JWT は、クライアントとサーバー間で情報を安全にやり取りするための仕組みです。 オブジェクトを使用して、クライアントとサーバー間で情報を暗号化し、署名し、またはその両方を行うことができます。

JWT についてはここまでにして本題に入りたいと思います。

作成する REST API について

3 つのエンドポイントを作成します。

  1. 「サインアップ」エンドポイント
  2. 「ログイン」エンドポイント
  3. 「ログイン後にしか呼び出しできない」エンドポイント

「サインアップ」エンドポイント

当然ながら、ログイン前にユーザーのサインアップ ( ユーザー登録 ) が必要です。 このパスは JWT を不要にし公開したままにします。

  • /api/register

「ログイン」エンドポイント

ログインに利用されるパスです。 「ユーザー名」と「パスワード」を受け取り、JWT を生成して返却します。

  • /api/ogin

「ログイン後にしか呼び出しできない」エンドポイント

このパスは JWT が必須となります。

  • /api/admin/user

作業開始 ( セットアップ )

まずはプロジェクトフォルダを作成します。

1
2
3
$ mkdir jwt-gin

$ cd jwt-gin

お約束の go.mod ファイルを作成します。 ここで作成するモジュール(アプリケーション)名は jwt-gin としました。

1
$ go mod init jwt-gin

必要なモジュールをインストールします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Gin フレームワーク
$ go get -u github.com/gin-gonic/gin

# ORM ライブラリ
$ go get -u github.com/jinzhu/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 ファイルを作ります。

1
$ vi main.go

最初のエンドポイントを作成します。

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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": "this is the register endpoint.",
		})
	})

	router.Run(":8080")
}

実行してみます。

1
$ go run main.go

ブラウザや curl でアクセスし、JSON が出力されれば成功です。

1
2
$ curl -X POST http://localhost:8080/api/register
{"data":"this is the register endpoint."}%

次にサインアップのメインロジックをコーディングしていきます。

「サインアップ」エンドポイント Controller 作成

登録処理を追加します。

Controller を格納するディレクトリ controllers を作成します。

1
$ mkdir ./controllers

auth.go というファイルを作成し、Controller の処理を実装します。

1
$ vi ./controllers/auth.go

auth.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package controllers

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func Register(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"data": "hello from the register endpoint.",
	})
}

作成した Register 関数が /register エンドポイントと結びつくよう main.go を変更します。

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
	"jwt-gin/controllers"

	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()

	public := router.Group("/api")

	public.POST("/register", controllers.Register)

	router.Run(":8080")
}

main.go が起動済みの場合は一度停止してから、起動します。

1
$ go run main.go

ブラウザや curl でアクセスし、JSON が出力されれば成功です。

1
2
$ curl -X POST http://localhost:8080/api/register
{"data":"hello from the register endpoint."}

Register 関数を更に充実させていきます。

「サインアップ」エンドポイント 入力チェック

サインアップには、ユーザー名とパスワードの情報だけが必要です。

binding と呼ばれる Gin の入力情報のチェック機能を利用します。

auth.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 run main.go を起動し直した後、動作確認します。

  1. ユーザー名だけ設定したリクエスト

    1
    2
    
    $ curl -X POST http://localhost:8080/api/register --data '{"username":"genzouw"}'
    {"error":"Key: 'RegisterInput.Password' Error:Field validation for 'Password' failed on the 'required' tag"}%
    
  2. パスワードだけ設定したリクエスト

    1
    2
    
    $ curl -X POST http://localhost:8080/api/register --data '{"password":"passwd"}'
    {"error":"Key: 'RegisterInput.Username' Error:Field validation for 'Username' failed on the 'required' tag"}%
    
  3. ユーザー名、パスワードの両方を設定したリクエスト

    1
    2
    
    $ curl -X POST http://localhost:8080/api/register --data '{"username":"genzouw", "password":"passwd"}'
    {"data":"validated!"}%
    

「サインアップ」エンドポイント DB 初期化 + Model 作成

認証情報を MySQL データベースに保存ようと思います。

既存の MySQL データベースがあればそちらを使っても構いません。 ここでは Docker を使って、MySQL を用意します。

1
2
# user=dev, password=dev, database=dev
$ 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

正しく起動すれば mysql コマンドで接続できるはずです。

1
$ mysql -h 127.0.0.1 -P 3307 -u dev -pdev dev

データベースへの接続処理と Model を格納するためのパッケージとコードを作成します。

1
2
3
$ mkdir -p ./models/

$ vi ./models/setup.go

./models/setup.go に以下のコードを記述します。

setup.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package models

import (
	"fmt"
	"log"
	"os"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"github.com/joho/godotenv"
)

var DB *gorm.DB

func ConnectDataBase() {
	err := godotenv.Load()

	if err != nil {
		log.Fatalf("Error loading .env file")
	}

	driver := os.Getenv("DB_DRIVER")
	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")

	dbURI := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", dbUser, dbPass, dbHost, dbPort, dbName)

	DB, err := gorm.Open(driver, dbURI)

	if err != nil {
		log.Fatal("Could not connect to the database", err)
	}

	DB.AutoMigrate(&User{})
}

以下の 2 つのものも必要となります。

  • .env
  • User 構造体

引き続き作成していきます。

プロジェクトのルートディレクトリに .env ファイルを作り、 MySQL サーバーへの接続情報を記述します。

1
2
3
4
5
6
7
8
$ cat <<EOF >>.env
DB_DRIVER=mysql
DB_USER=dev
DB_PASS=dev
DB_NAME=dev
DB_HOST=localhost
DB_PORT=3307
EOF

User Model を作成します。

1
$ vi ./models/user.go

user.go

1
2
3
4
5
6
7
8
9
package models

import "github.com/jinzhu/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 にデータベース接続処理を追記します。

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"jwt-gin/controllers"
	"jwt-gin/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")
}

ようやくデータベース接続処理の動作確認ができるところまで来ました。

main.go を立ち上げ直します。

1
2
3
4
5
# 立ち上げの前に、使用するパッケージが増えたため追加処理を行っておきます
$ go mod tidy

# 起動
$ go run main.go

MySQL データベース内のテーブルを一覧表示してみます。

1
2
3
4
5
6
7
$ mysql -h 127.0.0.1 -P 3307 -u dev -pdev dev -e "show tables;"
mysql: [Warning] Using a password on the command line interface can be insecure.
+---------------+
| Tables_in_dev |
+---------------+
| users         |
+---------------+

データベースのマイグレーション処理が正しく動作し users テーブルが追加されました。

「サインアップ」エンドポイント ユーザー情報登録

サインアップの実装の最終段階です。

users テーブルへのデータ登録ロジックを実装します。

user.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package models

import (
	"strings"

	"github.com/jinzhu/gorm"
	"golang.org/x/crypto/bcrypt"
)

type User struct {
	gorm.Model
	Username string `gorm:"size:255;not null;unique" json:"username"`
	Password string `gorm:"size:255;not null;" json:"password"`
}

func (u User) Save() (User, error) {
	err := DB.Create(&u).Error
	if err != nil {
		return User{}, err
	}
	return u, nil
}

func (u *User) BeforeSave() 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
}

User 構造体に追加したメソッドを呼び出し、データを保存します。

auth.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package controllers

import (
	"net/http"

	"jwt-gin/models"

	"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
	}

	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(),
	})
}

main.go を立ち上げ直します。

1
2
# 起動
$ go run main.go

再度、 curl などでリクエストを投げてみます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ curl -X POST http://localhost:8080/api/register --data '{"username":"genzouw", "password":"passwd"}'
{"data":{"ID":7,"CreatedAt":"2023-03-29T17:28:06.88854+09:00","UpdatedAt":"2023-03-29T17:28:06.88854+09:00","DeletedAt":null,"username":"genzouw","password":""}}%

$ mysql -h 127.0.0.1 -P 3307 -u dev -pdev dev -e "select * from users;"
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+---------------------+---------------------+------------+----------+--------------------------------------------------------------+
| id | created_at          | updated_at          | deleted_at | username | password                                                     |
+----+---------------------+---------------------+------------+----------+--------------------------------------------------------------+
|  7 | 2023-03-29 17:28:07 | 2023-03-29 17:28:07 | NULL       | genzouw  | $2a$10$G/ojbw02GTTGntOETLWzfekRmZaGfnVNLHSmnd8FFwTMY8tekqSte |
+----+---------------------+---------------------+------------+----------+--------------------------------------------------------------+

データが登録され、かつパスワードはハッシュ化されています。

「ログイン」エンドポイント 作成

ログイン用のエンドポイントはとてもシンプルです。

ユーザー名とパスワードを受け取り、データベース内の認証情報 ( users テーブル) と突き合わせを行います。

データが見つからなかった場合は、エラーレスポンスを返します。

main.go を編集します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
	"jwt-gin/controllers"
	"jwt-gin/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)

	router.Run(":8080")
}

controllers.Login メソッドの作成のために、 auth.go も編集します。

auth.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package controllers

import (
	"net/http"

	"jwt-gin/models"

	"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
	}

	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,
	})
}

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 ファイルに追記します。

user.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package models

import (
	"jwt-gin/utils/token"
	"strings"

	"github.com/jinzhu/gorm"
	"golang.org/x/crypto/bcrypt"
)

type User struct {
	gorm.Model
	Username string `gorm:"size:255;not null;unique" json:"username"`
	Password string `gorm:"size:255;not null;" json:"password"`
}

func (u User) Save() (User, error) {
	err := DB.Create(&u).Error

	if err != nil {
		return User{}, err
	}
	return u, nil
}

func (u *User) BeforeSave() 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 というファイルを新規に作成しこちらに含めます。

1
2
3
$ mkdir -p ./utils/token

$ vi ./utils/token/token.go

以下の3つの関数を外部から利用できるようにします。

  • GenerateToken()
  • TokenValid()
  • ExtractTokenId()

ただし、 TokenValid() 、 ExtractTokenId() の 2 つは後ほど利用します。

token.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package token

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
)

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
	}

	return nil
}

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
}

.env にコードに登場した環境変数を 2 つ追加します。

1
2
3
4
$ cat <<EOF >>.env
TOKEN_HOUR_LIFESPAN=1
API_SECRET=jwt-gin-dev
EOF

main.go を立ち上げ直します。

1
2
3
4
5
# 立ち上げの前に、使用するパッケージが増えたため追加処理を行っておきます
$ go mod tidy

# 起動
$ go run main.go

curl でいくつかリクエストを投げて動作を確認してみます。

  1. ユーザー名だけ正しい

    1
    2
    
    $ curl -X POST http://localhost:8080/api/login --data '{"username":"taro", "password":"passwd"}'
    {"error":"record not found"}%
    
  2. パスワードだけ正しい

    1
    2
    
    $ curl -X POST http://localhost:8080/api/login --data '{"username":"genzouw", "password":"pass"}'
    {"error":"crypto/bcrypt: hashedPassword is not the hash of the given password"}%
    
  3. ユーザー名、パスワードが正しい

    1
    2
    
    $ curl -X POST http://localhost:8080/api/login --data '{"username":"genzouw", "password":"passwd"}'
    {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2ODAwODgwNTIsInVzZXJfaWQiOjd9.lvqdlWCVxiCp0aPvQRnKUg0aKZFNbCfwR88r8v2vrec"}%
    

ログインに成功したときにトークンが生成されるようになりました。

認証用ミドルウェア ( Filter ) 作成

Gin にはミドルウェアと呼ばれる機能があります。 Controller にリクエストが到達する前に処理を行うことができます。

middlewares.go ファイルを新規に作成します。

1
2
3
$ mkdir -p ./middlewares/

$ vi ./middlewares/middlewares.go

middlewares.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package middlewares

import (
	"jwt-gin/utils/token"
	"net/http"

	"github.com/gin-gonic/gin"
)

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 の追加とこれに対するミドルウェアの設定を行います。

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"jwt-gin/controllers"
	"jwt-gin/middlewares"
	"jwt-gin/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")
	protected.Use(middlewares.JwtAuthMiddleware())
	protected.GET("/user", controllers.CurrentUser)

	router.Run(":8080")
}

auth.go に Controller となる CurrentUser 関数を追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package controllers

import (
	"net/http"

	"jwt-gin/models"
	"jwt-gin/utils/token"

	"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
	}

	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) {
	userId, err := token.ExtractTokenId(c)

	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
		return
	}

	var user models.User

	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(),
	})
}

main.go を立ち上げ直します。

1
2
# 起動
$ go run main.go

動作確認してみます。

まずは /api/login エンドポイントに対してログインを行い、トークンを取得します。

1
2
$ curl -X POST http://localhost:8080/api/login --data '{"username":"genzouw", "password":"passwd"}'
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2ODAwOTA2MzQsInVzZXJfaWQiOjd9.y1k0b3YqWjohqvuVyrQ7Bev5VQKiGMDkWP-K21YTstc"}

この操作でトークンが取得できたので、改めて jq コマンドを使ってパースしてみます。

1
2
$ curl --silent -X POST http://localhost:8080/api/login --data '{"username":"genzouw", "password":"passwd"}' | jq -rc .token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2ODAwOTA3NDksInVzZXJfaWQiOjd9.H6I_KkmFvSGZRhPfxZa3FpKsvS-UPEH8jdYtB2zUA2g

結果シェル変数に保存しておきます。

1
2
3
4
$ TOKEN=$(curl --silent -X POST http://localhost:8080/api/login --data '{"username":"genzouw", "password":"passwd"}' | jq -rc .token)

$ echo $TOKEN
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2ODAwOTA3ODUsInVzZXJfaWQiOjd9.0ptmj2BgGH54wIhJh9wvMQsMfyq5gWBwVRDWW8BN-WY

次に、/api/admin/user エンドポイントにリクエストしてみます。

まずはトークンを使わない場合。

1
2
$ curl --silent -X GET http://localhost:8080/api/admin/user
{"error":"token contains an invalid number of segments"}%

エラーとなりました。

今度は正しいトークンをヘッダーにセットしてリクエストしてみます。

1
2
$ curl --silent -X GET http://localhost:8080/api/admin/user -H "Authorization: Bearer ${TOKEN}"
{"data":{"ID":7,"CreatedAt":"2023-03-29T17:28:07+09:00","UpdatedAt":"2023-03-29T17:28:07+09:00","DeletedAt":null,"username":"genzouw","password":""}}

ひとこと

Controller をどういう単位でファイルに分割するとチーム開発しやすいのでしょうか?


参考ページ