読者です 読者をやめる 読者になる 読者になる

nirasan's tech blog

趣味や仕事の覚え書きです。Linux, Perl, PHP, Ruby, Javascript, Android, Cocos2d-x, Unity などに興味があります。

Go 言語で gorilla/mux, gorilla/context, gorilla/sessions を使ったユーザーログイン機能のサンプル

はじめに

Go 言語練習のため gorilla/mux, gorilla/context, gorilla/sessions を使ったユーザーログイン機能を実装してみました。
ユーザーの管理はメモリ上の map 型変数にユーザー名とパスワードを入れているだけなので、ここをDBに変えるのが次の課題のつもりです。

package main

import (
	"net/http"
	"html/template"
	"github.com/gorilla/mux"
	"github.com/gorilla/context"
	"github.com/gorilla/securecookie"
	"github.com/gorilla/sessions"
	"errors"
)

var (
	// セッションストアの初期化
	store *sessions.CookieStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64))
	// 登録ユーザーをメモリ上で管理
	users map[string]string = make(map[string]string)
)

const (
	SessionName = "session-name"
	ContextSessionKey = "session"
)

func main() {
	r := mux.NewRouter()
	handleFunc(r, "/", rootHandler)
	handleFunc(r, "/register", registerGetHandler).Methods("GET")
	handleFunc(r, "/register", registerPostHandler).Methods("POST")
	handleFunc(r, "/login", loginGetHandler).Methods("GET")
	handleFunc(r, "/login", loginPostHandler).Methods("POST")
	handleFunc(r, "/logout", logoutGetHandler).Methods("GET")
	handleFunc(r, "/logout", logoutPostHandler).Methods("POST")
	http.ListenAndServe(":8080", r)
}

// アプリケーション共通処理を常に呼び出すための糖衣構文
func handleFunc(r *mux.Router, path string, fn http.HandlerFunc) *mux.Route {
	return r.HandleFunc(path, applicationHandler(fn))
}

// アプリケーション共通処理
func applicationHandler(fn http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// セッションの取得
		session, err := store.Get(r, SessionName)
		if err != nil {
			// 不正なセッションだった場合は作り直す
			session, err = store.New(r, SessionName)
			checkError(err)
		}
		context.Set(r, ContextSessionKey, session)
		// 個別のハンドラー呼び出し
		fn(w, r)
	}
}

// トップページ
func rootHandler(w http.ResponseWriter, r *http.Request) {
	type page struct {
		Username string
	}
	p := &page{}
	session, err := getSession(r)
	checkError(err)
	if v, ok := session.Values["username"]; ok {
		p.Username = v.(string)
	}
	executeTemplate(w, "template/index.html", p)
}

// 登録ページ
func registerGetHandler(w http.ResponseWriter, r *http.Request) {
	executeTemplate(w, "template/register.html", nil)
}

// 登録処理
func registerPostHandler(w http.ResponseWriter, r *http.Request) {
	// フォームのパース
	r.ParseForm()
	// フォームの値の取得
	username, password := r.Form["username"][0], r.Form["password"][0]
	// 空なら登録ページへ
	if username == "" || password == "" {
		http.Redirect(w, r, "/register", http.StatusFound)
		return
	}
	// ユーザー登録
	users[username] = password
	// 現在のユーザーをセッションで管理
	session, err := getSession(r)
	checkError(err)
	session.Values["username"] = username
	session.Save(r, w)
	// トップページへ
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

// ログインページ
func loginGetHandler(w http.ResponseWriter, r *http.Request) {
	executeTemplate(w, "template/login.html", nil)
}

// ログイン処理
func loginPostHandler(w http.ResponseWriter, r *http.Request) {
	// フォームのパース
	r.ParseForm()
	// フォームの値の取得
	username, password := r.Form["username"][0], r.Form["password"][0]
	// 空ならログイン画面へ
	if username == "" && password == "" {
		http.Redirect(w, r, "/login", http.StatusFound)
		return
	}
	// ユーザーが登録済みでありパスワードが一致したらログイン状態に
	if v, ok := users[username]; ok && v == password {
		session, err := getSession(r)
		checkError(err)
		session.Values["username"] = username
		session.Save(r, w)
		http.Redirect(w, r, "/", http.StatusSeeOther)
	} else {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
	}
}

// ログアウトページ
func logoutGetHandler(w http.ResponseWriter, r *http.Request) {
	executeTemplate(w, "template/logout.html", nil)
}

// ログアウト処理
func logoutPostHandler(w http.ResponseWriter, r *http.Request) {
	session, err := getSession(r)
	checkError(err)
	delete(session.Values, "username")
	session.Save(r, w)
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

// セッションの取得
func getSession(r *http.Request) (*sessions.Session, error) {
	if v := context.Get(r, ContextSessionKey); v != nil {
		return v.(*sessions.Session), nil
	}
	return nil, errors.New("failed to get session")
}

// テンプレートの実行
func executeTemplate(w http.ResponseWriter, name string, data interface{}) {
	t, err := template.ParseFiles(name)
	checkError(err)
	err = t.Execute(w, data)
	checkError(err)
}

// エラーチェック
func checkError(err error) {
	if err != nil {
		panic(err)
	}
}

おわりに

net/http のハンドラーをチェインさせて共通処理を切り出すパターンは「Go言語によるWebアプリケーション開発」読んだらちゃんと書いてあったのでちゃんと読もうと思いました。
学習目的で gorilla/mux で実装してみてて実際勉強になってますが、普通に web アプリ作るならたぶん goji とか WAF を使った方がいい気がしてきてます。