【go + gin + gorm】webアプリにログイン機能を追加してみる


[【Go+Gin+Gorm】初心者だから超簡単webサービス作ってみる](https://qiita.com/dai-maru/items/7e97fc6623375c7eb14a)の続きです。

今回はgithubに上げておきました。
https://github.com/ymdd1/mytweet

ここではユーザー登録とログイン画面を作ります。
セッションはやりません（おそらく次回）。
ログイン画面からusernameとpasswordを入力してDBに存在していたらトップ画面にリダイレクトされるようになっています。

# 外部ライブラリのインストール
続きの方はターミナルを開いて以下を実行してください。
ここから始める方は`main.go`や`crypto/crypto.go`のimportを見て`go run`してください。

```
// gormConnect()内で.envファイル（環境変数定義）から定数を取得するときに使います
go get github.com/joho/godotenv
// Usersテーブルにパスワードをそのまま保存するとセキュリティ的に危ないので、これを使って暗号化して保存します。
go get golang.org/x/crypto/bcrypt
```

# .envファイル（環境変数定義）を使用する
先ほどインストールした`godotenv`は、go言語で.envファイルを使うためのライブラリです。
[ハードコーディング](https://wa3.i-3-i.info/word12866.html)を避けるため、今回は使ってみようと思います。
.envファイルの中に定数を記述して、コード内でその定数を使うことができます。使い方もクソもないかもですが、[こちら](https://qiita.com/sola-msr/items/fb7d6889d7bd7a6705d0)を参照するとわかりやすいかもです。
`main.go`と同じ階層に`.env`ファイルを作り、前回の続きの方は以下のように記述してください。
これらはMySQLのDB名だったり、ユーザー名だったりを記載しています。

```shell:.env
mytweet_DBMS=mysql
mytweet_USER=test
mytweet_PASS=12345678
mytweet_DBNAME=test

```

# usernameをユニークに設定する
ログインを実装するにあたって、ログインフォームから受け取ったusernameをデータベースで検索して一意なユーザーを取得したいので、usernameはユニークである必要があります。
なので`main.go`ファイルのUser構造体を変更しました。
`gorm:"unique;not null"`の部分です。
これによってアプリ側で、重複するUsernameを新たに登録しようとしてもデータベース側で弾けます。

`db.AutoMigrate()`はテーブルや不足しているカラムとインデックスのみ生成します。データ保護のため、既存のカラム型の変更や未使用のカラムの削除はしないので、前回と同じテーブルを使う方、申し訳ないんですが一度Usersテーブルをtruncateしてください。ここら辺railsはActive Recordがやってくれますよね。
AutoMigrateについて詳しくは[公式リファレンス](http://gorm.io/ja_JP/docs/migration.html)を参照ください。
プロダクト向きのマイグレーションツールが他にあるそうなので気になる方は調べてみてください。今回は規模が大きくないので、これでいきます。
[Go言語で使えるmigrationライブラリ](https://re-engines.com/2018/10/25/golang-migration/)

ちなみに、プロダクト開発でgormを使おうと考えている方はこちらの記事を読んでみるといいかもしれません。
[Go言語のGormを実践投入する時に最低限知っておくべきことのまとめ【ORM】](https://qiita.com/ttiger55/items/3606b8dd570637c12387)

```go:main.go
// User モデルの宣言
type User struct {
	gorm.Model
	Username string `form:"username" binding:"required" gorm:"unique;not null"`
	Password string `form:"password" binding:"required"`
}
```

# ユーザー登録処理
ユーザー登録でアプリ側にさせることは、フォーム画面からユーザー名とパスワードを受け取ってDBに登録することです。

URL`localhost:8080/signup`でユーザー登録画面にいきます。トップ画面上から飛べるようにするのを忘れてました（笑）。
この画面を出すだけなら特に関数は必要ありません。
登録ボタンを押すと`signup.html`フォーム内の`action="/signup"`が`/signup`にPOSTを投げるようにしています。


User型の`form`で構造体を定義し、`Bind`関数を使って、構造体で定義された内容と違ったデータが来てないか把握することができます。
気になる方は、[GinでBindingが物珍しかったので他のフレームワークも調べてみた](https://qiita.com/hiroyukim/items/5b6a12839a0f730d1fd8)を読んでみるといいと思います。

フォームの内容は変数`c`内に格納されていて、PostForm()を使って値を取り出します。


```go:main.go
// ユーザー登録画面
	router.GET("/signup", func(c *gin.Context) {

		c.HTML(200, "signup.html", gin.H{})
	})

	// ユーザー登録
	router.POST("/signup", func(c *gin.Context) {
		var form User
		// バリデーション処理
		if err := c.Bind(&form); err != nil {
			c.HTML(http.StatusBadRequest, "signup.html", gin.H{"err": err})
			c.Abort()
		} else {
			username := c.PostForm("username")
			password := c.PostForm("password")
			// 登録ユーザーが重複していた場合にはじく処理
			if err := createUser(username, password); err != nil {
				c.HTML(http.StatusBadRequest, "signup.html", gin.H{"err": err})
			}
			c.Redirect(302, "/")
		}
	})
```

取り出したusernameとpasswordを`createUser`関数に引数で渡します。
`/crypto/crypto.go`内の`PasswordEncrypt()`関数を使ってパスワードを暗号化します。
暗号化されたパスワードとユーザーネームを`Users`テーブルに保存します。

package内の関数は、先頭文字を大文字にするとpublic関数になり、小文字にするとprivate関数になります。
`PasswordEncrypt()`関数は先頭が大文字のPなので、public関数ですね。

ユーザーが重複していたりして登録できなかったときにリダイレクトしたいので、`GetErrors()`でエラーを取得し、returnできるようにしています。
[GORMのエラーハンドリングに関する記述](http://gorm.io/ja_JP/docs/error_handling.html)

```go:main.go
// ユーザー登録処理
func createUser(username string, password string) []error {
	passwordEncrypt, _ := crypto.PasswordEncrypt(password)
	db := gormConnect()
	defer db.Close()
	// Insert処理
	if err := db.Create(&User{Username: username, Password: passwordEncrypt}).GetErrors(); err != nil {
		return err
	}
	return nil

}
```


# ログイン処理
ログイン処理でアプリ側でさせることは、ログインフォームから受け取ったユーザー名とパスワードがDBに同じく保存されているか探すことです。
流れとしては、

```
ログインフォームからユーザー名とパスワードを受け取る
↓
ユーザー名をもとにUsersテーブルからユーザーレコードを取得する
↓
ログインフォームから受け取ったパスワードとDBから取得したユーザーレコードのパスワードと比較
↓
トップ画面へリダイレクトまたはログイン画面に戻る
```

URL`localhost:8080/login`でログイン画面にいきます。これまたトップ画面上から飛べるようにするのを忘れてました（笑）。
この画面を出すだけなら特に関数は必要ありません。
登録ボタンを押すと`login.html`フォーム内の`action="/login"`が`/login`にPOSTを投げるようにしています。

POSTで受け取ったユーザー名の値を`getUser()`に引数で入れ、DBからユーザーレコードを取得しています。取得したユーザーレコードはUser型として取得しているので、`.Password`でパスワードを取得できます。
パスワードの比較は、`CompareHashAndPassword()`の引数に`dbPassword `と`formPassword`を入れることで比較することができます。この関数は、`crypto`ディレクトリの`crypto.go`に定義されています。この関数のreturnはerror型なので、エラー内容があればif文で引っかかるようになってます。

```go:main.go
// ユーザーログイン画面
	router.GET("/login", func(c *gin.Context) {

		c.HTML(200, "login.html", gin.H{})
	})

	// ユーザーログイン
	router.POST("/login", func(c *gin.Context) {

		// DBから取得したユーザーパスワード(Hash)
		dbPassword := getUser(c.PostForm("username")).Password
		log.Println(dbPassword)
		// フォームから取得したユーザーパスワード
		formPassword := c.PostForm("password")

		// ユーザーパスワードの比較
		if err := crypto.CompareHashAndPassword(dbPassword, formPassword); err != nil {
			log.Println("ログインできませんでした")
			c.HTML(http.StatusBadRequest, "login.html", gin.H{"err": err})
			c.Abort()
		} else {
			log.Println("ログインできました")
			c.Redirect(302, "/")
		}
	})
```

データベースからwhere句を使ってユーザーを１件取得する際に使ったのが、`getUser`内の`db.First()`です。
`db.Where()`でも代用できると思われます。他にもdbにまつわる様々な関数があるので、詳しく知りたい方は以下を読んでみるといいかと思います。
[参照1](http://jinzhu.me/gorm/crud.html)
[参照2](http://gorm.io/ja_JP/docs/query.html)

```
db.First(&user, "username = ?", username)
↓
SELECT * FROM users WHERE username = "jinzhu";
```


```go:main.go
// ユーザーを一件取得
func getUser(username string) User {
	db := gormConnect()
	var user User
	db.First(&user, "username = ?", username)
	db.Close()
	return user
}
```

# 起動
起動させていろいろ遊んでみましょう。
ターミナルを付けてmytweetディレクトリ内で`go run main.go`をしてください。
`localhost:8080/signup`でユーザー登録
`localhost:8080/login`でログイン
ですよ。
トップ画面から飛べるようにするの忘れてごめんなさい。

MySQLを別タブで起動してちゃんとユーザー登録できてるか、ログインできてるか確かめてみましょう。
ちなみに、SQL文って書き方よく忘れるよね。

```
// mysql起動
$ mysql -uroot -p

// testデータベースを選択
$ use test;

// usersテーブル全レコード表示
$ select * from users;
```

各自print文(`log.Println()`)を使ってパスワードがちゃんとハッシュ化されてるかとか、ユーザー名がちゃんと取得できてるかとかみてみると勉強になるかと思います。


# 最後に
今回は、前回のコードにユーザー登録とログイン機能を追加してみました。
次回は、デプロイしてみたり、セッション機能を追加してみたりしたいと思ってます。

今回のコードはこちらから。
https://github.com/ymdd1/mytweet

cryptoの部分はこちらのサイトのコードを流用させていただきました。
[Webアプリ初心者がGo言語でサーバサイド（2. パスワード認証機能の実装）](https://qiita.com/wsuzume/items/8b282d553a4185cbac5c)