4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-02-28

概要

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

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

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

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

前提

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

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接続方法を詳しく知りたい方は下記を参照してください。

ユーザー新規登録の実装

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

モデルの構築

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

またORMgormを使用します。

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はハッシュ化して保存
  2. 新規登録時の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()
}

コンテナを起動

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

docker compose up

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

 [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コマンドで実際にリクエストを送ってみましょう。

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

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

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

まとめ

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

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

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

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?