【Go+Gin+Gorm】初心者だから超簡単webサービス作ってみるの続きです。
今回は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ファイルを使うためのライブラリです。
ハードコーディングを避けるため、今回は使ってみようと思います。
.envファイルの中に定数を記述して、コード内でその定数を使うことができます。使い方もクソもないかもですが、こちらを参照するとわかりやすいかもです。
main.go
と同じ階層に.env
ファイルを作り、前回の続きの方は以下のように記述してください。
これらはMySQLのDB名だったり、ユーザー名だったりを記載しています。
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】
// 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()を使って値を取り出します。
// ユーザー登録画面
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のエラーハンドリングに関する記述
// ユーザー登録処理
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文で引っかかるようになってます。
// ユーザーログイン画面
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";
// ユーザーを一件取得
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. パスワード認証機能の実装)