改めて学ぶセッションベース認証[Goによる実例付き]

アプリケーションにおけるユーザー認証は、セキュリティとユーザーエクスペリエンスの核となる部分です。
この記事では、セッションベース認証(この記事ではセッション認証とします)の基本を理解し、Go言語(Golang)のを使った具体的な実装方法を解説します。標準ライブラリのみ使用してシンプルに書いてあるのでGo言語を普段使わない人も理解しやすいかと思います。

# セッション認証とは？
セッション認証は、ユーザーが一度ログインすると、その後の各リクエストでユーザーを識別する方法です。
通常、セッション認証では以下のステップが含まれます。

1. ユーザーはブラウザを通じてIDとパスワードを含むログインリクエストをサーバーに送信します。
2. サーバーは提供された認証情報を検証し、正しいと判断された場合にセッションを確立します。セッション情報はサーバー内とCookieに保存されます。今回はメモリに保存しますが、実際のアプリケーションではDBやその他のストレージを使用することが一般的です。
3. サーバーは、セッションが確立されたことを示すレスポンスをブラウザに送り返します。
4. ユーザーが認証済みであることを示すセッション情報を含むリクエストを、ブラウザはサーバーに送信します。
5. サーバーはリクエストに含まれるセッション情報を検証し、ユーザーの認可を行います。
6. 認可が成功し、ユーザーが特定のアクションやリソースへのアクセスを許可されたことを示すレスポンスをサーバーがブラウザに送ります。
7. ユーザーがログアウトを要求すると、ブラウザはログアウトリクエストをサーバーに送信します。
8. サーバーはユーザーのサーバー内とCookieからセッションを削除し、セッション情報を無効化します。
9. サーバーは、セッションが正常に終了したことを示すレスポンスをブラウザに送り返します。

```
ブラウザ                   サーバー
  |                         |
  |--- 1. ログイン ------>   |
  |   (IDとパスワード)        |
  |                         | 2. セッションの確立(サーバー内とCookieに保存)
  |                         |
  |<-- 3. レスポンス-----     |
  |                         |
  |                         |
  |  --- 4. リクエスト ----->|
  |   (セッション情報含む)     | 5.認可成功
  |                         |
  |<-- 6. レスポンス -----    |
  |   (ユーザー識別)          |
  |                         |
  |  --- 7. ログアウト ------>|
  |                         | 8.セッション削除
  |<-- 9. レスポンス ---      |
  |   (セッション情報消去)     |
  |                         |


```
## Go言語によるセッション認証のサンプル実装
以下は、上記で説明したセッション認証の基本的な流れを実装しました。
```go
package main

import (
	"fmt"
	"net/http"
	"sync"
)

var (
	// セッション情報を保存するためのマップ
	sessions = make(map[string]bool)
	// セッション情報へのアクセスを同期するためのミューテックス
	sessionMutex = &sync.Mutex{}
)

func main() {
	http.HandleFunc("/login", loginHandler)
	http.HandleFunc("/dashboard", dashboardHandler)
	http.HandleFunc("/logout", logoutHandler)
	http.ListenAndServe(":8080", nil)
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
	// 本来はIDとパスワードでのユーザー認証のロジックを実装する

	// セッションIDを生成（ここではシンプルにユーザー名を使用）
	sessionID := "user123" // 実際にはもっと安全なランダムなIDを生成する

	// サーバーサイドでセッションを保存
	sessionMutex.Lock()
	sessions[sessionID] = true
	for id := range sessions {
		fmt.Println("loginHandler: Current session ID is ", id)
	}
	sessionMutex.Unlock()

	// クライアントにセッションIDをCookieとして送信
	http.SetCookie(w, &http.Cookie{
		Name:   "session_id",
		Value:  sessionID,
		Path:   "/",
		MaxAge: 60, // 有効期限を60秒（1分）に設定
	})

	w.Write([]byte("login successfully!"))
}

func dashboardHandler(w http.ResponseWriter, r *http.Request) {
	// CookieからセッションIDを取得
	cookie, err := r.Cookie("session_id")
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	sessionMutex.Lock()
	authenticated, ok := sessions[cookie.Value]
	if ok {
		for id := range sessions {
			fmt.Println("dashboardHandler: Current session ID is ", id)
		}
	}
	sessionMutex.Unlock()

	// セッションが有効かチェック
	if !ok || !authenticated {
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	w.Write([]byte("Welcome to your dashboard!"))
}

func logoutHandler(w http.ResponseWriter, r *http.Request) {
	// CookieからセッションIDを取得し、サーバーサイドでセッションを削除
	cookie, err := r.Cookie("session_id")
	if err == nil {
		sessionMutex.Lock()
		delete(sessions, cookie.Value)
		if len(sessions) == 0 {
			fmt.Println("logoutHandler: Current session ID is nothing")
		}
		sessionMutex.Unlock()
	}

	http.SetCookie(w, &http.Cookie{
		Name:   "session_id",
		Value:  "",
		Path:   "/",
		MaxAge: -1, // Cookieを削除
	})

	w.Write([]byte("Logout successful!"))
}

```
## 実際に動かす
### まずはサーバーを起動
```shell
go run ./main.go
```

### ログイン
そして、ブラウザで `http://localhost:8080/login` にアクセスします。
![Screenshot 2023-11-25 at 13.43.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309702/f2c35b20-8d55-d2bc-1c25-e696c2e06110.png)
アクセス成功です！
この時、サーバーではメモリにセッション情報を保持していることがわかります。
```shell
loginHandler: Current session ID is  user123
```
ブラウザの開発者ツールでCookieを確認すると、セッションがセットされているのがわかります。
また、有効期間も設定されていますね。UTCで表示されているので、9時間の差があります。
有効期間を過ぎてブラウザをリロードすると、こちらが削除されていることが確認できます。
![Screenshot 2023-11-25 at 14.50.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309702/9643bb13-b88c-f900-3f25-4f91dbb587cc.png)

### ダッシュボードへアクセス
次に、`http://localhost:8080/dashboard` にアクセスします。
![Screenshot 2023-11-25 at 13.46.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309702/1e0c358e-d027-6c65-f20b-570971f4f695.png)
もし、ここで`Forbidden`が表示されている場合、有効期限が切れているので再度ログインしてください。

この時、サーバーではメモリにセッション情報を保持していることがわかります。
```shell
dashboardHandler: Current session ID is  user123
```

### ログアウト
最後に、`http://localhost:8080/logout` にアクセスします。
![Screenshot 2023-11-25 at 13.49.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309702/a238b34a-9057-b7fa-50e9-2ba460f07cd0.png)

これで、Cookieに保存されたセッションが削除されているのがわかります。
![Screenshot 2023-11-25 at 13.50.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309702/475da239-aa4c-6d0e-a8bf-1405b93a6f8f.png)
この時、サーバーではメモリにセッション情報を保持していることがわかります。
```shell
logoutHandler: Current session ID is nothing
```

この状態で、`http://localhost:8080/dashboard` にアクセスします。
すると、セッションがないためエラーとなります。
![Screenshot 2023-11-25 at 13.56.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309702/b11bee5c-cbd7-737b-bc5a-3abccf3684fa.png)

# まとめ
セッション認証はWebアプリケーションにおける重要な部分であり、Go言語を使って効果的に実装することができます。
商用で開発する際には、適切なセキュリティ対策を講じ、ユーザーに安全で快適な体験を提供しましょう。


# 参考
https://sherryhsu.medium.com/session-vs-token-based-authentication-11a6c5ac45e4