#はじめに
GolangのGinフレームワークを使ってWebサイトを作っていますが、ログイン・ログアウトを作る必要があってセッション管理が必要になりました。
いろいろ調べてたら
Ginのmiddlewareにセッション情報を扱いやすくしてくれるものがあったのでこれを使います。
https://github.com/gin-gonic/contrib/tree/master/sessions
go get github.com/gin-gonic/contrib/sessions
#セッション管理をするための準備
今回はこんな感じでセッション情報の構造体を宣言しておきます。
SessionInfo struct {
UserID interface{} //ログインしているユーザのID
NickName interface{} //ログインしているユーザの名前
IsSessionAlive bool //セッションが生きているかどうか
}
※DBには会員登録をしたユーザの情報が既にあるとします。
※Localの3306ポートにそうした情報が入ってるMySQLサーバが立っているものとします。
https://github.com/notvitor/go-gin-boilerplate
今回、こちらのソースをお借りして改造しています。以下に書いてあることを試したい人はこのソースを借りて試すと良いと思います。
#やりたいこと
①メールアドレスとパスワードをHTMLからPOSTする。
②GolangでそのPOST内容を文字列として抽出して変数にセット。
③その文字列を元に、DBにアクセスしてメールアドレスとパスワードのセットを持つユーザ情報を探す。
④もしユーザ情報が引けたら、セッション構造体に値を入れて保存することでCookieにセッション情報を保持させる。
やっていきましょう。
#①メールアドレスとパスワードをHTMLからPOSTする。
ログインページのHTML(最小限のものだけ見せています。)
<div class="col-md-offset-4 col-md-4">
<form class="form-signin" role="form" method="POST" action="./signin" onsubmit="DisableButton();">
<h2 class="form-signin-heading">ログイン</h2>
メールアドレス
<input name="email" type="text" class="form-control" placeholder="Email address" required>
パスワード
<input name="password" type="password" class="form-control" placeholder="Password" required>
<br>
<button id="submitbutton" class="btn btn-lg btn-warning btn-block" type="submit">ログイン</button>
</form>
</div>
<!--Emailやパスワードが間違っていた時の処理はここでは省略-->
Bootstrap使っていたので少しClass名などがごちゃごちゃしていますが、大体こんな感じです。
こんな画面ができてると思ってください。
#②GolangでそのPOST内容を文字列として抽出して変数にセット。
文字列として抽出する前に上のログイン画面を表示するロジックを説明します。
Go ginでのルーティング
func main() {
loadTemplates()
server = gin.Default()
store := sessions.NewCookieStore([]byte("secret"))
server.Use(sessions.Sessions("SessionName", store))//SessionNameは任意
server.Static("/public/css/", "./public/css")
server.Static("/public/js/", "./public/js/")
server.Static("/public/fonts/", "./public/fonts/")
server.Static("/public/img/", "./public/img/")
server.GET("/", IndexRouter)
server.GET ("/signin", SigninFormRoute) //ログインページ表示
server.POST("/signin", Signin) //ログイン処理
server.Run(":3000") //これを書くとgo run server.goをしたらlocalohost:3000でWebサイトが立ち上がっている
↑ルーティングを行うmain関数。またWebサーバを立ち上げるのもここ。
func loadTemplates() {
baseTemplate := "templates/layout/_base.html"
templates = make(map[string]*template.Template)
templates["index"] = template.Must(template.ParseFiles(baseTemplate, "templates/home/index.html"))
templates["signin"] = template.Must(template.ParseFiles(baseTemplate, "templates/account/signin.html"))
}
↑HTMLテンプレートをMapしておく
func SigninFormRoute(g *gin.Context) {
server.SetHTMLTemplate(templates["signin"])
g.HTML(200, "_base.html", nil)
}
↑GETリクエストを受けた時の処理はこんな感じ。HTMLを見せるだけ。
##localhost:3000/signinにアクセス(GET)してHTMLページを表示させるには
server.GET ("/signin", SigninFormRoute) //ログインページ表示
で、localhost:3000/signinにGETリクエスト(ブラウザでページを表示させるときはGETリクエストがされてる)された時には、SigninFormRoute()を呼びますよという宣言
肝心のSinginFormRoute()には、HTMLtemplateを埋め込んで、_base.htmlを表示するように処理をしています。
_base.htmlは
ヘッダー宣言
{{ロードしたHTMLテンプレートを表示}}
フッター
という構造になっていて、つまりこうした処理をすることで、フッターとヘッダーがついたログイン画面をユーザに提示することができます。
テンプレートはもちろん付け替えることができるので、例えば["index"]ならトップページのHTMLテンプレートを読み込ませてログイン画面と共通のヘッダーとフッターのついたトップページを提示させます。
さて、以上でブラウザからアクセスされた時にどんな挙動をするのかが理解ったと思うので、POST内容を受け取る方に行きましょう。
##POSTリクエスト(テキストデータを送信)を受けた時の処理
func Signin(g *gin.Context) {
isExist, user := HTTPRequestManager.IsLoginUserExist(g)
if isExist {
SessionManager.Login(g, user)
}
info := SessionManager.GetSessionInfo(g) //Session情報を取得する
server.SetHTMLTemplate(templates["index"])
g.HTML(200, "_base.html", gin.H{
"SessionInfo": info,
})
}
この中でPOSTを受け取って処理をしているのは
isExist, user := HTTPRequestManager.IsLoginUserExist(g)
です。
内容は以下のコードです。
func IsLoginUserExist(g *gin.Context) (bool, DBManager.DBUsers) {
httpRequest := g.Request
httpRequest.ParseForm() //これをやらないとリクエストが取れない。
email := httpRequest.Form["email"][0]
encryptedPassword := toHash(httpRequest.Form["password"][0])
u := DBManager.GetLoginUser(email, encryptedPassword ,dbSession.Slave)
return u.ID != 0, u
}
httpRequest.Form["email"][0]
がPOST内容を文字列として抽出して変数にセットするという動作をしています。
[0]がついているのでわかると思いますが、POSTされた情報のうち、name="email"が複数あればもちろんその複数をとってきて配列として値を返します。(今回は1つなので[0]で取る)
#③その文字列を元に、DBにアクセスしてメールアドレスとパスワードのセットを持つユーザ情報を探す。
u := DBManager.GetLoginUser(email, encryptedPassword ,dbSession.Slave)
がこれですね。dbSession.Slaveはあまり関係なくて、dbrで作ったMySQLサーバへアクセスするためのDBセッションです。
dbrを使ってDBから値を取ってくる方法は詳しくは以下を参照してください。
http://qiita.com/CST_negi/items/5e276ddc0412cefef7e3
func GetLoginUser(email string, password string, sess *dbr.Session) DBUsers {
var u DBUsers
sess.Select("*").From("users").Where("email = ? AND password = ?", email, password).Load(&u)
return u
}
これで登録されているユーザの情報を返しています。
#④もしユーザ情報が引けたら、セッション構造体に値を入れて保存することでCookieにセッション情報を保持させる。
もう一度POSTリクエストを受けた時の処理を見ましょう。
func Signin(g *gin.Context) {
isExist, user := HTTPRequestManager.IsLoginUserExist(g)
fmt.Println("Login true=Success : ", isExist) //デバッグ用
if isExist {
SessionManager.Login(g, user)
}
info := SessionManager.GetSessionInfo(g) //Session情報を取得する
server.SetHTMLTemplate(templates["index"])
g.HTML(200, "_base.html", gin.H{
"SessionInfo": info,
})
}
isExist, user := HTTPRequestManager.IsLoginUserExist(g)
ユーザが存在しているか(isExist)と、存在していた場合のユーザ情報を変数に格納しています。
if IsExist {
SessionManager.Login(g, user)
}
ここでユーザ情報が存在していた場合にセッションを作り出すという処理をしています。
func Login(g *gin.Context, user DBManager.DBUsers) {
session := sessions.Default(g)
session.Set("alive", true)
session.Set("userID", user.ID)
session.Set("nickName", user.NickName)
session.Save()
}
セッションを作り出す処理をしています。
セッションが生きてるか変数と、ログインしているユーザのIDとログインしているユーザの名前をセッションに保存しています。
以上で、ログインが実装できました。
#ログアウト方法(セッションを消す)
func ClearSession(g *gin.Context) {
session := sessions.Default(g)
session.Clear()
session.Save()
println("Session clear")
}
という感じでセッションをクリアして保存してください。
session.Save()をしないとセッションがクリアされません。
以上でセッション管理はできるかなと思います。
#HTMLテンプレートにセッション情報を埋め込んで制御する
先ほど_base.htmlを読んでテンプレートを読みこませることで、共通ヘッダーを作ることができる話をしましたが、
これで例えば _base.htmlで
ログイン済み(セッションが既にある)→マイページ、ログアウトリンクを持つ共通ヘッダーを表示
ログインしてない(セッションが無い)→会員登録、ログインリンクを持つ共通ヘッダーを表示
ということをしたいということがあると思います。
(セッションの状態によって、共通ヘッダーを変化させる。)
ここでもう一度POSTリクエストが飛んできた時の処理を見せますが、
func Signin(g *gin.Context) {
isExist, user := HTTPRequestManager.IsLoginUserExist(g)
fmt.Println("Login true=Success : ", isExist) //デバッグ用
if isExist {
SessionManager.Login(g, user)
}
info := SessionManager.GetSessionInfo(g) //Session情報を取得する
server.SetHTMLTemplate(templates["index"])
g.HTML(200, "_base.html", gin.H{
"SessionInfo": info,
})
}
では、
g.HTML(200, "_base.html", gin.H{
"SessionInfo": info,
})
でSessionの情報を_base.htmlに渡しています。(templates["index"]テンプレートを渡しているため、実質このテンプレートに対してもSession情報を渡しています。)
<ul class="nav navbar-nav navbar-right">
{{if .SessionInfo.IsSessionAlive}}
<li><a href="/mypage">マイページ</a></li>
<li><a href="/signout">ログアウト</a></li>
{{else}}
<li><a href="/signup">会員登録</a></li>
<li><a href="/signin">ログイン</a></li>
{{end}}
</ul>
_base.htmlで、ヘッダーをこのように書いていますが、
ログイン済み(セッションが既にある)→マイページ、ログアウトリンクを持つ共通ヘッダーを表示
ログインしてない(セッションが無い)→会員登録、ログインリンクを持つ共通ヘッダーを表示
をこれで実現しています。
以上です。