1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go+Gin+gorm】クリーンアーキテクチャでログイン機能を実装したAPI

Last updated at Posted at 2023-11-27

保険

この記事はGoを勉強して1ヶ月くらいの奴が書いてます。
また、クリーンアーキテクチャについてわかったつもりのやつが書いてます。
ご了承ください。

機能

  • ユーザー登録
  • ログイン
  • ログアウト

クリーンアーキテクチャ

image.png

ログイン処理の流れ

理解を簡単にするために、ユーザーがログインする際の実際の処理の流れについて考えてみます。

  1. ユーザーがエンドポイント/loginにPOSTリクエスト
  2. ルーターが/loginに対応するコントローラーを呼ぶ
  3. コントローラーがリクエストボディのjsonを構造体に変換してユースケースに渡す
  4. ユースケースが渡された構造体のEmailを引数にリポジトリを呼び出す
  5. リポジトリがDBにアクセスし、Emailが一致するレコードをSELECTし、構造体に変換してユースケースに渡す
  6. ユースケースがEmailPasswordが一致するか判定し、一致したらトークンを発行
  7. コントローラーがトークンをjsonに変換してレスポンスを返す

各レイヤーの役割

router

  • POST /signupでコントローラーのSignup関数を呼ぶ
  • POST /loginでコントローラーのLogin関数を呼ぶ
  • POST /logoutでコントローラーのLogout関数を呼ぶ

controller

  • Signup:リクエストのbodyのjsonを構造体に変換してユースケースのSignup関数に渡し、結果をjsonで返す
  • Login:リクエストのbodyのjsonを構造体に変換してユースケースのLogin関数に渡し、ログインに成功した場合はクッキーにログイン情報を保存
  • Logout:クッキーのログイン情報を削除

usecase

  • Signup:User構造体のパスワードをハッシュ化し、リポジトリのCreateUserに渡す
  • LoginEmailフィールドを引数としてリポジトリのGetByEmailを呼び出し、EmailPasswordを照合し、一致したらトークンを生成してコントローラーに返す

repository

  • CreateUser:DBにユーザー情報をINSERTする
  • GetByEmailEmailフィールドが一致するレコードをSELECTし構造体に変換してユースケースに返す

request / response

今回実装するAPIのリクエスト・レスポンスの例です。

POST /signup

example request
{
    "email": "hogehoge1@example.com"
    "password": "hoge1"
}
example response
{
    "ID": 1
    "email": "hogehoge1@example.com"
}

POST /login

example request
{
    "email": "hogehoge1@example.com"
    "password": "hoge1"
}
example response
{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDExMjc5MTgsInVzZXJfaWQiOjF9.Tp5tUkrLCNhxJnDc7Z0a4UfqCcsF22jEqbheeRn82E0"
}

実装

entity

ここではUser構造体を定義します。主キーのIDと、登録に必要なEmailPasswordです。

./entity/user.go
package entity

type User struct {
	ID       uint   `json:"id" gorm:"primaryKey"`
	Email    string `json:"email" gorm:"unique"`
	Password string `json:"password"`
}

Emailには重複がないようにunique制約をつけています。
ここで、一番内側のentity層にgormの情報が書かれているのは厳密には良くないのではと思いましたが、どうなんでしょう。

main

main.go
package main

import (
	"gin-login/controller"
	"gin-login/db"
	"gin-login/repository"
	"gin-login/router"
	"gin-login/usecase"
)

func main() {
	db := db.NewDB()
	userRepository := repository.NewUserRepository(db)
	userUsecase := usecase.NewUserUsecase(userRepository)
	userController := controller.NewUserController(userUsecase)
	r := router.NewRouter(userController)
	r.Run()
}

router

特にいうことはないです。

./router/router.go
package router

import (
	"gin-login/controller"

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

func NewRouter(uc controller.UserController) *gin.Engine {
	r := gin.Default()

	r.POST("/signup", uc.Signup)
	r.POST("/login", uc.Login)
	r.POST("/logout", uc.Logout)

	return r
}

controller

  • Signup
  • Login
  • Logout

の3つのメソッドを持つインターフェースを定義し、実装しています。
ログイン情報はCookieに保存しています。

./controller/user_controller.go
package controller

import (
	"gin-login/entity"
	"gin-login/usecase"
	"net/http"
	"time"

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

type UserController interface {
	Signup(c *gin.Context)
	Login(c *gin.Context)
	Logout(c *gin.Context)
}

type userController struct {
	uu usecase.UserUsecase
}

func NewUserController(uu usecase.UserUsecase) UserController {
	return &userController{uu}
}

func (uc *userController) Signup(c *gin.Context) {
	user := entity.User{}
	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, err)
	}

	err := uc.uu.Signup(user)
	if err != nil {
		c.JSON(http.StatusInternalServerError, err)
	}
	c.JSON(http.StatusOK, nil)
}

func (uc *userController) Login(c *gin.Context) {
	user := entity.User{}
	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, err)
	}

	tokenString, err := uc.uu.Login(user)
	if err != nil {
		c.JSON(http.StatusInternalServerError, err)
	}

	cookie := &http.Cookie{
		Name:     "token",
		Value:    tokenString,
		Expires:  time.Now().Add(1 * time.Hour),
		Path:     "/",
		Domain:   "localhost",
		Secure:   true,
		HttpOnly: true,
		SameSite: http.SameSiteNoneMode,
	}
	http.SetCookie(c.Writer, cookie)

	c.JSON(http.StatusOK, tokenString)
}

func (uc *userController) Logout(c *gin.Context) {
	cookie := &http.Cookie{
		Name:     "token",
		Value:    "",
		Expires:  time.Now(),
		Path:     "/",
		Domain:   "localhost",
		Secure:   true,
		HttpOnly: true,
		SameSite: http.SameSiteNoneMode,
	}

	http.SetCookie(c.Writer, cookie)
	c.JSON(http.StatusOK, nil)
}

usecase

  • Signup
  • Login

の2つのメソッドを持つインターフェースを定義し、実装しています。
ログインに成功した場合はトークンを生成します。

./usecase/user_usecase.go
package usecase

import (
	"gin-login/entity"
	"gin-login/repository"
	"os"
	"time"

	"github.com/golang-jwt/jwt/v4"
	"golang.org/x/crypto/bcrypt"
)

type UserUsecase interface {
	Signup(user entity.User) error
	Login(user entity.User) (string, error)
}

type userUsecase struct {
	ur repository.UserRepository
}

func NewUserUsecase(ur repository.UserRepository) UserUsecase {
	return &userUsecase{ur}
}

func (uu *userUsecase) Signup(user entity.User) error {
	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
	if err != nil {
		return err
	}
	newUser := entity.User{Email: user.Email, Password: string(hash)}
	if err := uu.ur.CreateUser(&newUser); err != nil {
		return err
	}
	return nil
}

func (uu *userUsecase) Login(user entity.User) (string, error) {
	storedUser := entity.User{}
	if err := uu.ur.GetUserByEmail(&storedUser, user.Email); err != nil {
		return "", err
	}
	err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(user.Password))
	if err != nil {
		return "", err
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"user_id": storedUser.ID,
		"exp":     time.Now().Add(time.Hour * 12).Unix(),
	})
	tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

repository

リポジトリは、DBへの読み書きを行います。
2つのメソッド

  • GetUserByEmail
  • CreateUser

を持つインターフェースを定義し、それらを実装していきます。

./repository/user_repository.go
package repository

import (
	"gin-login/entity"

	"gorm.io/gorm"
)

type UserRepository interface {
	GetUserByEmail(user *entity.User, email string) error
	CreateUser(user *entity.User) error
}

type userRepository struct {
	db *gorm.DB
}

func NewUserRepository(db *gorm.DB) UserRepository {
	return &userRepository{db}
}

func (ur *userRepository) GetUserByEmail(user *entity.User, email string) error {
	if err := ur.db.Where("email=?", email).First(user).Error; err != nil {
		return err
	}
	return nil
}

func (ur *userRepository) CreateUser(user *entity.User) error {
	if err := ur.db.Create(user).Error; err != nil {
		return err
	}
	return nil
}
  • GetUserByEmail(user *entity.User, email string) error

空のUser構造体へのポインタuserとstring型のemailを受け取り、合致するレコードを構造体に変換してuserへ格納します。Gormを使っているので構造体への変換は勝手にやってくれます。エラーが起きた場合はerrorをそのまま返します。

  • CreateUser(user *entity.User) error

ユーザーから入力されたEmailPasswordを持つUser構造体のポインタを受け取り、DBへINSERTします。エラーが起きた場合はerrorをそのまま返します。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?