はじめに
今回は,前回に続きユーザーの新規作成、ログイン、ログアウト機能を実装していきます。
※動作確認にPgAdminとPostmanを使います。
リポジトリの実装
User関係のリポジトリインターフェースを実装していきます。このコードは、ユーザー情報を扱うためのリポジトリインターフェース IUserRepository と、そのインターフェースを実装する具体的なリポジトリ構造体 userRepository を定義しています。
NewUserRepository 関数は、userRepository 構造体のインスタンスを作成するために使用されます。
repository/user_repository.go
package repository
import (
"go-rest-api/model"
"gorm.io/gorm"
)
type IUserRepository interface {
//このメソッドは、emailアドレスで指定されたユーザーの情報を取得します。
//ユーザー情報をmodel.Userのポインタに格納します。
GetUserByEmail(user *model.User, email string) error
//このメソッドは、新しいユーザーを作成します。
//user *model.Userは新規に作成するユーザーの情報を格納するmodel.User構造体のポインタです。
CreateUser(user *model.User) error
}
type userRepository struct {
//このフィールドは、データベース接続 (`gorm.DB` オブジェクト) を保持します。
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) IUserRepository {
//新しい `userRepository` インスタンスを作成し、データベース接続を `db` フィールドに設定して返します。
return &userRepository{db}
}
func (ur *userRepository) GetUserByEmail(user *model.User, email string) error {
//gorm.DB の Where、First、Error メソッドを使って、メールアドレスでユーザーを取得します。
if err := ur.db.Where("email=?", email).First(user).Error; err != nil {
return err
}
return nil
}
func (ur *userRepository) CreateUser(user *model.User) error {
//gorm.DB の Create メソッドを使って、新しいユーザーを作成します。
if err := ur.db.Create(user).Error; err != nil {
return err
}
return nil
}
ユースケースの実装
次はユースケースを作成していきます。下記のコードはユーザー登録(サインアップ)とログイン処理を行うユースケースを実装したものです。
usecase/user_usecase.go
package usecase
import (
"go-rest-api/model"
"go-rest-api/repository"
"os"
"time"
//go mod tidyコマンドを実行する
"github.com/golang-jwt/jwt/v4"
"golang.org/x/crypto/bcrypt"
)
//IUserUsecase: ユーザー関連のユースケースが実装すべきメソッドを定義しています。
type IUserUsecase interface {
//ユーザー登録処理を実行するメソッドです。
SignUp(user model.User) (model.UserResponse, error)
//ユーザーログイン処理を実行するメソッドです。
Login(user model.User) (string, error)
}
//ユーザーリポジトリへの依存関係を保持するフィールドです。
type userUsecase struct {
ur repository.IUserRepository
}
//userUsecase 構造体のインスタンスを生成する関数です。
func NewUserUsecase(ur repository.IUserRepository) IUserUsecase {
return &userUsecase{ur}
}
//ユーザー登録処理を実行するメソッドです。
func (uu *userUsecase) SignUp(user model.User) (model.UserResponse, error) {
//パスワードをハッシュ化して、データベースに保存します。
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
//ハッシュ化処理中にエラーが発生した場合、エラーを返します。
if err != nil {
return model.UserResponse{}, err
}
//ハッシュ化されたパスワードとメールアドレスを使って、新しいユーザーの情報を格納する model.User 構造体を作成します。
newUser := model.User{Email: user.Email, Password: string(hash)}
//ユーザーリポジトリの CreateUser メソッドを呼び出して、新しいユーザーを作成します。
if err := uu.ur.CreateUser(&newUser); err != nil {
return model.UserResponse{}, err
}
//登録されたユーザーのIDとメールアドレスを使って、レスポンス構造体 model.UserResponse を作成します。
resUser := model.UserResponse{
ID: newUser.ID,
Email: newUser.Email,
}
//登録されたユーザーの情報を含む model.UserResponse とエラーオブジェクト nil を返します。
return resUser, nil
}
//ログイン処理を実行するメソッドです。
func (uu *userUsecase) Login(user model.User) (string, error) {
//データベースから取得したユーザー情報を格納する model.User 構造体を空で作成します。
storedUser := model.User{}
//ユーザーリポジトリの GetUserByEmail メソッドを呼び出して、メールアドレスでユーザーを取得します。
if err := uu.ur.GetUserByEmail(&storedUser, user.Email); err != nil {
return "", err
}
//bcrypt パッケージの CompareHashAndPassword 関数を使って、入力されたパスワードとデータベースに保存されたハッシュ化されたパスワードを比較します。
//storedUser.Password は、データベースに保存されたハッシュ化されたパスワードです。
//user.Password は、ユーザーがログイン時に入力したプレーンテキストのパスワードです。
err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(user.Password))
if err != nil {
return "", err
}
//jwt パッケージの NewWithClaims 関数を使って、JWT トークンを作成します。
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": storedUser.ID,
//jwtトークンの有効期限を12時間に設定
"exp": time.Now().Add(time.Hour * 12).Unix(),
})
//NewWithClaimsの返り値に対してSignedStringというメソッドを実行することによってjwtトークンを生成する
tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
if err != nil {
return "", err
}
return tokenString, nil
}
コントローラの実装
下記のコードは、ユーザーのサインアップ、ログイン、ログアウト処理を行うためのコントローラを定義しています。controller/user_controller.go
package controller
import (
"go-rest-api/model"
"go-rest-api/usecase"
"net/http"
"os"
"time"
//go mod tidyコマンドを実行してパッケージをダウンロードしておく
"github.com/labstack/echo/v4"
)
//ユーザーコントローラが実装すべきメソッドを定義するインターフェースです。
type IUserController interface {
//ユーザー登録処理を実行するメソッドです。
SignUp(c echo.Context) error
//ユーザーログイン処理を実行するメソッドです。
Login(c echo.Context) error
//ユーザーログアウト処理を実行するメソッドです。
LogOut(c echo.Context) error
}
type userController struct {
//ユーザーユースケースへの依存関係を保持するフィールドです。
uu usecase.IUserUsecase
}
//ユーザーユースケースオブジェクトを受け取り、userController構造体のインスタンスを生成して返します。
func NewUserController(uu usecase.IUserUsecase) IUserController {
return &userController{uu}
}
//ユーザー登録処理を実行するメソッドの実装です。
func (uc *userController) SignUp(c echo.Context) error {
//ユーザーの情報を格納する model.User 構造体を空で作成します。
user := model.User{}
//リクエストからユーザー情報を取得する処理です。
//リクエストからユーザー情報を取得して、user 構造体にバインドして格納します。
//バインド処理中にエラーが発生した場合、エラーを返します。
//エラーが発生した場合、HTTPステータスコード 400 (Bad Request) を返します。
if err := c.Bind(&user); err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
//ユーザーユースケースの SignUp メソッドを呼び出して、ユーザー登録処理を実行します。
//uu フィールドは、ユーザーユースケースへのポインタです。
//SignUp メソッドは、ユーザー登録処理を実行します。
userRes, err := uc.uu.SignUp(user)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}
//ユーザー登録処理が成功した場合、userRes 変数には、ユーザーの情報を格納する
//ユーザー登録処理が成功した場合、HTTPステータスコード 201 (Created) と、ユーザーの情報を含む model.UserResponse オブジェクトを JSON形式で返します。
//model.UserResponse 構造体が格納されます。
return c.JSON(http.StatusCreated, userRes)
}
//ログイン処理を実行するメソッドの実装です。
func (uc *userController) Login(c echo.Context) error {
//ユーザーの情報を格納する model.User 構造体を空で作成します。
user := model.User{}
//リクエストからユーザー情報を取得する処理です。
//c.Bind(&user) は、リクエストからユーザー情報を取得して、user 構造体にバインドします。
//if err != nil { ... } は、バインド処理中にエラーが発生した場合、エラーを返します。
//エラーが発生した場合、HTTPステータスコード 400 (Bad Request) を返します。
if err := c.Bind(&user); err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
//ユーザーユースケースの Login メソッドを呼び出して、ユーザーログイン処理を実行します。
//ログイン処理が成功した場合、tokenString 変数には、JWTトークンが格納されます。
tokenString, err := uc.uu.Login(user)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}
//新しいクッキーを作成します。
cookie := new(http.Cookie)
//クッキーの名前を "token" に設定します。
cookie.Name = "token"
//クッキーの値を、生成されたJWTトークンに設定します。
cookie.Value = tokenString
//クッキーの有効期限を24時間後に設定します。
cookie.Expires = time.Now().Add(24 * time.Hour)
//クッキーのパスを "/" に設定します。
cookie.Path = "/"
//クッキーのドメインを、環境変数 API_DOMAIN から取得した値に設定します。
cookie.Domain = os.Getenv("API_DOMAIN")
//クッキーをHTTPのみで使用できるように設定します。
cookie.HttpOnly = true
// クッキーを SameSiteNoneMode に設定します。
//http.SameSiteNoneMode は、クッキーがすべてのサイトからアクセスできることを示します。これは、CSRF攻撃に対する保護が弱いため、注意が必要です。
cookie.SameSite = http.SameSiteNoneMode
//クッキーをレスポンスに設定して、クライアントに送信します。
c.SetCookie(cookie)
//HTTPステータスコード 200 (OK) を返します。
return c.NoContent(http.StatusOK)
}
//ログアウト処理を実行するメソッドの実装です。
func (uc *userController) LogOut(c echo.Context) error {
//新しいクッキーを作成します。
cookie := new(http.Cookie)
//クッキーの名前を "token" に設定します。
cookie.Name = "token"
//クッキーの値を空文字列に設定します。
cookie.Value = ""
//クッキーの有効期限を現在時刻に設定します。これにより、クッキーはすぐに期限切れになり、削除されます。
cookie.Expires = time.Now()
//クッキーのパスを "/" に設定します。これは、クッキーがアプリケーションのすべてのページで使用できることを示します。
cookie.Path = "/"
//クッキーのドメインを、環境変数 API_DOMAIN から取得した値に設定します。
cookie.Domain = os.Getenv("API_DOMAIN")
//クッキーをHTTPのみで使用できるように設定します。これにより、JavaScriptからクッキーにアクセスできないように、セキュリティを強化します。
cookie.HttpOnly = true
//クッキーを SameSiteNoneMode に設定します。これは、クッキーがすべてのサイトからアクセスできることを示します。
cookie.SameSite = http.SameSiteNoneMode
//クッキーをレスポンスに設定して、クライアントに送信します。これにより、クライアントは、このクッキーを次回のアクセス時にサーバーに送信します。
c.SetCookie(cookie)
//HTTPステータスコード 200 (OK) を返します。
return c.NoContent(http.StatusOK)
}
routerの実装
このコードは、NewRouter 関数を呼び出すことで、REST APIのルーティング設定を行います。NewRouter 関数は、ユーザーコントローラーのインスタンス uc を引数として受け取ります。
この関数は、Echoフレームワークのインスタンスを作成し、signup, login, logout エンドポイントへのリクエストをそれぞれユーザーコントローラーの SignUp, Login, LogOut メソッドにマッピングします。
router/router.go
package router
import (
"go-rest-api/controller"
//go mod tidyコマンドを実行してパッケージをダウンロードしておく
"github.com/labstack/echo/v4"
)
func NewRouter(uc controller.IUserController) *echo.Echo {
//Echoフレームワークの新しいインスタンスを作成します。
e := echo.New()
// `POST` メソッドで `/signup` エンドポイントにアクセスしたときに、`uc.SignUp` 関数を呼び出すように設定します。
e.POST("/signup", uc.SignUp)
//`POST` メソッドで `/login` エンドポイントにアクセスしたときに、`uc.Login` 関数を呼び出すように設定します。
e.POST("/login", uc.Login)
//`POST` メソッドで `/logout` エンドポイントにアクセスしたときに、`uc.LogOut` 関数を呼び出すように設定します。
e.POST("/logout", uc.LogOut)
//設定されたルーティング設定を持つ Echoフレームワークのインスタンスを返します。
return e
}
mainの実装
このコードは、データベース接続、ユーザーリポジトリ、ユーザーユースケース、ユーザーコントローラー、ルーティング設定などのコンポーネントを構築し、REST APIサーバーを起動します。main.go
package main
import (
"go-rest-api/controller"
"go-rest-api/db"
"go-rest-api/repository"
"go-rest-api/router"
"go-rest-api/usecase"
)
func main() {
//`db` パッケージの `NewDB` 関数を呼び出して、データベースとの接続を作成します。
db := db.NewDB()
//`repository` パッケージの `NewUserRepository` 関数を呼び出して、ユーザーリポジトリの新しいインスタンスを作成します。
userRepository := repository.NewUserRepository(db)
//`usecase` パッケージの `NewUserUsecase` 関数を呼び出して、ユーザーユースケースの新しいインスタンスを作成します。
userUsecase := usecase.NewUserUsecase(userRepository)
//controller パッケージの NewUserController 関数を呼び出して、ユーザーコントローラの新しいインスタンスを作成します。
userController := controller.NewUserController(userUsecase)
//`router` パッケージの `NewRouter` 関数を呼び出して、REST APIのルーティング設定を行います。
e := router.NewRouter(userController)
//Echoフレームワークの `Start` メソッドを呼び出して、ポート 8080 でREST APIサーバーを起動します。
e.Logger.Fatal(e.Start(":8080"))
}
プログラムを起動
下記のコマンドでプログラムを起動していきます。GO_ENV=dev go run main.go
次はPostmanを使って動作確認します。
http://localhost:8080/signup
のエンドポイントを入力します。
メソッドはPOSTを選択し、Bodyのところはrawを選択し、JSONを選択します。
emailとpasswordを入力しSendボタンを押下します。
成功しますと201 Createdのステータス、生成されたユーザーのidとemailが返ってきます。
PgAdminを起動していきます。
Dockerで立ち上げてるPostgressのデータベースの中に作成したユーザー情報が登録されています。(usersテーブルを見ています)
パスワードはハッシュ化されたパスワードが登録されています。
エンドポイントをhttp://localhost:8080/login
に変更してSendボタンを押下します。
200 OKのステータスが返ってきて、CookiesタブにCookieが設定されます。
エンドポイントをhttp://localhost:8080/logout
に変更してSendボタンを押下します。
200 OKのステータスが返ってきて、Cookieが削除されます。
まとめ
今回はユーザーの新規追加、ログイン、ログアウト機能を実装していきました。次回はタスク関係の処理を実装していきます。