Sessionの期限更新と清掃【Go】
処理を実行するごとにsessionの有効期限を更新し、ログアウト時に、他のユーザーに関するsession情報も含めて、
一定時間を超えたsessionを削除する機能を用意します。
過去の記事を基にしています。
Session【Go】 - 技術向上
bcryptでパスワードをhash化【Go】 - 技術向上
main.go (package main)
type user struct { UserName, First, Last, Role string Password []byte } type session struct { un string lastActivity time.Time // 最終処理時間を保持 } var tpl *template.Template var dbUsers = make(map[string]user) var dbSessions = make(map[string]session) var dbSessionsCleaned time.Time // 最後にdbSessionsを清掃した時間を保持 const sessionLength int = 30 // sessionの期限を30(秒)に指定(MaxAgeの指定に使う) func init() { tpl = template.Must(template.ParseGlob("templates/*")) dbSessionsCleaned = time.Now() // 初期化 } func main() { http.HandleFunc("/", index) http.HandleFunc("/bar", bar) http.HandleFunc("/signup", signup) http.HandleFunc("/login", login) http.HandleFunc("/logout", logout) http.Handle("/favicon.ico", http.NotFoundHandler()) http.ListenAndServe(":8080", nil) } func index(w http.ResponseWriter, req *http.Request) { u := getUser(w, req) tpl.ExecuteTemplate(w, "index.gohtml", u) } func bar(w http.ResponseWriter, req *http.Request) { if !alreadyLoggedIn(w, req) { http.Redirect(w, req, "/", http.StatusSeeOther) return } u := getUser(w, req) if u.Role != "007" { http.Error(w, "Must be 007", http.StatusForbidden) return } tpl.ExecuteTemplate(w, "bar.gohtml", u) } func signup(w http.ResponseWriter, req *http.Request) { if alreadyLoggedIn(w, req) { http.Redirect(w, req, "/", http.StatusSeeOther) return } if req.Method == http.MethodPost { un := req.FormValue("username") p := req.FormValue("password") f := req.FormValue("firstname") l := req.FormValue("lastname") r := req.FormValue("role") if _, ok := dbUsers[un]; ok { http.Error(w, "Username already taken.", http.StatusForbidden) return } c := makeSession(w) c.MaxAge = sessionLength // 有効期限は30秒 http.SetCookie(w, c) dbSessions[c.Value] = session{ un: un, lastActivity: time.Now(), // 最終処理時間を更新 } bs, err := bcrypt.GenerateFromPassword([]byte(p), bcrypt.MinCost) // 外部パッケージを用いたハッシュ化 if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } u := user{ UserName: un, First: f, Last: l, Role: r, Password: bs, // ハッシュ化済パスワード } dbUsers[un] = u // テーブルに格納 http.Redirect(w, req, "/", http.StatusSeeOther) return } tpl.ExecuteTemplate(w, "signup.gohtml", nil) } func login(w http.ResponseWriter, req *http.Request) { if alreadyLoggedIn(w, req) { http.Redirect(w, req, "/", http.StatusSeeOther) return } if req.Method == http.MethodPost { un := req.FormValue("username") p := req.FormValue("password") u, ok := dbUsers[un] // 存在したらokはtrue if !ok { http.Error(w, "Username or/and Password do not match.", http.StatusForbidden) return } err := bcrypt.CompareHashAndPassword(u.Password, []byte(p)) // 外部パッケージを用いた、ハッシュ化済パスワードとbyte配列の照合 if err != nil { // 合わなかった場合 http.Error(w, "Username or/and Password do not match.", http.StatusForbidden) return } c := makeSession(w) c.MaxAge = sessionLength // 有効期限は30秒 http.SetCookie(w, c) dbSessions[c.Value] = session{ un: un, lastActivity: time.Now(), // 最終処理時間を更新 } http.Redirect(w, req, "/", http.StatusSeeOther) return } tpl.ExecuteTemplate(w, "login.gohtml", nil) } func logout(w http.ResponseWriter, req *http.Request) { if !alreadyLoggedIn(w, req) { http.Redirect(w, req, "/", http.StatusSeeOther) return } c, _ := req.Cookie("session") delete(dbSessions, c.Value) // 自身のsessionを削除 c.MaxAge = -1 http.SetCookie(w, c) // Cookieを削除 if time.Now().Sub(dbSessionsCleaned) > (time.Second * 30) { go cleanSessions() // dbSessions全体の清掃を実行 } http.Redirect(w, req, "/", http.StatusSeeOther) return }
session.go (package main)
func getUser(w http.ResponseWriter, req *http.Request) user { c, err := req.Cookie("session") if err != nil { c = makeSession(w) } var u user if ss, ok := dbSessions[c.Value]; ok { // dbSessionsに保存したユーザー名をキーに、dbUsersからuser情報を取得するため u = dbUsers[ss.un] } return u } func alreadyLoggedIn(w http.ResponseWriter, req *http.Request) bool { c, err := req.Cookie("session") if err != nil { return false } ss, ok := dbSessions[c.Value] if ok { ss.lastActivity = time.Now() // 最終処理時間を更新 dbSessions[c.Value] = ss } _, ok = dbUsers[ss.un] // 存在していたら(ログインしていたら)okはtrue c.MaxAge = sessionLength http.SetCookie(w, c) return ok } func makeSession(w http.ResponseWriter) *http.Cookie { id, err := uuid.NewV4() // 外部パッケージを用いたunique idの生成 if err != nil { log.Fatal(err) } return &http.Cookie{ Name: "session", Value: id.String(), // 文字列化 } } func cleanSessions() { fmt.Println("BEFORE CLEAN") // デモ用 for k, v := range dbSessions { if time.Now().Sub(v.lastActivity) > (time.Second * 30) { // デモ用のため、時間を短くしている delete(dbSessions, k) } } dbSessionsCleaned = time.Now() fmt.Println("AFTER CLEAN") // デモ用 }
ログアウト処理時に、定期的にdbSessionsを清掃する処理をgroutineで書いています。
mapの要素の「削除」の場合には、競合によるエラーは発生しないため、この方法で問題ありません。
更新の場合は、複数ユーザーが同時にログアウトを行なうと、競合エラーが発生する可能性があります。
各種.gohtmlの紹介はしませんが、formのinput要素をgoファイルと合わせれば、後は自由です。