56
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-09

【Go+Gin+Gorm】初心者だから超簡単webサービス作ってみるの続きです。

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

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

外部ライブラリのインストール

続きの方はターミナルを開いて以下を実行してください。
ここから始める方はmain.gocrypto/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ファイルを使うためのライブラリです。
ハードコーディングを避けるため、今回は使ってみようと思います。
.envファイルの中に定数を記述して、コード内でその定数を使うことができます。使い方もクソもないかもですが、こちらを参照するとわかりやすいかもです。
main.goと同じ階層に.envファイルを作り、前回の続きの方は以下のように記述してください。
これらはMySQLのDB名だったり、ユーザー名だったりを記載しています。

.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について詳しくは公式リファレンスを参照ください。
プロダクト向きのマイグレーションツールが他にあるそうなので気になる方は調べてみてください。今回は規模が大きくないので、これでいきます。
Go言語で使えるmigrationライブラリ

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

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に登録することです。

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

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

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

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のエラーハンドリングに関する記述

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から取得したユーザーレコードのパスワードと比較
↓
トップ画面へリダイレクトまたはログイン画面に戻る

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

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

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句を使ってユーザーを1件取得する際に使ったのが、getUser内のdb.First()です。
db.Where()でも代用できると思われます。他にもdbにまつわる様々な関数があるので、詳しく知りたい方は以下を読んでみるといいかと思います。
参照1
参照2

db.First(&user, "username = ?", username)
↓
SELECT * FROM users WHERE username = "jinzhu";
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. パスワード認証機能の実装)

56
37
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
56
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?