Go言語でのWebアプリ開発学習その15~gorilla/sessionsを使ったセッション管理その1

前回 ( https://note.com/yasmizohawks/n/nbdadcb9902d0 ) に引き続き
Go言語でのWebアプリ開発についていろいろ見てみます。
今回はWebアプリ開発で必ずといっていいほど出てくる
セッション管理についてです。


■ 標準ライブラリにセッション管理機能がないだと?

Java には標準仕様であるサーブレット仕様において
セッション管理機能がサポートされています。

Go の標準ライブラリにもてっきりこの
セッション管理機能
がサポートされてるのかと思ったら、
サポートされてないようです。

ってことは、自分でがんばって実装するしかないのか?
って話になりますが、
セッション管理機能は誰が実装しても似たようなものになる
と思うので、
セッション管理機能実装済みの Go 用のライブラリが
どこかにありそうな気がして調べてみたら

Gorilla web toolkit
という、Go のWebアプリ開発に役立つライブラリたちを提供してくれる
オープンソースプロジェクトがありました。
Go ではよく利用されているっぽいです。

Gorilla web toolkit
https://gorilla.github.io/

この
Gorilla web toolkit
が提供するライブラリの中に、セッション管理機能ライブラリとして
gorilla/sessions
があります。

今回はこの
gorilla/sessions
の使い方を見てみようと思います。

■ gorilla/sessions 使う準備:Go のバージョンアップ

gorilla/sessions の最新バージョンは
Go のバージョン 1.23 じゃなとダメらしいです。

今の環境を確認したら

>go version
go version go1.20.3 windows/amd64

あら、古い。ということで、
https://go.dev/dl/
から
go1.23.6.windows-amd64.msi
をダウンロード。

Windows インストーラなんでインストーラ起動したときに
古いバージョンあったら気を利かして
古い方を削除してくれるかなと思い
go1.23.6.windows-amd64.msi を実行。

以下のダイアログが出ました。
古いバージョン消してくれるってよ。

インストーラ処理を継続、バージョンアップできました。

■ gorilla/sessions 使う準備:go.mod ファイルの作成/更新

今回は、標準ライブラリではないライブラリを使いますが、
そのためには以下を行う必要があるようです。

  1. go.mod ファイルの作成

  2. ソースコードの import 文への ライブラリパス定義

  3. go mod tidy コマンドを使っての go.mod ファイル更新

1.go.mod ファイルの作成
 
go mod init コマンドを使って go.mod ファイルを作成します。
 【サンプルソース置き場】ディレクトリでコマンド実行します。
 モジュール名は「webSessionSample」としています。

C:\【サンプルソース置き場】>go mod init webSessionSample
go: creating new go.mod: module webSessionSample
go: to add module requirements and sums:
go mod tidy

この時点での生成された go.mod ファイルの中身は以下です。

module webSessionSample

go 1.23.6

2.ソースコードの import 文への ライブラリパス定義
 次にソースコードを作成します。
 実際のソースについては後述しますが、
 gorilla/sessions 使うには import 文に
 "github.com/gorilla/sessions"を入れる必要があります。

import (
  "github.com/gorilla/sessions"
)

3.go mod tidy コマンドを使っての go.mod ファイル更新
 go mod tidy コマンドは
 ・ソースの import 文にある非標準ライブラリのパスを検出し
 ・そのパスを使ってライブラリを取得し
 ・go.mod ファイルを更新
 します。

 【サンプルソース置き場】ディレクトリでコマンド実行します。

C:\【サンプルソース置き場】>go mod tidy
go: finding module for package github.com/gorilla/sessions
go: downloading github.com/gorilla/sessions v1.4.0
go: found github.com/gorilla/sessions in github.com/gorilla/sessions v1.4.0
go: downloading github.com/gorilla/securecookie v1.1.2
go: downloading github.com/google/gofuzz v1.2.0

 gorilla/sessions 使うのに必要なものがダウンロードされてますね。
 また、go.mod ファイルが以下のように更新されてました。

module webSessionSample

go 1.23.6

require github.com/gorilla/sessions v1.4.0

require github.com/gorilla/securecookie v1.1.2 // indirect

 require github.com/gorilla/~
 が追記されてます。

 go.mod ファイルは作成も更新もファイルを直接編集するのではなく
 コマンド使うのがセオリーのようです。

■ gorilla/sessions 使ったセッション管理サンプル作ってみる

それでは
gorilla/sessions 使ったセッション管理サンプルを作ってみます。

各ファイルの配置は以下になります。

【サンプルソース置き場】ディレクトリ
 ┣ webSessionSample01.go
 ┣ go.mod
 ┗ pages ディレクトリ
   ┣ loginPageSample01.html
   ┗ topPageSample01.html

ログイン画面 ( loginPageSample01.html ) 上のボタンが押されたら
セッションが生成され、セッションにデータが格納されます。
そして、トップページ画面 ( topPageSample01.html ) に
リダイレクトされます。

トップページ画面は、セッションから取得したデータを表示します。
それぞれのソースを見てみましょう。

  • ログイン画面、トップページ画面の処理を行う webSessionSample01.go

package main

import (
	"html/template"
	"net/http"
	"github.com/gorilla/sessions"
)

// セッション情報格納場所の生成
var store = sessions.NewCookieStore([]byte("secret-key"))

var loginPageTemp *template.Template
var topPageTemp *template.Template

const (
	SessionName = "sample-session01"
)

type UserInfo struct {
	UserName string
	Age int
	Sex string
}

func init() {
	loginPageTemp = 
		template.Must(template.ParseFiles("pages/loginPageSample01.html"))
	topPageTemp = 
		template.Must(template.ParseFiles("pages/topPageSample01.html"))
}

func main() {
	http.HandleFunc("/viewLogin", viewLoginPage)
	http.HandleFunc("/viewTopPage", viewTopPage)
	http.HandleFunc("/executeLogin", executeLogin)

	if err := http.ListenAndServe(":8080", nil); err != nil {
		panic(err)
	}
}

func viewLoginPage(w http.ResponseWriter, r *http.Request) {
	if err := loginPageTemp.Execute(w, nil); err != nil {
		panic(err)
	}
}

func viewTopPage(w http.ResponseWriter, r *http.Request) {
	// CookieStoreからSessionNameに紐付いたSession構造体を取得
	session, err := store.Get(r, SessionName)

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	var u UserInfo

	if userName := session.Values["UserName"]; userName != nil {
		u.UserName = userName.(string)
	}

	if age := session.Values["Age"]; age != nil {
		u.Age = age.(int)
	}

	if sex := session.Values["Sex"]; sex != nil {
		u.Sex = sex.(string)
	}

	if err := topPageTemp.Execute(w, u); err != nil {
		panic(err)
	}
}

func executeLogin(w http.ResponseWriter, r *http.Request) {
	// loginId := r.FormValue("loginId")
	// password := r.FormValue("password")
	// 【中略】認証処理を行う。
	// loginIdとpasswordでの認証が成功したとする

	// SessionNameに紐付いたSession構造体をCookieStoreに生成
	// Session構造体を変数sessionに割り当てる
	session, err := store.Get(r, SessionName)

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Session構造体へ必要な情報を入れる
	session.Values["UserName"] = "溝口泰成"
	session.Values["Age"] = 53
	session.Values["Sex"] = "おとこ"

	// Session構造体の内容を保存
	err = session.Save(r, w)

	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// トップページへリダイレクト
	http.Redirect(w, r, "/viewTopPage", 301)
}

サンプルソースがだんだん長くなってきましたね。
そろそろソースを複数パッケージに分けるなどしないといけないかも。

◆ import
 
import に"github.com/gorilla/sessions"を含めます。

import (
  "html/template"
  "net/http"
  "github.com/gorilla/sessions"
)

◆ セッション情報格納場所の生成
 gorilla/sessions は、セッション情報保存先として
 Cookie とファイルを選べるようですが
 今回はネット上のサンプルによく出てくる Cookie にします。

var store = sessions.NewCookieStore([]byte("secret-key"))

 NewCookieStore 関数を使って
 セッション情報格納場所である CookieStore を生成します。
 NewCookieStore の引数にはいわゆる秘密鍵を設定します。
 この鍵を使ってセッション情報の暗号化/復号を行うようです。
 サンプルではとりあえず文字列「secret-key」を鍵としてます。
 CookieStore はグローバル変数 store に設定します。

◆ 各画面のテンプレートファイルを読み込んでおく
 ログイン画面、トップページ画面それぞれに対応する
 テンプレートファイルを init 関数であらかじめ読み込んでおきます。

func init() {
  loginPageTemp =
    template.Must(
      template.ParseFiles("pages/loginPageSample01.html"))
  topPageTemp =
    template.Must(
      template.ParseFiles("pages/topPageSample01.html"))
}

◆ URLパスへの関数割り当て、HTTPサーバ起動
 
main 関数部分は以下になります。

http.HandleFunc("/viewLogin", viewLoginPage)
http.HandleFunc("/viewTopPage", viewTopPage)
http.HandleFunc("/executeLogin", executeLogin)
if err := http.ListenAndServe(":8080", nil); err != nil {
  panic(err)
}

 http.HandleFunc を使って
 URLパス「/viewLogin」受信時には viewLoginPage 関数を
 URLパス「/viewTopPage」受信時には viewTopPage 関数を
 URLパス「/executeLogin」受信時には executeLogin 関数を
 実行するようにしてます。

 そして
 http.ListenAndServe
 でHTTPサーバを起動します。

◆ viewLoginPage 関数でログイン画面表示
 viewLoginPage 関数では、
 HTMLテンプレートファイル読み込み済みの
 Template構造体型グローバル変数 loginPageTemp を使って
 ログイン画面をブラウザへ送信します。

if err := loginPageTemp.Execute(w, nil); err != nil {
  panic(err)
}

◆ viewTopPage 関数でセッション情報をトップ画面に表示
 viewTopPage 関数では、まず
 CookieStore が設定されているグローバル変数 store を使って
 Session構造体を取得してます。

session, err := store.Get(r, SessionName)

 store.Get 関数の
 第1引数にはHTTPリクエスト情報の入った http.Request が
 第2引数にはセッション名称(定数SessionName)が
 渡されていて

 HTTPリクエストの Cookie に SessionName に対応する情報が
 存在すればその情報が格納された Session構造体を返します。

 Cookie に SessionName に対応する情報が無ければ
 新規に Session構造体を生成し、それを返します。

 次に、以下の部分で UserInfo 構造体に
 セッションから取得した情報を設定しています。

var u UserInfo

if userName := session.Values["UserName"]; userName != nil {
  u.UserName = userName.(string)
}
if age := session.Values["Age"]; age != nil {
  u.Age = age.(int)
}
if sex := session.Values["Sex"]; sex != nil {
  u.Sex = sex.(string)
}

 セッションに情報を格納するときは
 Session構造体内に定義されてる Values マップに設定します。
 なので、逆にセッションから情報を取得するときは
 session.Values["キー名"]
 で取得することになります。

 if の条件判定部分で
 userName := session.Values["UserName"]; userName != nil
 としているのは、
 セッションが存在しない場合にアクセスされたときを想定していて

 セッションが存在しない場合は
 session.Values["UserName"]は nil になってしまい
 u.UserName = userName.(string)
 部分でエラーになってしまうため、
 nil かどうかを判断するようにしてます。

 また、
 userName.(string)
 の「.(string)」ですが、これは
 Session構造体の Values マップの定義が以下になっているためです。

Values map[interface{}]interface{}

 Values マップはキーもバリューも「interface{}」型、つまり
 「どんな型のデータも設定可能」になっているため、
 Values マップから値を取得するときは
 値を設定したときのデータ型にキャストする必要があるんですね。
 
 なので、以下のように「型アサーション」を使って
 後ろに「.(string)」を付けて string 型の値にしています。
 session.Values["UserName"].(string)

 最後に HTMLテンプレートファイル読み込み済みの
 Template構造体型グローバル変数 topPageTemp を使って
 HTMLテンプレートに UserInfo 構造体を渡して
 トップ画面をブラウザへ送信します。

if err := topPageTemp.Execute(w, u); err != nil {
  panic(err)
}

◆ executeLogin 関数でセッションに値を設定
 executeLogin 関数は
 ログイン画面でログインID、パスワードを入力し
 送信ボタンを押されたときに実行される処理の実装イメージです。

 ログイン処理なので、通常は認証処理を行うのですが
 今回はセッション管理が目的で認証は目的では無いので
 【中略】としてコメントにしてます。

// loginId := r.FormValue("loginId")
// password := r.FormValue("password")
// 【中略】認証処理を行う。
// loginIdとpasswordでの認証が成功したとする

 認証が成功したら、セッション情報の作成に入ります。
 まず、CookieStore が設定されている
 グローバル変数 store を使って Session構造体を生成します。

session, err := store.Get(r, SessionName)

 store.Get 関数については前述しましたが、
 通常ログイン処理実行開始時は
 まだセッション情報が存在しないはずなので
 このサンプルでも「セッション情報はまだ存在しない」想定で
 とりあえず作ってます。

 つまり上記 store.Get 関数処理では、
 HTTPリクエストの Cookie に SessionName に対応する情報が無いので
 新規に Session構造体を生成し、それを返します。

 次に、Session構造体にセッションで保持したい情報を設定します。

session.Values["UserName"] = "溝口泰成"
session.Values["Age"] = 53
session.Values["Sex"] = "おとこ"

 Session構造体内で定義されている Values マップに値を設定します。
 本来は DB などから取得したデータなどを設定したりしますが
 今回はとりあえず文字列や数値をそのまま入れてます。

 次に、Session構造体に紐付いている関数 Save を使って
 Session構造体内の各情報を
 HTTP レスポンスの Set Cookie ヘッダへ設定します。

err = session.Save(r, w)

 この Save 関数を実行しないと
 ブラウザの Cookie に Session構造体内の各情報は設定されません。

 最後に トップ画面 ( URLパス:/viewTopPage )に
 リダイレクトします。

http.Redirect(w, r, "/viewTopPage", 301)

 リダイレクトとすることで、
 ・ログイン画面からのリクエスト
 ・ブラウザからのトップ画面へのリダイレクトリクエスト
 という「2つのリクエスト」が発生することになり、
 「2つのリクエスト」間でセッション情報が
 Cookie 経由で受け渡されている確認ができます。

  • ログイン画面テンプレートファイル loginPageSample01.html

<!DOCTYPE html>
<html>
	<head>
		<title>ログイン</title>
	</head>
	<body>
		<h3>ログイン</h3>
		<form action="/executeLogin" method="POST">
			ログインID:<input type="text" name="loginId"><br />
			パスワード:<input type="password" name="password"><br />
			<input type="submit" value="送信">
		</form>
	</body>
</html>

通常の HTML フォームを使ったものです。
送信ボタンが押されると「/executeLogin」が送信されます。
webSessionSample01.go の executeLogin 関数が動きます。

  • トップ画面テンプレートファイル topPageSample01.html

<!DOCTYPE html>
<html>
	<head>
		<title>トップページ</title>
	</head>
	<body>
		<h3>ログインユーザ情報</h3>
		<p>名前:{{ .UserName }}</p>
		<p>年齢:{{ .Age }}</p>
		<p>性別:{{ .Sex }}</p>
	</body>
</html>

webSessionSample01.go から渡された
UserInfo 構造体 の内容を表示します。

■ gorilla/sessions 使ったセッション管理サンプル実行してみる

それではサンプルを実行してみましょう。

まず、【サンプルソース置き場】ディレクトリに移動して
HTTPサーバを起動

C:\【サンプルソース置き場】>go run webSessionSample01.go

次に、ログイン画面ではなくてトップ画面のURL
http://localhost:8080/viewTopPage
をブラウザのURL欄に入れてトップ画面出します。

トップ画面は UserInfo 構造体 の内容を表示するのですが、
まだ Cookie にセッション情報が無いために
UserInfo 構造体内の各変数に 初期値が設定されていて
それが表示されてますね。

Chrome の EditThisCookie というプラグインで
Cookie の中身確認すると

Cookie が無いようです。

次に、ログイン画面のURL
http://localhost:8080/viewLogin
をブラウザのURL欄に入れてログイン画面出します。

ログインID、パスワードを適当に入れて
送信ボタン押します。

http://localhost:8080/viewTopPage
にリダイレクトされ、
webSessionSample01.go の executeLogin 関数で
Session構造体内で定義されている Values マップに設定した
値が表示されてますね。

Chrome の EditThisCookie プラグインで
Cookie の中身確認すると

webSessionSample01.go の定数 SessionName に定義した
「sample-session01」と

SessionName = "sample-session01"

それに紐付く情報として暗号化された
セッション情報が Cookieに格納されてます。

ログイン画面表示前にトップ画面
http://localhost:8080/viewTopPage
表示したときは
Cookie に情報が無かったため UserInfo 構造体内の変数の初期値が表示

ログイン画面のボタン押下後にトップ画面
http://localhost:8080/viewTopPage
が表示されたときは
Cookie に情報が設定されたため UserInfo 構造体内の変数に設定された
Cookie から取得された値が表示

されていることがわかります。

次回も gorilla/sessions について見てみようかと思います。

#プログラミング
#IT
#プログラミング言語
#Go言語
#GO
#Golang
#Webアプリケーション開発

いいなと思ったら応援しよう!