ginでWebアプリのログイン処理を実装してセッションについて学ぶ

この記事の対象者

Go言語で書かれたWebアプリケーションフレームワークであるginでCookieを使用した基礎的なセッション管理の実装について知りたい方向け

github.com

この記事内で紹介している実装の全体が見たい方はこちら

github.com

セッションとは

まとめると「Webアプリケーションにアクセス中のユーザー固有の情報を保存する仕組み」

セッションとcookieの関係

  • セッション
    • Webアプリケーションにアクセス中のユーザー固有の情報を保存する仕組み
    • セッション情報はクライアント側(Cookie)、サーバ側(ファイル, DB, KVS, ...)などどこに保存するかはWebアプリ開発者が指定可能
  • Cookie
    • データをクライアント(Webブラウザ)に保存する仕組み
    • セッション情報の保存先として標準的に使用される

gin-contrib/sessions/cookieを使用してセッション管理する

  • cookieをセッションストアとして使用する場合、session情報はSet-cookieヘッダでクライアントに送信されブラウザに保存される
  • よくあるセッションIDのみCookieに保存するパターンはセッション情報はサーバ側に保存されていて、Cookieに保存されたセッションIDをサーバに送信して管理しているが、gin-contrib/sessions/cookieはセッション情報をすべてCookieに保存する仕組みとなっている

https://github.com/litencatt/playground-gin/pull/1/files

package main

import (
    "playground-gin/handler"

    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    store := cookie.NewStore([]byte("secret"))
    r.Use(sessions.Sessions("sample_session", store))

    r.LoadHTMLGlob("templates/*.go.tmpl")
    r.GET("/", handler.RootHandler)
    r.GET("/foo", handler.FooHandler)
    r.GET("/bar", handler.BarHandler)

    r.Run(":8090")
}

以下セッションへの値の保存と取得例

func RootHandler(c *gin.Context) {
    session := sessions.Default(c)
    session.Set("session_value", 1)
    session.Save()

    c.HTML(http.StatusOK, "root.go.tmpl", gin.H{
        "title": "index",
        "title": "root",
    })
}

func FooHandler(c *gin.Context) {
    session := sessions.Default(c)
    val := session.Get("session_value")
    var v int
    if val != nil {
        v = val.(int) + 1
    }
    session.Set("session_value", v)
    session.Save()
    c.HTML(http.StatusOK, "foo.go.tmpl", gin.H{
        "title": "foo",
        "value": val,
    })
}

Chromeの場合DevToolのApplicationタブよりCookiesを見るとsample_sessionという名前でデータが保存されているのがわかる

セッション情報を複数扱いたい場合

sessions.SessionsMany()を使用することで2つ以上のセッションを扱うことが可能 https://github.com/litencatt/playground-gin/pull/2/files

// main.go
    sessionNames := []string{"session_foo", "session_bar"}
    r.Use(sessions.SessionsMany(sessionNames, store))

使用する場合はセッション名を指定してsessions.DefaultMany()で取得する

   sessionFoo := sessions.DefaultMany(c, "session_foo")
    sessionFoo.Set("value", 1)
    sessionFoo.Save()

    sessionBar := sessions.DefaultMany(c, "session_bar")
    sessionBar.Set("value", 1)
    sessionBar.Save()

ログインとセッションの関係

セッションはアクセス中のユーザー固有の情報を保存する仕組みであることから、これを利用してログイン処理などで認証後の状態を表す情報をセッションに保存することで以降のリクエストは認証済みのユーザーからのリクエストとして処理することができるようになり、マイページなどそのユーザーしか閲覧することができないような情報をユーザーに参照させることができるようになる。

これをginで実装する場合、今回以下のようにPOST /loginのハンドラでセッション内にloginというキーに対して1という値を設定しこれをログイン情報として保存することで実現してみました。 ここでは超簡略化しているため実際にはログインフォームより/loginにPOSTされてきたメールアドレスやパスワードを使用した認証処理の実装が必要になると思います。 https://github.com/litencatt/playground-gin/pull/3

func PostLoginHandler(c *gin.Context) {
    sessionFoo := sessions.DefaultMany(c, "session_foo")
    sessionFoo.Set("login", 1)
    sessionFoo.Save()

    c.Redirect(http.StatusFound, "/mypage")
}

また、未ログインユーザーの/mypageへのアクセスを拒否するため現在の認証処理を行うためのmiddlewareを登録します。これにより指定したエンドポイントに対してのアクセス時にUseで登録した処理が事前に呼び出されるようになります。

   a := r.Group("/")
    a.Use(handler.AuthCheck())
    {
        a.GET("/mypage", handler.MyPageHandler)
    }

今回登録したAuthCheck()ではセッション情報にもつloginの値が1となっているかをチェックしており、1でない場合はエラーページにリダイレクトするようにしています。 これによりログインしている場合のみ/mypageへのアクセスができるようにすることができました。

func AuthCheck() gin.HandlerFunc {
    return func(c *gin.Context) {
        sessionFoo := sessions.DefaultMany(c, "session_foo")
        login := sessionFoo.Get("login")
        if login != 1 {
            c.Redirect(http.StatusFound, "/error")
        }
    }
}

未ログイン時はエラーページに飛ばされる

ログイン後はマイページにアクセスできる

参考