前回の記事に引き続き、Goにおけるセッション管理機構の実装方法を解説していきます。
前回の記事では、セッション管理機構を実現するための設計を考えました。
そこにおいて、セッションマネージャに実装すべき管理機能と、個々のセッションにに紐づけられるべき変数操作などの機能を分けて洗い出しました。
今回は、前者であるセッションマネージャの実装例を提示し、内容についての解説を行っていきたいと思います。
やや複雑な内容になっていきますが、頑張って学習していきましょう。
Contents
セッションマネージャに必要な機能
実装の前に、セッションマネージャに必要な機能をおさらいしておきましょう。
前回行った設計において、セッションマネージャには以下の機能を実装する必要があるとしました。
- セッションマネージャの構造定義
- ユニークなセッションIDの発行
- クライアント情報とセッション情報の関連付け
- セッションの生成・保存・破棄
これらの機能を漏れなく実装できた時点で、セッションマネージャとして必要最低限の機能を備えたこととします。
manager.goファイルの記述
それでは、Goディレクトリの中にsessionsディレクトリをつくり、その中にmanager.goファイルを作成してください。
manager.goファイルには、以下の内容を記述してみてください。
package sessions // セッション管理パッケージ import ( "crypto/rand" "encoding/base64" "errors" "io" "net/http" ) /* ******************* * * セッションマネージャ構造体 * ****** */ type Manager struct { database map[string]interface{} } var mg Manager /* ******************* * * 新規マネージャ生成 * ****** */ func NewManager() *Manager { return &mg } /* ******************* * * セッションIDの発行 * ****** */ func (m *Manager) NewSessionID() string { b := make([]byte, 64) if _, err := io.ReadFull(rand.Reader, b); err != nil { return "" } return base64.URLEncoding.EncodeToString(b) } /* ******************* * * 新規セッションの生成 * ****** */ func (m *Manager) New(r *http.Request, cookieName string) (*Session, error) { cookie, err := r.Cookie(cookieName) if err == nil && m.Exists(cookie.Value) { return nil, errors.New("sessionIDはすでに発行されています") } session := NewSession(m, cookieName) session.ID = m.NewSessionID() session.request = r return session, nil } /* ******************* * * セッション情報の保存 * ****** */ func (m *Manager) Save(r *http.Request, w http.ResponseWriter, session *Session) error { m.database[session.ID] = session c := &http.Cookie{ Name: session.Name(), Value: session.ID, Path: "/", } http.SetCookie(session.writer, c) return nil } /* ******************* * * 既存セッションの存在チェック * ****** */ func (m *Manager) Exists(sessionID string) bool { _, r := m.database[sessionID] return r } /* ******************* * * 既存セッションの取得 * ****** */ func (m *Manager) Get(r *http.Request, cookieName string) (*Session, error) { cookie, err := r.Cookie(cookieName) if err != nil { // リクエストからcookie情報を取得できない場合 return nil, err } sessionID := cookie.Value // cookie情報からセッション情報を取得 buffer, exists := m.database[sessionID] if !exists { return nil, errors.New("無効なセッションIDです") } session := buffer.(*Session) session.request = r return session, nil } /* ******************* * * セッションの破棄 * ****** */ func (m *Manager) Destroy(sessionID string) { delete(m.database, sessionID) }
いくつかエラーが出る箇所があると思いますが、いったん気にせずにおいてください。
ここからは、実装内容について解説していきます。
セッションマネージャの構造体定義
ファイルの先頭では、セッションマネージャの構造体を以下のように定義しています。
/* ******************* * * セッションマネージャ構造体 * ****** */ type Manager struct { database map[string]interface{} }
database変数は、キーが文字列型で要素がインターフェース型であるマップとして定義しています。
キーとして用いられるのは、グローバルかつユニークなセッションIDです。
要素には、キーのIDに紐づくセッション情報を格納します。
var mg Manager /* ******************* * * 新規マネージャ生成 * ****** */ func NewManager() *Manager { return &mg }
上記は、Manager構造体のいわゆるインスタンス化に相当する処理です。
新しいセッションの作成
ここからは、セッション管理を行うための個別具体的な機能の実装になります。
まず、新しくセッションを作成する場合の処理を観ていきましょう。
セッションIDの発行
新しくセッションを生成するにあたり、まずはセッションIDを発行する必要があります。
以下は、セッションIDを発行する関数です。
/* ******************* * * セッションIDの発行 * ****** */ func (m *Manager) NewSessionID() string { b := make([]byte, 64) if _, err := io.ReadFull(rand.Reader, b); err != nil { return "" } return base64.URLEncoding.EncodeToString(b) }
セッションIDは、クライアントを識別するために、グローバルにユニークである必要があります。
今回は、cryptoパッケージの乱数生成機能を使用して、64バイト長のランダム文字列を生成することでユニークであることを保証しようとしています。
この部分は、「ユニークであること」をどの程度の精度で担保する必要があるか、システム要件によって実装内容を健闘するべきでしょう。
新規セッションの生成
セッションの実体化自体は、session.goファイル側に記述するべき内容です。
セッションマネージャ側で行う新規セッション生成処理は、クライアントのCookieをチェックして既にセッションが開始済みでないことを確認し、session.goファイルの処理を呼び出す制御処理になります。
/* ******************* * * 新規セッションの生成 * ****** */ func (m *Manager) New(r *http.Request, cookieName string) (*Session, error) { cookie, err := r.Cookie(cookieName) if err == nil && m.Exists(cookie.Value) { return nil, errors.New("sessionIDはすでに発行されています") } session := NewSession(m, cookieName) session.ID = m.NewSessionID() session.request = r return session, nil }
42行目の(*Request) Cookie関数は、リクエストに含まれる名前付きCookieのうち、引数で指定された名前のCookieを返します。
これにより、リクエストのCookieをチェックし、すでに該当のクライアントのセッションが開始されていないかをチェックしています。
47行目以降は、NewSession関数で新しいセッション構造体を生成し、構造体に必要な情報をセットしています。
構造体の定義およびNewSession関数の処理内容は、次回の記事にてsession.goファイル側に記述します。
セッション情報の保存
以下は、セッション情報をサーバとクライアントの双方に保存する処理です。
/* ******************* * * セッション情報の保存 * ****** */ func (m *Manager) Save(r *http.Request, w http.ResponseWriter, session *Session) error { m.database[session.ID] = session c := &http.Cookie{ Name: session.Name(), Value: session.ID, Path: "/", } http.SetCookie(session.writer, c) return nil }
58行目では、セッションマネージャ構造体のマップに対し、セッションIDをキーとして、セッション構造体を要素に代入しています。
サーバサイドへのセッション情報保存がこれにあたります。
続いて60行目で同一のセッションIDを保持するCookieを生成し、66行目でクライアントのCookieに保存しています。
この処理により、サーバに保持されるセッション情報とクライアントをIDで紐づけることが可能になります。
既存セッションの管理
続いて、保存されているセッション情報を管理するための機能を見ていきましょう。
既存セッションの存在チェック
/* ******************* * * 既存セッションの存在チェック * ****** */ func (m *Manager) Exists(sessionID string) bool { _, r := m.database[sessionID] return r }
こちらの処理はシンプルで、引数で渡されたセッションIDが既に存在するかどうかをチェックしています。
クライアントリクエストに含まれるセッションIDの存在チェックのほか、新規IDを発行した時の重複チェックにも用いることができます。
既存セッション情報の取得
/* ******************* * * 既存セッションの取得 * ****** */ func (m *Manager) Get(r *http.Request, cookieName string) (*Session, error) { cookie, err := r.Cookie(cookieName) if err != nil { // リクエストからcookie情報を取得できない場合 return nil, err } sessionID := cookie.Value // cookie情報からセッション情報を取得 buffer, exists := m.database[sessionID] if !exists { return nil, errors.New("無効なセッションIDです") } session := buffer.(*Session) session.request = r return session, nil }
82行目では新規セッションの生成時と同様に、Cookie情報を取得しています。
そうしてCookieから取得したセッションIDをキーに、セッションマネージャ構造体が保持しているマップから、セッション構造体を取得しています。
つまり、サーバサイドに保存されている情報をクライアントのリクエストと紐づけて取得している処理であり、セッション管理によってステートフルなレスポンスをまさに実現しているのがこの処理です。
セッションの破棄
最後にセッションの破棄ですが、今回のサンプルでは非常に簡素な内容での実装となっています。
セッションマネージャ構造体のマップから、セッションIDをキーに、delete関数で要素を削除しているだけです。
/* ******************* * * セッションの破棄 * ****** */ func (m *Manager) Destroy(sessionID string) { delete(m.database, sessionID) }
終わりに
今回、必要最低限でのセッションマネージャの実装例を提示し、解説していきました。
この他にも、セッションの有効期間の管理など、セッションマネージャ側に実装すべき機能が考えられます。
そのような機能については、また別の機会を設けてご紹介できればと思います。
また、このセッションマネージャは単体で使用することはできません。
次回の記事で実装・解説するsession.goとあわせて利用できる形になりますので、引き続きお付き合いください。