Go(echo)で簡易的なCookieを実装してみた

# はじめに

以前、[Go(echo)とMySQLでAPIサーバーを構築](https://qiita.com/T_death/items/e5d99d26b235ddf7659a)してみました。それの続きとして今回は、Go(echo)を使ったCookieの実装をしてみました。  
サーバー構築とかバックエンドとか全然やったことなかったので、無知が故に沼った箇所が多々ありました。自分への戒めとして、それらも記述したいと思います。

## 実装した処理の流れ

* フロント → localhost:3000
* バック → localhost:8080
* localhost:3000→localhost:8080/setSessionに、データをPOSTメソッドで送る+Cookieの作成
* localhost:3000→localhost:8080/readSessionに、GETメソッドでブラウザ側にCookieがあるかを確認

たったこれだけ。

## GoとCookie

まずはCookieとは何かを知る必要があり、さらにそれをどのようにGoで実装するのかを知る必要がありました。[こちらの記事](https://zenn.dev/chillout2san/articles/4868c825158729)が細かく説明してくれていたので、参考にしました。

## 環境構築(Goのインストール以降の話)

以下のディレクトリ構成で作りました。
```
./practice_cookie
    ├─ cookie.go
    ├─ fileserver.go
    ├─ index.html
    ├─ go.mod
    └─ go.sum
```

必要なフレームワークをインストールします。
1. `go.mod`の作成
    * ターミナルで`go mod init main`を実行して、go.modファイルを作成
    * go.modと同じディレクトリで、`go get github.com/labstack/echo/v4`と`github.com/labstack/echo/v4/middleware`を実行
2. `go.sum`の作成
    * go.modと同じディレクトリで、`go mod tidy`を実行

echoのインストールで1つ気をつけてほしいポイントがありまして。GoでCookieを実装する方法を調べると、色々と記事が出てきますが、多くの記事で使われているechoフレームワークには**echo**と**echo/v4**の2つがあります(middlewareも同様)。これらが混同してしまうと、実行時にエラーが発生するため、echoかecho/v4かのどちらかに統一してください。今回のこの記事ではecho/v4にしました。  
僕は2つのフレームワークが混在してしまって、ここでちょっと沼りました。

## ソースコード

### cookie.go
```cookie.go
package main

import (
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

// 受け取るデータ
type info struct {
	Id string `json:"id"`
	Password string `json:"password"`
}

// Cookieの作成
func setCookie(c echo.Context) error {
	post := new(info)
	err := c.Bind(&post)
	if err != nil {
		fmt.Println(err)
		return c.String(http.StatusCreated, "Missed setCookie!")
	}

	cookie := &http.Cookie{
		Name: "cookie",
		Value: post.id,
		MaxAge: 10, // 10secs
		Path: "/",
	}
	
	c.SetCookie(cookie)
	return c.String(http.StatusCreated, "Success setCookie!")
}

// Cookieの確認
func readCookie(c echo.Context) error {
	cookie, err := c.Cookie("cookie")
	if err != nil {
		return c.String(http.StatusCreated, "Missed readCookie!")
	}
	
    // Cookie情報がちゃんとあるか確認
	// fmt.Println(cookie.Name)
	// fmt.Println(cookie.Value)

	return c.String(http.StatusCreated, "Success readCookie!")
}

func main() {
	e := echo.New()

	// ミドルウェアの設定
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		AllowCredentials: true,
		AllowOrigins: []string{"http://localhost:3000"},
		AllowMethods: []string{
			http.MethodPost,
			http.MethodGet,
		},
	}))

	// ハンドラーの設定
	e.POST("/setCookie", setCookie)
	e.GET("/readCookie", readCookie)

    // サーバー起動
	e.Logger.Fatal(e.Start(":8080"))
}
```
コードの内容をざっくりと説明すると、
* `setCookie`で、フロント側から送られるjson形式のデータを受け取り、それをもとにCookie情報を作成します
* `readCookie`で、ブラウザ側にCookie情報があるかを確認します
* `main`で、Cookieのやり取りをする設定を色々やってます

ここで、Cookieのやりとりをする際に、めちゃくちゃ大事になってくる内容が、main関数の中に記述している`middleware.CORSWithConfig`です。ここはかなり沼りました..。  
[こちらの記事](https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34#cookie%E3%82%82%E8%A8%B1%E5%8F%AF%E3%81%97%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88)のおかげで、無事解決できました。端的に話すと、Cookieのやりとりをするためには、バック側で`Access-Control-Allow-Origin`と`credentials`の設定が必須であるということです。この設定が無くてもフロント側とデータのやりとりはできるのですが、Cookieだけはやりとりができない、という事態になります。  

データは送れているっぽいのに、Cookieは送れてなさそう..?ってことは、通信は問題ないからCookieを新規作成するコードが間違ってるのか！と勘違いしてしまいました。

### fileserver.go

```fileserver.go
package main

import (
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	fileServer := http.FileServer(http.Dir("."))
	mux.Handle("/", http.StripPrefix("/", fileServer))
	server := http.Server{
		Addr: ":3000",
		Handler: mux,
	}

	server.ListenAndServe()
}
```
フロント側で使用する静的なファイル(今回はindex.html)を持ってもらうだけのサーバーです。同じディレクトリに静的なファイルがあるため、引数が`.`や`/`になっていますが、ここでパス名を指定したら、他の静的ファイルが存在するディレクトリを指定できるらしいです。

### index.html
```index.html
<html>
    <body>
        <h1>入力フォーム</h1>
        <input type="text" id="id" placeholder="id"><br>
        <input type="password" id="password" placeholder="password"><br>
        <input type="button" value="Cookieを新規作成" onclick="setCookie()">
        <input type="button" value="Cookieの確認" onclick="readCookie()">
    </body>
</html>

<script>
    const baseURL = "http://localhost:8080"
    
    async function setCookie() {
        var id = document.getElementById("id").value;
        var password = document.getElementById("password").value;
        console.log(id, password);
        await fetch(baseURL + "/setCookie", {
            method: "POST",
            mode: "cors",
            credentials: "include",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                "id": id,
                "password": password,
            })
        })
        .then(response => console.log(response.text()));
    }
    
    async function readCookie() {
        await fetch(baseURL + "/readCookie", {
            method: "GET",
            mode: "cors",
            credentials: "include",
        })
        .then(response => console.log(response.text()));
    }
</script>
```
JavaScriptのfetchを使って、バック側とやりとりをしています。ここにも沼ポイントがありました。  
[先ほど参考にした記事](https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34#cookie%E3%82%82%E8%A8%B1%E5%8F%AF%E3%81%97%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88)に、Cookieのやりとり時にフロント側でやるべき設定も記載されています。フロント側では、`mode`と`credentials`の設定が必須です。これらが無いとエラーになります。また、今回POSTメソッドにて送るデータはjson形式になっているため、`headers`の設定も必要です。  

個人的に、このフロント側での必須な設定が一番沼でした。Cookieのやりとりは全部バックで設定するものだと勝手に思っていたので、フロント側でも考慮するなんてこれっぽっちも思っていませんでした。良い勉強になりました。  

その他、細かいフロント側の話(async/awaitやpromise)に関する知識は、[こちらの記事](https://qiita.com/sotasato/items/31be24d6776f3232c0c0)がわかりやすかったです。

## 実行確認
2つのターミナルで、今回の作業ディレクトリまで移動して、それぞれのターミナルで`go run cookie.go`と`go run fileserver.go`を実行します。cookie.goの方は大きな文字でEchoと表示され、fileserver.goの方は何も表示されませんが、それで合ってます。  
Chromeで`http://localhost:3000`にアクセスすると、以下のような画面になっているはずです。その画面上でF12を使って、DevToolsを確認できるようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/a59ce941-5380-1507-1874-999b37d1e5c6.png)
テキトーにidとpasswordを入力して、まずは`Cookieを新規作成`ボタンをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/8fcc19e0-65f8-de1b-cd58-d4dd49c8d5cd.png)

すると上画像のように、DevToolsで入力したidとpasswordが確認できると思います。同時に、promiseも表示されるので、クリックして確認してみると、`"Success setCookie!"`が確認でき、見事Cookieの作成ができました。  

---

Cookieがブラウザ上に本当にあるかを確認するためには、DevToolsのApplicationでCookieとあるので、そこの中にある`http://localhost:3000`を見てみます。
![スクリーンショット 2023-08-19 14.29.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/1e4a31c3-9f3e-796c-c3e3-3229f73a465b.png)
すると、cookieというNameでpasswordというValueがありました。これらのCookieの内容は、`cookie.go`で設定しています。今回のCookieの設定では、10秒だけ保持されるような設定にしているため、10秒経つとCookie情報は自動的に無くなります(設定した時間が経過してもDevTools上には表示されていますが、ブラウザを再読み込みすると消えます)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/2ec38a65-c78d-cf9a-0212-6b1d111ff364.png)

---

最後に、Cookie情報がきちんと読み取れているかを確認します。
`Cookieを新規作成`してから、設定した時間内で、今度は`Cookieの確認`ボタンをクリックしてみます。![スクリーンショット 2023-08-19 14.37.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/3959effb-6c7b-952e-133f-4b938e3d0572.png)
こんな感じで、設定した時間だけCookieが有効になっていることが確認できました。

# 終わりに

Cookieの実装が、思っていた数倍難しかったです。コーディングの複雑さとかは全然感じませんでしたが、記事中にもちょこちょこ書いていたとおり、Cookieのやりとりに必要な設定周りで苦戦しました。一方で、今回の苦戦した箇所は非常に良い経験になったと思います。