Go言語でWebサイトを作ってみる （8: 管理者のみ見られるページを作ってみる）

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

前回のユーザーページ作成篇 http://qiita.com/y_ussie/items/45d916f741e12c4ec9b7 の続きになります。
今回は、前回作成したユーザー情報を拡張して権限を持たせるようにし、管理者のみ見られるページを作っていきたいと思います。

# やりたいこと

今回は以下を実装してみようと思います。

* ユーザー情報に、ユーザーの権限（一般ユーザー、管理者）を持たせるようにする
* 管理者がログインした場合には、管理者のホーム画面に遷移するようにする
* 管理者のホーム画面以下の階層は、管理者しか見られないようにする
* 管理者しか見られない階層に、ユーザー一覧画面を作成する

## ユーザー情報の拡張

まずはユーザー情報に、ユーザーの権限を保持するフィールドを追加します。
権限を保持するフィールドとして、`Roles []Role` を追加しています。
複数の権限を持てるよう、スライスにしています。

```go:model/user.go
// User はユーザーの情報を表します。
type User struct {
	ID       ID        `json:"id"`
	UserID   string    `json:"user_id"`
	Password StringMD5 `json:"password"`
	FullName string    `json:"full_name"`
	Roles    []Role    `json:"roles"`
}

// Copy は情報のコピーを行います。
func (u *User) Copy(f *User) {
	u.ID = f.ID
	u.UserID = f.UserID
	u.Password = f.Password
	u.FullName = f.FullName
	u.Roles = make([]Role, len(f.Roles))
	copy(u.Roles, f.Roles)
}

// Role はユーザーの権限を表します。
type Role string
```

JSONデータにも以下のように "role" フィールドを追加します。
一般ユーザーの場合には "user" のみ、管理者ユーザーの場合には "user", "admin" の両方の権限を持たせています。

```json:data/users.json
[
    {
        "id": "806d5832-e253-40ca-be80-a00878898c37",
        "user_id": "a-chan",
        "password": "5f4dcc3b5aa765d61d8327deb882cf99",
        "full_name": "Nishiwaki Ayaka",
        "roles": ["user"]
    },
    {
        "id": "e7bdce91-c9bb-4a29-91f8-750ad4b18bfa",
        "user_id": "kashiyuka",
        "password": "5f4dcc3b5aa765d61d8327deb882cf99",
        "full_name": "Kashino Yuka",
        "roles": ["user"]
    },
    {
        "id": "b87c826e-429b-4f50-aa35-b99f11a40b08",
        "user_id": "nocchi",
        "password": "5f4dcc3b5aa765d61d8327deb882cf99",
        "full_name": "Omoto Ayano",
        "roles": ["user"]
    },
    {
        "id": "c7a70e10-0c89-48ab-9b30-43fad782cbe4",
        "user_id": "admin",
        "password": "5f4dcc3b5aa765d61d8327deb882cf99",
        "full_name": "Administrator",
        "roles": ["user","admin"]
    }
]
```

## ログイン時の管理者権限チェックと管理者ホーム画面への遷移

次に、ログイン時にユーザーの権限をチェックし、管理者であった場合には管理者のホーム画面に遷移させるようにします。ログイン処理は以下のように変更します。

```go:handler.go
// 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)
	}
	// ログインしたユーザーが管理者かチェックする
	isAdmin, err := CheckRoleByUserID(userID, model.RoleAdmin)
	if err != nil {
		c.Echo().Logger.Debugf("Admin Role Check Error. [%s]", userID, err)
		isAdmin = false
	}
	if isAdmin {
		// 管理者でログインした場合には管理者のホーム画面に遷移する
		c.Echo().Logger.Debugf("User is Admin. [%s]", userID)
		return c.Redirect(http.StatusTemporaryRedirect, "/admin")
	}
	return c.Redirect(http.StatusTemporaryRedirect, "/users/"+userID)
}
```

新規作成した `CheckRoleByUserID()` にてログイン対象のユーザーが管理者権限を持っているかチェックし、管理者であった場合には /admin に遷移させるようにしています。

`CheckRoleByUserID()` は以下のような関数です。ユーザーIDをキーにユーザー情報を取得し、`User#Roles` に指定された権限が含まれているかチェックしています。

```go:auth.go
// CheckRoleByUserID はユーザーが指定された権限を持っているか確認します。
func CheckRoleByUserID(userID string, role model.Role) (bool, error) {
	users, err := userDA.FindByUserID(userID, model.FindFirst)
	if err != nil {
		return false, err
	}
	user := &users[0]
	for _, v := range user.Roles {
		if v == role {
			return true, nil
		}
	}

	return false, nil
}
``` 

## 管理者権限チェックのためのミドルウェア適用

とりあえず /admin への遷移はできましたが、このままでは /admin 以下は誰でも見られてしまいます。
/admin のリクエストハンドラにて権限をチェックしてもいいのですが、今後管理者の画面は増えていきますので、 /admin 以下の階層をグループにし、グループ全体に管理者権限チェックのミドルウェアを適用することでチェックを行ってみたいと思います。

ミドルウェアのコードは以下です。リクエストハンドラを受け取り、管理者権限チェックを行って管理者であった場合にはそのままリクエストハンドラを実行して結果を返します。
管理者でなかった場合にはリクエストハンドラを実行せず、エラーページを返します。

```go:auth.go
// MiddlewareAuthAdmin は管理者権限を持ったユーザーのみが参照できる
// ページに適用するMiddlewareです。
func MiddlewareAuthAdmin(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		isAdmin, err := CheckRole(c, model.RoleAdmin)
		if err != nil {
			c.Echo().Logger.Debugf("Admin Page Role Error. [%s]", err)
			isAdmin = false
		}
		if !isAdmin {
			msg := "管理者でログインしていません。"
			return c.Render(http.StatusOK, "error", msg)
		}
		return next(c)
	}
}
```

このミドルウェアを、以下のようにして /admin 以下の階層に適用します。
`Echo#Group()` にて /admin 以下を admin グループとし、ミドルウェアを適用します。
admin グループのルーティング設定には、 /admin 以下のパスを設定します。

/admin の場合 ""、 /admin/users の場合 "/users" になります。

```go:handler.go
// ルーティングに対応するハンドラを設定します。
func setRoute(e *echo.Echo) {
	e.GET("/", handleIndexGet)
	e.GET("/login", handleLoginGet)
	e.POST("/login", handleLoginPost)
	e.POST("/logout", handleLogoutPost)
	e.GET("/users/:user_id", handleUsers)
	e.POST("/users/:user_id", handleUsers)

	// 管理者のみが参照できるページ
	admin := e.Group("/admin", MiddlewareAuthAdmin)
	admin.GET("", handleAdmin)
	admin.POST("", handleAdmin)
	admin.GET("/users", handleAdminUsersGet)
}
```

## 管理者ホーム以下の階層に、ユーザー一覧画面を作成する

あとは、 /admin にて管理者のホーム画面、 /admin/users にてユーザー一覧が表示されるよう実装します。

/admin のテンプレートは以下のようになっています。リクエストハンドラではこのテンプレートを表示しているだけです。

```html:template/admin.html
{{define "content"}}
<h2>Administrator Page</h2>
<hr />
<form action="/admin/users" method="GET">
    <input type="submit" value="ユーザー一覧" style="width:100px"/>
</form>
<hr />
<form action="/logout" method="POST">
    <input type="submit" value="ログアウト" style="width:100px"/>
</form>
{{end}}
```

/admin/users のテンプレートは以下のようになっています。

```html:template/admin_users.html
{{define "content"}}
<h2>ユーザー一覧</h2>
<hr />
<table class="table">
<thead class="thead">
<tr>
<th>User ID</th>
<th>Full Name</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{{range .}}
<tr>
<td>{{.UserID}}</td>
<td>{{.FullName}}</td>
<td>{{.Roles}}</td>
</tr>
{{end}}
</tbody>
</table>
<form action="/admin" method="POST">
    <input type="submit" value="管理者画面に戻る" style="width:150px"/>
</form>
{{end}}
```

/admin/users のリクエストハンドラは以下のようになっています。
今回新規作成した `UserDataAccessor#FindAll()` にて、ユーザー情報を全件取得し、テンプレートに渡して表示させています。

```go:handler.go
// GET:/admin/users
func handleAdminUsersGet(c echo.Context) error {
	users, err := userDA.FindAll()
	if err != nil {
		return c.Render(http.StatusOK, "error", err)
	}
	return c.Render(http.StatusOK, "admin_users", users)
}
```

# 実行結果

## 管理者ユーザーでのログイン

/login にて、ユーザー「admin」でログインします。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/70686/255f5c7d-429f-b346-34a5-fe95e8dc8451.png)

## 管理者ホーム画面

管理者ですので、管理者ホーム画面（/admin）が表示されます。

![2.png](https://qiita-image-store.s3.amazonaws.com/0/70686/4adfd013-1354-4e2a-4b78-e5381cd20619.png)

「ユーザー一覧」ボタンを押下します。

## ユーザー一覧画面

ユーザー一覧画面が表示されます。

![3.png](https://qiita-image-store.s3.amazonaws.com/0/70686/cbd380a9-ca9a-355b-370e-2e90e208b49e.png)

## 一般ユーザーでのログイン

次に、管理者でないユーザー「a-chan」でログインしてみます。

![4.png](https://qiita-image-store.s3.amazonaws.com/0/70686/c5b31ad0-55ab-9810-c8a7-9d134855be1a.png)

ログインした後、アドレスバーに /admin を入力してみます。

## エラー画面

以下のように、管理者でログインしていない旨のエラー画面が表示されます。

![5.png](https://qiita-image-store.s3.amazonaws.com/0/70686/419894b3-4f6a-85bc-b130-c52bff347599.png)

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

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


# 次回予定

管理者画面にて、ユーザー情報の編集・追加・削除ができるようにしてみます。