golangでログイン機能を作る①(bcryptでパスワード暗号化)
【環境】
MacBook Air (M1, 2020)
OS: MacOS Big Sur version11.6
Docker Desktop for Mac version4.5.0
golangでログイン機能を作っていきます。
今回はIDとパスワードで新規会員登録とログインまでです。
bcryptというgolangの暗号化パッケージを使いパスワードを暗号化します。
bcryptを使ったパスワード認証
bcrypt暗号化手法について、こちらの記事を参考にしました。
- bcryptはハッシュ値を使った暗号化(平文保存でも鍵使用の暗号化でもない。)
- ソルトとストレッチングにより元に戻すことが困難
- ソルト:パスワードにハッシュを付与した後に暗号化
- ストレッチング:ハッシュ値への計算を数千〜数万回繰り返す暗号化
ディレクトリ構成
go_blog
├── .air.toml
├── build
│ ├── app
│ │ └── Dockerfile
│ └── db
│ ├── Dockerfile
├── cmd
│ └── go_blog
│ └── main.go
├── controller
│ ├── login_controller.go
│ ├── router.go
│ └── home_controller.go
├── crypto
│ └── crypto.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── model
│ ├── database.go
│ └── user_model.go
└── view
├── login.html
├── signup.html
└── home.html
データベースのMySQL接続, フレームワークのgin, ORMのgorm, ホットリロードのAirなどの技術を使っています。
docker-compose.yml, 各Dockerfile, database.go, .air.toml等の詳細については各過去記事をご参照下さい。
ルーティング
package main
import (
"go_blog/controller"
)
func main() {
router := controller.GetRouter()
router.Run(":8080")
}
main.goではrouter.goのGetRouter関数を呼び出しているのみです。
package controller
import (
"github.com/gin-gonic/gin"
)
func GetRouter() *gin.Engine {
router := gin.Default()
router.LoadHTMLGlob("view/*.html")
router.GET("/", getTop)
router.GET("/signup", getSignup)
router.POST("/signup", postSignup)
router.GET("/login", getLogin)
router.POST("/login", postLogin)
return router
}
フレームワークのginを使いTemplateの読み込みとルーティングしています。
getTop, getSignup, postSignup, getLogin, postLogin関数はControllerにあります。
暗号化部分
package crypto
import (
"golang.org/x/crypto/bcrypt"
)
// 暗号(Hash)化
func PasswordEncrypt(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(hash), err
}
// 暗号(Hash)と入力された平パスワードの比較
func CompareHashAndPassword(hash, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
今回のメイン箇所です。(と、いってもこちらの記事を丸々引用させて頂いただけです、、、、ありがとうございます。)
PasswordEncryptで平文のパスワードを暗号(Hash)化しています。
こちらは新規会員登録(Signup) 時に使います。
CompareHashAndPasswordでは暗号(Hash)化したパスワードと入力されたパスワードを比較し、エラーがnilなら認証成功です。
ログイン(Login) 時に使います。
モデル
package model
import (
"errors"
"fmt"
"go_blog/crypto"
"gorm.io/gorm"
)
type User struct {
gorm.Model
UserId string
Password string
}
func init() {
Db.Set("gorm:table_options", "ENGINE = InnoDB").AutoMigrate(User{})
}
func (u *User) LoggedIn() bool {
return u.ID != 0
}
func Signup(userId, password string) (*User, error) {
user := User{}
Db.Where("user_id = ?", userId).First(&user)
if user.ID != 0 {
err := errors.New("同一名のUserIdが既に登録されています。")
fmt.Println(err)
return nil, err
}
encryptPw, err := crypto.PasswordEncrypt(password)
if err != nil {
fmt.Println("パスワード暗号化中にエラーが発生しました。:", err)
return nil, err
}
user = User{UserId: userId, Password: encryptPw}
Db.Create(&user)
return &user, nil
}
func Login(userId, password string) (*User, error) {
user := User{}
Db.Where("user_id = ?", userId).First(&user)
if user.ID == 0 {
err := errors.New("UserIdが一致するユーザーが存在しません。")
fmt.Println(err)
return nil, err
}
err := crypto.CompareHashAndPassword(user.Password, password)
if err != nil {
fmt.Println("パスワードが一致しませんでした。:", err)
return nil, err
}
return &user, nil
}
Signup関数とLogin関数はControllerから呼ばれます。
- Signup関数
- 送られてきたUserIdと一致するUserが既に登録されているか確認
- PasswordEncrypt関数で送られてきたPasswordを暗号化
- gormのCreate関数で新規会員登録
- Login関数
- 送られてきたUserIdと一致するUserを取得
- 取得したUserの暗号化済みPasswordと送られてきたPasswordをCompareHashAndPassword関数でチェック
UserのLoggedInメソッドはtop.htmlで現在ログイン中かの確認に使います。
Controller
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
)
func getTop(c *gin.Context) {
c.HTML(http.StatusOK, "home.html", nil)
}
top.htmlを表示するためだけのControllerです。
package controller
import (
"go_blog/model"
"net/http"
"github.com/gin-gonic/gin"
)
func getSignup(c *gin.Context) {
c.HTML(http.StatusOK, "signup.html", nil)
}
func postSignup(c *gin.Context) {
id := c.PostForm("user_id")
pw := c.PostForm("password")
user, err := model.Signup(id, pw)
if err != nil {
c.Redirect(301, "/signup")
return
}
c.HTML(http.StatusOK, "home.html", gin.H{"user": user})
}
func getLogin(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", nil)
}
func postLogin(c *gin.Context) {
id := c.PostForm("user_id")
pw := c.PostForm("password")
user, err := model.Login(id, pw)
if err != nil {
c.Redirect(301, "/login")
return
}
c.HTML(http.StatusOK, "top.html", gin.H{"user": user})
}
GET(getSignup関数とgetLogin関数)は各htmlを表示するだけです。
postSignup関数では先程user_model.goで実装したSignup関数にUserIdとPasswordを送り、Userデータとエラーを取得しています。
エラーなら再び/singupへリダイレクトされ、エラーが無ければログイン状態となりtop.htmlを開きます。
postLogin関数もpostSingup関数とほとんど同じ形です。
View
<h1>TOP</h1>
{{ if .user.LoggedIn }}
<p>ログイン中【{{ .user.UserId }}】</p>
{{ end }}
<a href="/signup">会員登録</a>
<a href="/login">ログイン</a>
localhost:8080/にアクセスするとTOPページが表示されます。
ログイン中はgoからUserのデータが送られ、ログイン中と表示されます。
(SessionとCookieは次回実装、今回はページが更新されるとログインが解けます。)
<h1>SIGNUP</h1>
<form method="POST" action="/signup">
<p>ユーザーID: <input type="text" name="user_id" /></p>
<p>パスワード: <input type="password" name="password" /></p>
<p><input type="submit" value="新規会員登録" />
<a href="/"><input type="button" value="戻る" /></a></p>
</form>
ユーザーIDとパスワードを入力しサーバーへPOST(/singup)送信します。
バリデーションは未実装なので空白でも送信されてしまいます。
<h1>LOGIN</h1>
<form method="POST" action="/login">
<p>ユーザーID: <input type="text" name="user_id" /></p>
<p>パスワード: <input type="password" name="password" /></p>
<p><input type="submit" value="ログイン" />
<a href="/"><input type="button" value="戻る" /></a></p>
</form>
ユーザーIDとパスワードを入力しサーバーへPOST(/login)送信します。
こちらもバリデーションは未実装です。
参考
Discussion