Go言語でWebサイトを作ってみる （7: ログインしたユーザーしか見られないページを作ってみる）

> すべての記事の一覧はこちら。
**Go言語でWebサイトを作ってみる：目次**
http://qiita.com/y_ussie/items/8fcc3077274449478bc9

前回のJSONからのユーザー情報読み込み篇 http://qiita.com/y_ussie/items/8704ce209704bf191e63 の続きになります。
今回は、前回作成したユーザー情報読み込み機能を使用して、ログイン機能を作っていきたいと思います。

# やりたいこと

前回は

* ユーザー情報をJSONから読み込んで参照できるようにする

まで作成しましたので、今回は

* ログイン画面からユーザーID/パスワードでログインできるようにする
* ログインユーザーしか見られないページを作ってみる

を実装してみたいと思います。

まずは認証関係の処理をまとめたコードを作成します。

## auth.go
https://github.com/yoshinoyaussie/golang-website-sample/blob/chapter-7/webserver/auth.go

```go:auth.go
package main

import (
	"errors"

	"./model"
	"./session"
	"github.com/labstack/echo"
)

// auth.goが返すエラーの定義
var (
	ErrorInvalidUserID   = errors.New("Invalid UserID")
	ErrorInvalidPassword = errors.New("Invalid Password")
	ErrorNotLoggedIn     = errors.New("Not Logged In")
)

// UserLogin はユーザーログイン時の処理を行います。
func UserLogin(c echo.Context, userID string, password string) error {
	users, err := userDA.FindByUserID(userID, model.FindFirst)
	if err != nil {
		return err
	}
	user := &users[0]
	encodePassword := model.EncodeStringMD5(password)
	if user.Password != encodePassword {
		return ErrorInvalidPassword
	}
	sessionID, err := sessionManager.Create()
	if err != nil {
		return err
	}
	err = session.WriteCookie(c, sessionID)
	if err != nil {
		return err
	}
	sessionStore, err := sessionManager.LoadStore(sessionID)
	if err != nil {
		return err
	}
	sessionData := map[string]string{
		"user_id": userID,
	}
	sessionStore.Data = sessionData
	err = sessionManager.SaveStore(sessionID, sessionStore)
	if err != nil {
		return err
	}

	return nil
}

// UserLogout はユーザーログアウト時の処理を行います。
func UserLogout(c echo.Context) error {
	sessionID, err := session.ReadCookie(c)
	if err != nil {
		return err
	}
	err = sessionManager.Delete(sessionID)
	if err != nil {
		return err
	}

	return nil
}

// CheckUserID は指定されたユーザーIDでログインしているか確認します。
func CheckUserID(c echo.Context, userID string) error {
	sessionID, err := session.ReadCookie(c)
	if err != nil {
		return err
	}
	sessionStore, err := sessionManager.LoadStore(sessionID)
	if err != nil {
		return err
	}
	sessionUserID, ok := sessionStore.Data["user_id"]
	if !ok {
		return ErrorNotLoggedIn
	}
	if sessionUserID != userID {
		return ErrorInvalidUserID
	}

	return nil
}
```

auth.go には、以下の3つの関数を実装しています。

### UserLogin()

* パラメータとしてユーザーID, パスワードを受け取り、ユーザー情報と照合して一致するかチェックします。
* チェックの結果、一致した場合には以下の処理を行います。
  * セッションデータストアを作成し、セッションIDを取得します。
  * セッションIDをCookieに保存します。
  * セッションIDをキーにして、セッションデータストアにユーザーIDを保存します。

### UserLogout()

* CookieよりセッションIDを取得します。
* セッションIDをキーにして、セッションデータストアの削除を行います。

### CheckUserID()

* CookieよりセッションIDを取得します。
* セッションIDをキーにして、セッションデータストアよりユーザーIDを取得します。
* パラメータとして受け取ったユーザーIDと、セッションデータストアより取得したユーザーIDが一致するかチェックします。

セッションデータストアに関する処理は、以前のセッションデータストア篇
http://qiita.com/y_ussie/items/b1db86b0b54ec69bb928
で作成したものです。

これらの関数を使用して、ログイン画面関連のリクエストハンドラを作成します。

https://github.com/yoshinoyaussie/golang-website-sample/blob/chapter-7/webserver/handler.go

```go:handler.go
	e.GET("/login", handleLoginGet)
	e.POST("/login", handleLoginPost)
	e.POST("/logout", handleLogoutPost)
```

```go:handler.go
// GET:/login
func handleLoginGet(c echo.Context) error {
	return c.Render(http.StatusOK, "login", nil)
}

// POST:/login
func handleLoginPost(c echo.Context) error {
	userID := c.FormValue("userid")
	password := c.FormValue("password")
	err := UserLogin(c, userID, password)
	if err != nil {
		c.Echo().Logger.Debugf("User[%s] Login Error. [%s]", userID, err)
		msg := "ユーザーIDまたはパスワードが誤っています。"
		data := map[string]string{"user_id": userID, "password": "", "msg": msg}
		return c.Render(http.StatusOK, "login", data)
	}
	return c.Redirect(http.StatusTemporaryRedirect, "/users/"+userID)
}

// POST:/logout
func handleLogoutPost(c echo.Context) error {
	err := UserLogout(c)
	if err != nil {
		c.Echo().Logger.Debugf("User Logout Error. [%s]", err)
		return c.Render(http.StatusOK, "login", nil)
	}
	msg := "ログアウトしました。"
	data := map[string]string{"user_id": "", "password": "", "msg": msg}
	return c.Render(http.StatusOK, "login", data)
}
```

## GET /login

ログイン画面を表示します。「ログイン」押下で POST:/login に遷移します。

## POST /login

ログイン処理を行います。ここで UserLogin() を使用しています。
ログインが正常に行われると、/users/<ユーザーID> のページに遷移します。

## POST /logout

ログアウト処理を行い、ログイン画面を表示します。

ここまででログインの処理は作成できましたので、最後に /users/<ユーザーID> で遷移する各ユーザーのページに、現在ログイン中かどうかのチェックを追加します。 

```go:handler.go
// GET:/users/:user_id
// POST:/users/:user_id
func handleUsers(c echo.Context) error {
	userID := c.Param("user_id")
	err := CheckUserID(c, userID)
	if err != nil {
		c.Echo().Logger.Debugf("User Page[%s] Roll Error. [%s]", userID, err)
		msg := "ログインしていません。"
		return c.Render(http.StatusOK, "error", msg)
	}
	users, err := userDA.FindByUserID(c.Param("user_id"), model.FindFirst)
	if err != nil {
		return c.Render(http.StatusOK, "error", err)
	}
	user := users[0]
	return c.Render(http.StatusOK, "user", user)
}
```

最初に CheckUserID() でパスパラメータにて指定されたユーザーIDでログインしているかチェックし、ログインしていない場合にはエラーページを表示します。
ログインしている場合にはユーザーのページを表示します。

# 実行結果

## ログイン画面

/login で以下のログイン画面が表示されます。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/70686/580841db-d8d1-6f30-5d3d-242d84e057dd.png)

ユーザー「a-chan」でログインしてみます。

![2.png](https://qiita-image-store.s3.amazonaws.com/0/70686/f5f794df-da0d-ba8d-e839-df89f3b74e46.png)

## ユーザー画面

/users/a-chan に遷移し、ユーザー「a-chan」の画面が表示されます。

![3.png](https://qiita-image-store.s3.amazonaws.com/0/70686/51a1cddc-adb7-0f02-eca7-7b803c6c2015.png)

この状態で、アドレスバーに /users/nocchi を入力し、ユーザー「nocchi」のページを表示しようとしてみます。

## 認証エラー

エラー画面が表示されました。

![4.png](https://qiita-image-store.s3.amazonaws.com/0/70686/d2f71858-5d42-2418-b27f-746fcb09d9e1.png)

再度 /users/a-chan を入力し、ユーザー「a-chan」のページを表示してみます。
ユーザー「a-chan」でログイン中なので、こちらは正常に表示されます。

![5.png](https://qiita-image-store.s3.amazonaws.com/0/70686/7d29ad96-7a81-2f9a-b8dd-6aae72c94773.png)

「ログアウト」を押下します。

## ログアウト

ログイン画面に戻ります。

![6.png](https://qiita-image-store.s3.amazonaws.com/0/70686/b67a60aa-4800-fbc3-eebf-9c2a56aa7e80.png)


# 今回使用したコードについて

今回使用したコードの一式は以下の GitHub に置いてあります。
https://github.com/yoshinoyaussie/golang-website-sample/tree/chapter-7


# 次回予定

すべてのユーザーのページを参照できる管理者ユーザーを作成してみます。
また、ユーザー情報の編集・追加・削除ができるようにしてみます。