🤖

golangでログイン機能を作る①(bcryptでパスワード暗号化)

2022/03/25に公開約6,500字

【環境】
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等の詳細については各過去記事をご参照下さい。

ルーティング

main.go
package main

import (
	"go_blog/controller"
)

func main() {
	router := controller.GetRouter()
	router.Run(":8080")
}

main.goではrouter.goのGetRouter関数を呼び出しているのみです。

router.go
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にあります。

暗号化部分

crypto.go
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) 時に使います。

モデル

user_model.go
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

home_controller.go
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です。

login_controller.go
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

home.html
<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は次回実装、今回はページが更新されるとログインが解けます。)

signup.html
<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)送信します。
バリデーションは未実装なので空白でも送信されてしまいます。

login.html
<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)送信します。
こちらもバリデーションは未実装です。

参考

https://qiita.com/dai-maru/items/f7cdd22baf3425a1722d
https://zenn.dev/kou_pg_0131/articles/go-digest-and-compare-by-bcrypt
https://zenn.dev/someone7140/articles/02181927acd040
https://blog.motikan2010.com/entry/2017/02/13/【Go言語】パスワードをハッシュ化(bcrypt)
https://qiita.com/wsuzume/items/8b282d553a4185cbac5c

Discussion

ログインするとコメントできます