処理を実行するごとに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ファイルと合わせれば、後は自由です。