概要
今回は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", "./..."]
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
で定義します。
データベースのセットアップ
データベースのセットアップは下記のように記載しました。
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接続方法を詳しく知りたい方は下記を参照してください。
ユーザー新規登録の実装
まずは新規登録処理の実装を行なっていきます。
モデルの構築
まずはモデルにロジックを記載していきます。
またORMにgorm
を使用します。
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点です。
- Passwordはハッシュ化して保存
- 新規登録時のinput用の構造体の定義
最初の頃なぜわざわざSignUpInput
のような構造体を作成するのか疑問でした。
定義する理由としてGin
には送られてきたパラメータを構造体にバインドできる機能があり、
簡単にリクエストのボディに格納されたデータを取得できるためです。
コントローラー側でcontextからcontext.PostForm("パラメーター名")
で各データは取得できますが、
構造体を利用してバインドした方がシンプルなのでこちらを採用しました。
バリデーションの追加
今回バリデーションの実装はozzo-validation
を利用します。
go-playground/validator
を利用して構造体にタグを追加することで
バリデーションを実装することもできますが、
個人的にタグ部分が煩雑になってしまうのと、バリデーション部分は分離して管理した方が
保守性が良いのではないかと思いozzo-validation
を利用しました。
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
}
コントローラーの構築
コントローラーの実装は下記のように記載しました。
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の設定は下記です。
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)
}
}
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を記載していきます。
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文字で定義するのではなく、わかりやすい命名をつけた方がいいのではと思っています。
次回はログイン機能を実装していきたいと思います。
最後まで読んでいただいてありがとうございました!