Ginを利用して新規登録、ログイン機能をJWT認証で実装（新規登録編）

# 概要

今回は`Go`のフレームワークである`Gin`を利用して、自分なりに`JWT認証`の実装を行なってみたので

アウトプットとして共有します。

ただひとつの記事で記載すると分量が多くなるので、**3回**に分けて記載していきたいと思います。

まず**新規登録機能**の実装を行います。

# 前提

開発環境はDockerを利用します。

```Dockerfile
FROM golang:1.20-alpine3.16
ENV ROOT /app
    TZ Asia/Tokyo
WORKDIR ${ROOT}
RUN apk update && apk add --no-cache git
COPY go.mod go.sum ./
RUN go mod download
COPY . .
EXPOSE 8080
CMD ["go", "run", "./..."]
```

```docker-compose.yml
version: '3.8'
services:
  db:
    image: mysql:8.0
    container_name: mysql
    env_file:
      - .env
    environment:
      MYSQL_ROOT_PASSWORD: $DB_ROOT_PASSWORD
      MYSQL_DATABASE: $DB_NAME
      MYSQL_USER: $DB_USER
      MYSQL_PASSWORD: $DB_PASSWORD
    volumes:
      - ./mysql:/var/lib/mysql
    ports:
      - 3306:3306

  api:
    build: .
    container_name: api
    working_dir: /app
    env_file:
      - .env
    environment:
      TZ: "Asia/Tokyo"
    volumes:
      - .:/app
    ports:
      - 8080:8080
    depends_on:
      - db
```

各環境変数は適宜`.env`ファイル等で設定してください。

また今回は**MVCアーキテクチャ**で実装していきます。

各バージョンは下記

|package|version|
|---|--|
|github.com/gin-gonic/gin|v1.8.2|
|github.com/go-ozzo/ozzo-validation|v3.6.0|
|gorm.io/gorm|v1.24.5|
|gorm.io/driver/mysql|v1.4.6|

**モジュールパス**は簡易的に`app`で定義します。

# データベースのセットアップ

データベースのセットアップは下記のように記載しました。

```pkg/database/database.go
package database

import (
    "fmt"
    "os"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var db *gorm.DB

func SetUpDB() (err error) {
    connectInfo := fmt.Sprintf(
        "%s:%stcp(db)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        os.Getenv("DB_USER"),
        os.Getenv("DB_PASSWORD"),
        os.Getenv("DB_NAME"),
    )
    db, err = gorm.Open(mysql.Open(connectInfo), &gorm.Config{})
    if err != nil {
        return err
    }

    return nil
}

func GetDB() *gorm.DB {
    return db
}
```

データベースとやりとりを行う際に使用する変数`db`は関数（`GetDB()`）から値を返すことで

パッケージ（ここではdatabaseパッケージ）外から直接変数（`db`）にアクセスされるのを防ぐ工夫をしています。

そうすることによって不用意に値が変更されるのを防いでいます。

gormを使用したDB接続方法を詳しく知りたい方は下記を参照してください。

https://qiita.com/soicchi/items/4ad65a2a5f536f81d3fb

# ユーザー新規登録の実装

まずは新規登録処理の実装を行なっていきます。

## モデルの構築

まずはモデルにロジックを記載していきます。

また**ORM**に`gorm`を使用します。

```pkg/models/user.go
package models

import (
    "crypto/sha256"
    "fmt"

    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    Name string `gorm:"size:255;not null"`
    Email string `gorm:"size:255;not null"`
    Password string `gorm:"size:255;not null"`
}

type SignUpInput struct {
    Name string `json:"name" binding:"required"`
    Email string `json:"name" binding:"required"`
    Password string `json:"name" binding:"required"`
}

func Encrypt(char string) string {
    encryptText := fmt.Sprintf("%x", sha256.Sum256([]byte(char)))
    return encryptText
}

func (user *User) Create(db *gorm.DB) (User, error) {
    user := User{
        Name: user.Name,
        Email: user.Email,
        Password: Encrypt(user.Password)
    }
    result := db.Create(&user)

    return user, result.Error
}
```

ポイントは下記2点です。
1. **Passwordはハッシュ化して保存**
1. **新規登録時のinput用の構造体の定義**

最初の頃なぜわざわざ`SignUpInput`のような構造体を作成するのか疑問でした。

定義する理由として`Gin`には**送られてきたパラメータを構造体にバインドできる機能**があり、

簡単にリクエストのボディに格納されたデータを取得できるためです。

コントローラー側でcontextから`context.PostForm("パラメーター名")`で各データは取得できますが、

構造体を利用してバインドした方がシンプルなのでこちらを採用しました。

## バリデーションの追加

今回バリデーションの実装は`ozzo-validation`を利用します。

`go-playground/validator`を利用して構造体にタグを追加することで

バリデーションを実装することもできますが、

個人的にタグ部分が煩雑になってしまうのと、バリデーション部分は分離して管理した方が

保守性が良いのではないかと思い`ozzo-validation`を利用しました。

```pkg/models/user.go
package models

import (
    "crypto/sha256"
    "fmt"

    "github.com/go-ozzo/ozzo-validation" // 追加
    "github.com/go-ozzo/ozzo-validation/is" // 追加
    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    Name string `gorm:"size:255;not null"`
    Email string `gorm:"size:255;not null"`
    Password string `gorm:"size:255;not null"`
}

type SignUpInput struct {
    Name string `json:"name" binding:"required"`
    Email string `json:"name" binding:"required"`
    Password string `json:"name" binding:"required"`
}

func Encrypt(char string) string {
    encryptText := fmt.Sprintf("%x", sha256.Sum256([]byte(char)))
    return encryptText
}

func (user *User) Create(db *gorm.DB) (User, error) {
    user := User{
        Name: user.Name,
        Email: user.Email,
        Password: Encrypt(user.Password)
    }
    result := db.Create(&user)

    return user, result.Error
}

// 下記追加した部分
func (user *User) Validate() error {
    err := validation.ValidateStruct(user
        validation.Field(&user.Name,
            validation.Required.Error("Name is requred"),
            validation.Length(1, 255).Error("Name is too long"),
        ),
        validation.Field(&user.Email
            validation.Required.Error("Email is required"),
            is.Email.Error("Email is invalid format"),
        ),
        validation.Field(&user.Password
            validation.Required.Error("Password is required"),
            validation.Length(8, 255).Error("Password is less than 7 chars or more than 256 chars"),
        )
    )
    return err
}
```

## コントローラーの構築

コントローラーの実装は下記のように記載しました。

```pkg/controllers/auth.go
package controllers

import (
    "net/http"

    "github.com/gin-gonic/gin"

    "app/models"
)

type Handler struct {
    DB *gorm.DB
}

func (handler *Handler) SignUpHandler(context *gin.Context) {
    var signUpInput models.SignUpInput
    err := context.ShouldBind(&signUpInput)
    if err != nil {
        // 本来ログ等でerrは出力した方がよいが今回は省略
        context.JSON(http.StatusBadRequest, gin.H{
            "message": "Invalid request body",
        })
        return
    }

    newUser := &models.User{
        Name: signUpInput.Name,
        Email: signUpInput.Email,
        Password: signUpInput.Password,
    }
    
    err = newUser.Validate()
    if err != nil {
        context.JSON(http.StatusBadRequest, gin.H{
            "message": err.Error(),
        })
        return
    }

    user, err := newUser.Create(handler.DB)
    if err != nil {
        context.JSON(http.StatusBadRequest, gin.H{
            "message": "Failed to create user",
        })
        return
    }

    context.JSON(http.StatusOK, gin.H{
        "user_id": user.ID,
        "message": "Successfully created user",
    })
}
```

**※本来各エラー内容はログに出力するべきだが、今回は割愛**

最終的に何を返すかは仕様により適宜決めてください。

# routerの設定

routerの設定は下記です。

```pkg/router/auth.go
package router

import (
    "github.com/gin-gonic/gin"

    "app/controllers"
)

func addAuthRouter(rg *gin.RouterGroup, h *controllers.Handler) {
    auth := rg.group("/auth")
    {
        auth.POST("/signup", handler.SignUpHandler)
    }
}
```

```pkg/router/router.go
package router

import (
    "github.com/gin-gonic/gin"

    "app/controllers"
    "app/database"
)

func Run() {
    router := setupRouter()
    router.Run()
}

func setupRouter() *gin.Engine {
    router := gin.Default()
    handler := &controllers.Handler{
        DB: database.GetDB(),
    }

    api := router.group("/api")
    v1 := api.group("/v1")
    addAuthRouter(v1, handler)

    return router
}
```

`router`の設定を単一のファイルにまとめると将来的に煩雑になりそうだったので、

各グループごと（**今回はauth部分のrouter**）ファイルを作成し、できるだけ分割して管理するようにしています。

# main.go

最後にエントリーポイントとなるmain.goを記載していきます。

```cmd/app/main.go
package main

import (
	"github.com/soicchi/chatapp_backend/pkg/database"
	"github.com/soicchi/chatapp_backend/pkg/router"
)

func main() {
	err = database.SetupDB()
	if err != nil {
		panic(err)
	}

	router.Run()
}

```

# コンテナを起動

下記コマンドを実行してコンテナを立ててみましょう。

```shell
docker compose up
```

すると下記のように立ち上がったと思います。

```shell
 [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
api    | 
api    | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
api    |  - using env:	export GIN_MODE=release
api    |  - using code:	gin.SetMode(gin.ReleaseMode)
api    | 
api    | [GIN-debug] POST   /api/v1/auth/signup       --> app/pkg/controllers.(*Handler).SignUpHandler-fm (3 handlers)
api    | [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
api    | Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
api    | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
api    | [GIN-debug] Listening and serving HTTP on :8080
```

curlコマンドで実際にリクエストを送ってみましょう。

```shell
curl -X POST -H "Content-Type: application/json" http://127.0.0.1:8080/api/v1/auth/signup -d '{"name":"sample-test", "email":"sample@sample.com", "password":"Test1234"}'
```

下記のようにレスポンスが返ってくれば成功です。

```shell
{"message":"Successfully created user", "user_id":18}
```

# まとめ

JWT認証機能の構築として今回は、まず新規登録機能を実装してみました。

あと個人的に`Go`を使う場合の変数の定義は１文字で定義するのではなく、わかりやすい命名をつけた方がいいのではと思っています。

次回はログイン機能を実装していきたいと思います。

最後まで読んでいただいてありがとうございました！

https://qiita.com/soicchi/items/56a0e900ca1000cee3e7