Go言語でログイン機能を実装してみた

# はじめに

Go言語で簡単なログイン機能(サインアップ、サインイン）を実装したときのお話
今回も要素検証だけですので、単純構成で作っています

# 検証環境

こちらの記事で構築した環境を利用しました。

https://qiita.com/sedori/items/840e39a0cbf9d5bff006

# ディレクトリ構成

```shell
go-gin-gorm
├── tmp
├── .air.toml
├── docker-compose.yml
├── Dockerfile
├── go.mod
├── go.sum
├── libraries
│       └ crypto.go
└── main.go
```

# 実行条件

```shell
Golang v1.22.3
Gin v1.10.0
GORM v1.25.10
MySQL 8.0.29
```

# ログイン機能の実装

~~サインインを確認するために実装した~~サインアップとサインインの実装例を以下で示します。

### パスワードの暗号化

暗号化にはbcryptを使用しています。
bcryptというのはパスワードのハッシュ化手法のひとつです。

https://pkg.go.dev/golang.org/x/crypto/bcrypt

プロジェクト以下に `libraries` ディレクトリを作成し、その配下に `crypto.go` を作成しましょう。

```golang:crypto.go
package crypto

import (
    "golang.org/x/crypto/bcrypt"
)

// 暗号化用
func PasswordEncrypt(password string) (string, error) {
    hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(hash), err
}

// パスワードの比較用
func CompareHashAndPassword(hash, password string) error {
    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
```

### アカウント作成用の処理

まずはログイン機能のひとつ、サインアップ機能を作ります。

```golang:main.go
package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "go_gin_gorm/libraries"
    "fmt"
)

type User struct {
    Id int
    UserId   string
    Password string
}

type JsonRequest struct {
    UserId  string `json:"user_id"`
    Password  string    `json:"password"`
}

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

    dsn := "root@tcp(mysql:3306)/first?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        fmt.Println("DB接続失敗");
        return
    }

    // Sign up用ルーティング
    engine.POST("/signup", func(c *gin.Context) {
        var json JsonRequest
        if err := c.ShouldBindJSON(&json); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error" : err.Error(),
            })
            return
        }
        userId := json.UserId
        pw := json.Password

        // 同一ユーザIDの検証
        user := User{}
        db.Where("user_id = ?", userId).First(&user)
        if user.Id != 0 {
            c.JSON(http.StatusBadRequest, gin.H{
                "message" : "そのUserIdは既に登録されています。",
            })
            return
        }

        // パスワードの暗号化
        encryptPw, err := crypto.PasswordEncrypt(pw)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{
                "message" : "パスワードの暗号化でエラーが発生しました。",
            })
            return
        }
        
        // DBへの登録
        user = User{UserId: userId, Password: encryptPw}
        db.Create(&user)
        
        c.JSON(http.StatusOK, gin.H{
            "message" : "アカウント登録完了",
        })
    })
    engine.Run(":8080")
}
```

`main.go` の内容を少し見ていきましょう

```golang
type JsonRequest struct {
    UserId  string `json:"user_id"`
    Password  string    `json:"password"`
}
```

リクエストボディにはjsonを採用したため、バインドするための構造体 `JsonRequest` を用意します。

https://gin-gonic.com/ja/docs/examples/binding-and-validation/

```golang
var json JsonRequest
if err := c.ShouldBindJSON(&json); err != nil {
    c.JSON(http.StatusBadRequest, gin.H{
        "error" : err.Error(),
    })
    return
}
userId := json.UserId
pw := json.Password
```

`ShouldBindJSON` を用いて構造体にリクエストをバインドします。
取り出す際には `構造体の変数名.構造体が持つキー名` で行います。

```
encryptPw, err := crypto.PasswordEncrypt(pw)
if err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{
        "message" : "パスワードの暗号化でエラーが発生しました。",
    })
    return
}
```

`crypto.go` に実装した `PasswordEncrypt` 関数を呼び出しています。
`crypto.go` を利用するためには、importに `"go_gin_gorm/libraries"` を追加します。

### サインイン用の処理

次はサインイン機能を実装します。

`main.go` を編集します。 `// Sign in用ルーティング` の処理を追加しましょう。

```golang:main.go

...

func main() {

...

    // Sign up用ルーティング
    engine.POST("/signup", func(c *gin.Context) {

...

    })

    // Sign in用ルーティング
    engine.POST("/signin", func(c *gin.Context) {
        var json JsonRequest
        if err := c.ShouldBindJSON(&json); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error" : err.Error(),
            })
            return
        }
        userId := json.UserId
        pw := json.Password

        user := User{}
        db.Where("user_id = ?", userId).First(&user)
        if user.Id == 0 {
            c.JSON(http.StatusUnauthorized, gin.H{
                "message" : "ユーザーが存在しません。",
            })
            return
        }

        err := crypto.CompareHashAndPassword(user.Password, pw)
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{
                "message" : "パスワードが一致しません。",
            })
            return
        }
        
        c.JSON(http.StatusOK, gin.H{
            "message" : "ログイン成功",
        })
    })

...

```

これも中身を見てみきましょう。基本的にはサインアップ用の処理と同じで、jsonから値を取得してデータベースと照合しています。
サインアップと異なるのは、暗号化して登録したパスワードと入力値を比較する必要があるということです。

```golang
err := crypto.CompareHashAndPassword(user.Password, pw)
if err != nil {
    c.JSON(http.StatusUnauthorized, gin.H{
        "message" : "パスワードが一致しません。",
    })
    return
}
```

`crypto.go` に実装した `CompareHashAndPassword` 関数を呼び出しています。
エラーがなければ照合成功ということになります。

### 動作確認

作成が完了したら実際に動作を確認します。
Docker開発環境が起動していることを確認してから、リクエストを送ってみましょう。


#### サインアップ

```shell:signup
curl -X POST "http://localhost:8080/signup" -H "Content-Type: application/json" -d '{"user_id": "login_user", "password": "Password_"}'
```

```json:result
{
    "message": "アカウント登録完了"
}
```

#### サインイン

```shell:signin
curl -X POST "http://localhost:8080/signin" -H "Content-Type: application/json" -d '{"user_id": "login_user", "password": "Password_"}'
```

```json:result
{
    "message": "ログイン成功"
}
```

# まとめ
いかがだったでしょうか？
今回はサインアップとサインインだけですが、ログイン状態の維持機能やログアウト機能を追加してみるのも良いかもしれません。