[Go] echo で session 管理を実装してみた時の話


## 環境
* MacBook Air (M1, 2020)
* MacOS Monterey 12.0.1
* VSCode 1.71.0
* go version go1.18 darwin/arm64
    * echo v4.7.2
    * gorilla/sessions v1.2.1

## やりたいこと & この記事で書くこと
go の [echo](https://echo.labstack.com) と [gorilla/sessions](https://github.com/gorilla/sessions) を用いて、以下のような処理を実装します。


1. クライアントからアプリケーションサーバーに認証が必要なリクエストをする。
1. アプリケーションサーバーがセッションファイルを作成して、セッション管理する。
1. ブラウザが cookie を保存する。
1. 以降同様のリクエストがきた場合にセッションを確認してユーザーの判別を行う。


と言っても、ほとんどの実装は echo や gorilla/sessions が提供してくれているので、こちらで書くべきコードは軽量です。
上記の処理を実装する上で、gorilla/sessions のセッション管理などコードを読んでみたので、それも紹介します。 

## この記事で書かないこと 
* go 環境のセットアップ
* cookie、セッションとは何か
    * [この記事](https://yutaro-blog.net/2021/08/27/session-cookie/#index_id0)がおすすめです。

* echo の使い方
    * [公式ドキュメント](https://echo.labstack.com)があるので、そちらを参考にしてみてください。


## 早速コードを書く

今回は API サーバーに echo を使うので、公式ドキュメントを見てみます。すると、[session](https://echo.labstack.com/middleware/session/) という項目があるのでとりあえずこのコードを使って実装してみます。

```go:main.go
e := echo.New()
e.Use(session.Middleware(sessions.NewCookieStore([]byte("secret"))))

e.GET("/", func(c echo.Context) error {
  sess, _ := session.Get("session", c)
  sess.Options = &sessions.Options{
    Path:     "/",
    MaxAge:   86400 * 7,
    HttpOnly: true,
  }
  sess.Values["foo"] = "bar"
  sess.Save(c.Request(), c.Response())
  return c.NoContent(http.StatusOK)
})

```

この実装だけで最初に紹介したやりたいことはほぼ担保できています。


## 何をやっているのかわからないのでコードを読む
上記の実装ではセッションファイルはどこに作成されたのかなどがわからないので、コードを読んでみます。 

まず気になるのは `NewCookieStore()` です。名前から察するに新しい cookie を作成してどこかに登録しているのかな ... ?

```go:store.go
func NewCookieStore(keyPairs ...[]byte) *CookieStore {
	cs := &CookieStore{
		Codecs: securecookie.CodecsFromPairs(keyPairs...),
		Options: &Options{
			Path:   "/",
			MaxAge: 86400 * 30,
		},
	}

	cs.MaxAge(cs.Options.MaxAge)
	return cs
}
```
https://github.com/gorilla/sessions/blob/a6a8e49c83a2dba9cf7f486feb2662d6439851f2/store.go#L49-L60

みた感じ codec のように双方向の暗号化や複合化を行うっぽいです。 
この段階ではファイルは作成されていないようですね。

次に気になるのは、以下のコードです。 
```go:store.go
func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore {
	if path == "" {
		path = os.TempDir()
	}
	fs := &FilesystemStore{
		Codecs: securecookie.CodecsFromPairs(keyPairs...),
		Options: &Options{
			Path:   "/",
			MaxAge: 86400 * 30,
		},
		path: path,
	}

	fs.MaxAge(fs.Options.MaxAge)
	return fs
}
```
https://github.com/gorilla/sessions/blob/a6a8e49c83a2dba9cf7f486feb2662d6439851f2/store.go#L136-L151

こちらの関数では、一番最初に path を取得しているのがわかります。ということはファイルでセッションを管理する場合おそらくこっちの関数を使えば、それが実装できそうな気がするので、ここでコードの修正を行います。

```go:main.go
e := echo.New()
e.Use(session.Middleware(sessions.NewFilesystemStore([]byte("secret"))))
.
.
.
```

ひとまずこれで実行してみて、ファイルが作成されること等確認してみます。 

最終的なコード:
```go:main.go

func main() {
	e := echo.New()
	e.Debug = true
	e.Logger.SetLevel(log.DEBUG)

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Use(session.Middleware(sessions.NewFilesystemStore("", []byte("secret"))))

	e.GET("/", sessionHandler)

	e.Logger.Fatal(e.Start(":1323"))
}

func sessionHandler(c echo.Context) error {
	session, _ := session.Get("session-valorant_agent", c)
	session.Options = &sessions.Options{
		Path:     "/",
		MaxAge:   86400 * 7,
		HttpOnly: true,
	}
	session.Values["foo"] = "bar"
	session.Save(c.Request(), c.Response())

	return c.NoContent(http.StatusOK)
}

```

何事もなくアクセスできました。 
さて、セッションファイルはどこに作成されたのでしょうか ... 
先ほどのコードをさらに読み進めると、以下の関数を呼んでいることがわかりました。 

```go:file_unix.go
func tempDir() string {
	dir := Getenv("TMPDIR")
	if dir == "" {
		if runtime.GOOS == "android" {
			dir = "/data/local/tmp"
		} else {
			dir = "/tmp"
		}
	}
	return dir
}
```

`TMPDIR` からディレクトリパスを取得してその下にファイルを作成しているようです。 
ターミナルから `echo $TMPDIR` を実行してみると、以下のような出力を得られました。
`/var/folders/fb/x4pq5_3s7bd588mrf_dr03j80000gn/T/`

とりあえず `cd` で移動して `ls` してみます。
大量のファイルがあることがわかりましたが、今回確認したいのはセッションファイルなのでそれっぽい名前のファイルを探したところ `session_1` というファイルが見つかりました。
![スクリーンショット 2022-09-21 22.10.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376889/384228c4-9656-6551-7f60-549f9ce08bae.png)

このファイルを `less` で開いて中身を確認してみます。 

`$ less {ファイル名}`

以下のような文字列が格納されていました。 

`MTY2MzYzMjU1OHxEdi1CQkFFQ180SUFBUkFCRUFBQUlQLUNBQUVHYzNSeWFXNW5EQVVBQTJadmJ3WnpkSEpwYm1jTUJRQURZbUZ5fAfKkPKvwILEwfTiSA_t5CqxCKhyJxUqlzWPUR6-KXJO`


この文字列が cookie として保存されているのか chrome の検証を用いて確認してみます。
![スクリーンショット 2022-09-21 22.17.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376889/b1217ad8-d94f-345e-b2c9-2670c913f653.png)
ありました。保存されているようです。

23/01/11 更新
上記の cookie の確認ですが、よくみたら全然違うものが Value に入ってますね。。。
もうちょっと使ってみる必要がありそうです。

## 裏側で色々やりすぎでしょ ... (褒め言葉)
echo の session は公式のドキュメントにもあるように gorilla/sessions に依存しています。 
gorilla/sessions の gtihub に飛ぶと README に以下のようなことが書いてありました。

> * シンプルなAPI：署名された（オプションで暗号化された）クッキーを設定する簡単な方法として使用します。
> * Cookieやファイルシステムにセッションを保存するためのバックエンドを内蔵しています。
> * フラッシュメッセージ：読み込まれるまで持続するセッションの値。
> * セッションの持続性（別名「remember me」）を切り替えたり、他の属性を設定する便利な方法。
> * 認証キーと暗号化キーをローテーションさせる仕組み。
> * 異なるバックエンドを使用しても、1つのリクエストで複数のセッションを使用できます。
> * カスタムセッションバックエンド用のインターフェースとインフラストラクチャ：共通のAPIを使用して、異なるストアからのセッションを取得し、一括保存することができます。

(DeepL で翻訳しています。)
https://github.com/gorilla/sessions/blob/master/README.md

今回のケースに関係しそうなのは、上から2番前の
`Cookieやファイルシステムにセッションを保存するためのバックエンドを内蔵しています。`
この一文です。 

こちらで実装するコードは軽量ながら、gorilla/sessions が裏側でよしなにやってくれていたんだな ... 


## 最後に 
OSS のコードを読むのはいろんな発見もあって面白いですね。 github は(結構前ですが) iOS アプリも作ってくれたので、暇な時にすぐ読めるのは暇つぶしにもなって良いですね ~ 

個人開発で Web アプリケーションを実装中の過程で行ったことを紹介してみました。 
今後も何か気づいたことがあれば記事を書こうと思っています。