4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RCC (立命館コンピュータークラブ)Advent Calendar 2024

Day 10

Go言語のGinフレームワークでセッション機能を作る: 自動SignInとSignOut編

Posted at

はじめに

本記事は以下の記事で実装したSignUp機能を持つAPIに自動SignInとSignOutの機能を追加するものになります。

実装する機能

1. サインイン
2. 自動サインイン (セッション情報の取得)
3. サインアウト (セッションの無効化)

コードの変更

前回の記事にて作成したAPIに変更を加えていきます。

セッション管理の拡張

infrastructure/session/session_manager.goに新しいメソッドを追加します。

  • GetUserID(): セッションからユーザーIDを取得
  • InvalidateSession(): セッションを無効化
package session
// 省略

type ISessionManager interface {
	CreateSession(w http.ResponseWriter, r *http.Request, userID uint) error
     
     // ↓↓追加部分(2行)
	GetUserID(r *http.Request) (uint, bool)
	InvalidateSession(w http.ResponseWriter, r *http.Request) error
}

// 省略

// 以下全て追加
func (sm *SessionManager) GetUserID(r *http.Request) (uint, bool) {
	session, err := sm.store.Get(r, "session-name")
	if err != nil {
		return 0, false
	}

	userID, ok := session.Values["user_id"].(uint)
	return userID, ok
}

func (sm *SessionManager) InvalidateSession(w http.ResponseWriter, r *http.Request) error {
	session, err := sm.store.Get(r, "session-name")
	if err != nil {
		return err
	}

	// セッションを無効化
	session.Options.MaxAge = -1
	return session.Save(r, w)
}

ハンドラーの追加

interface/handler/user_handler.goに新しいメソッドを実装:

  • SignIn(): ログイン処理
  • SignOut(): ログアウト処理
  • GetCurrentUser(): 現在のユーザー情報を取得
type (
	SignUpRequest = application.CreateUserInput

     // ↓↓↓追加部分(1行)
	SignInRequest = application.SignInUserInput
)

type IUserHandler interface {
	SignUp(ctx *gin.Context)

     // ↓↓↓追加部分(3行)
	SignIn(ctx *gin.Context)
	SignOut(ctx *gin.Context)
	GetCurrentUser(ctx *gin.Context)
}

// 省略
// ログイン処理
func (h *UserHandler) SignIn(ctx *gin.Context) {
	var request SignInRequest
	if err := ctx.ShouldBindJSON(&request); err != nil {
		ctx.JSON(400, gin.H{"error": err.Error()})
	}

	id, err := h.userUsecase.SignIn(ctx, request)
	if err != nil {
		ctx.JSON(500, gin.H{"error": err.Error()})
	}

	if err := h.sessionManager.CreateSession(ctx.Writer, ctx.Request, id); err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Session creation failed"})
		return
	}
}

// ログアウト処理
func (h *UserHandler) SignOut(ctx *gin.Context) {
	if err := h.sessionManager.InvalidateSession(ctx.Writer, ctx.Request); err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Logout failed"})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"message": "sign out successful"})
}

// 現在のユーザー情報を取得
func (h *UserHandler) GetCurrentUser(ctx *gin.Context) {
	userID, exists := h.sessionManager.GetUserID(ctx.Request)
	if !exists {
		ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
		return
	}

	user, err := h.userUsecase.FindByID(ctx, userID)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "User not found"})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"id":    user.ID,
		"name":  user.Username,
		"email": user.Email,
	})
}

ルーティングの更新

main.goに新しいエンドポイントを追加

func main() {
	// 省略

	// ルーティング
	router := gin.Default()
	router.POST("/api/user/signup", userHandler.SignUp)

    // ↓↓↓↓追加部分(3行)
	router.POST("/api/user/signin", userHandler.SignIn)
	router.POST("/api/user/signout", userHandler.SignOut)
	router.GET("/api/user/me", userHandler.GetCurrentUser)

	log.Fatal(http.ListenAndServe(":8080", router))
}

その他: DB操作部分

ユースケースの追加

  • SignIn(): EmailとPasswordを使って認証
  • FindByID(): IDを使ってuserデータを取り出す
package application

// 省略

// 追加部分(type2つ)
type SignInUserInput struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

type UserOutput struct {
	ID       uint   `json:"id"`
	Username string `json:"username"`
	Email    string `json:"email"`
}

type IUserUsecase interface {
	Create(ctx context.Context, input CreateUserInput) (uint, error)

     // 追加部分(2行)
	SignIn(ctx context.Context, input SignInUserInput) (uint, error)
	FindByID(ctx context.Context, id uint) (*UserOutput, error)
}

// 省略

// 以下を追加
func (u *UserUsecase) FindByID(ctx context.Context, id uint) (*UserOutput, error) {
	user, err := u.userRepo.FindByID(ctx, id)
	if err != nil {
		return nil, err
	}

	var userOutput UserOutput
	userOutput.ID = user.ID
	userOutput.Username = user.Name
	userOutput.Email = user.Email

	return &userOutput, nil
}

func (u *UserUsecase) SignIn(ctx context.Context, input SignInUserInput) (uint, error) {
	// ユーザー検索
	user, err := u.userRepo.FindByEmail(ctx, input.Email)
	if err != nil {
		return 0, err
	}

	// パスワード検証
	err = bcrypt.CompareHashAndPassword(
		[]byte(user.Password),
		[]byte(input.Password),
	)
	if err != nil {
		return 0, err
	}
	return user.ID, nil
}

repository

domain/repository/iuser_repository.goで追加するDBアクセス部分のinterfaceを定義する

package repository
// 省略

type IUserRepository interface {
	Create(ctx context.Context, user *model.User) (uint, error)

     // 追加部分(2行)
	FindByID(ctx context.Context, id uint) (*model.User, error)
	FindByEmail(ctx context.Context, email string) (*model.User, error)
}

dao

infrastructure/dao/user.go でデータベース操作部分を実装する。

  • FindByID(): IDを使ってDBからuserデータを取り出す
  • FindByEmail(): Emailを使ってDBからuserデータを取り出す
package dao

// 省略

// 以下を追加
func (r *userRepository) FindByID(ctx context.Context, id uint) (*model.User, error) {
	user := &model.User{}
	err := r.db.WithContext(ctx).Where("id = ?", id).First(&user).Error
	if err != nil {
		return nil, err
	}
	return user, nil
}

func (r *userRepository) FindByEmail(ctx context.Context, email string) (*model.User, error) {
	user := &model.User{}
	err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
	if err != nil {
		return nil, err
	}
	return user, nil
}

動作確認

SignIn

エンドポイント

# POST
http://localhost:8080/api/user/signin

リクエストボディ

{
    "email": "test_email",
    "password": "test_password"
}

スクリーンショット 2024-12-10 15.38.57.png

自動SignIn

エンドポイント

# GET
http://localhost:8080/api/user/me

スクリーンショット 2024-12-10 15.47.30.png

  • SignInやSignUpを行うとCookieにsession-nameのキーとバリューが格納されています
  • Cookieを使用してバックエンドからuserのデータがレスポンスとして返される

フロントでこのエンドポイントを叩くことでuserが存在するか?存在する場合はログイン処理を飛ばすって感じです。

自動SignOut

エンドポイント

# POST
http://localhost:8080/api/user/signout

スクリーンショット 2024-12-10 15.51.29.png

  • SignOutを行うとCookieに保存されていたsession-nameのキーとバリューがなくなります
  • これで次回以降は、SignInやSignUpを行う必要があります

おわりに

今回の記事では、前回のSignUp機能に続き、サインイン、自動サインイン、サインアウト機能を実装しました。これによってユーザーの認証機能による基本的なBE実装が完了です。

パスワード変更の実装の仕方などを後日書くつもりです。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?