search
LoginSignup
31

More than 3 years have passed since last update.

posted at

updated at

Webアプリ初心者がGo言語でサーバサイド(2. パスワード認証機能の実装)

はじめに

本記事は「Webアプリ初心者がGo言語でサーバサイド(1. 簡単なHTTPサーバの実装)」の続きです。

本記事で用いるサンプルコードはGitHubにアップしてあります。

次「Webアプリ初心者がGo言語でサーバサイド(3. セッションマネージャの実装)

パスワード認証機能をつける

次に、もっとも基礎的な機能としてパスワードログインの機能をつけることにします。この記事を書くにあたり、最初からMySQLを使うことも考えたのですが、長ったらしくなるのでまずは MySQL も Redis も使わずにユーザ情報の管理とセッション管理の機能を実装することにしました。とりあえず動作するダミーを作っておき、あとできちんとしたものに置き換えるという寸法です。建物建てるときにまず足場を組むようなものです。

さて、ここで気持ちを切り替えてください。いまみなさんが扱っているのはユーザの最重要機密のひとつです。パスワードとは言うなれば「家の玄関の鍵」であり、これをネットワーク上でIPアドレスとユーザ名を付加してやりとりするということは、悪い人がいるかもしれない公共の場で、家の鍵にご丁寧に住所と名前を書いて投げっこするようなものです。

WireShark などでパケットを覗いてみると分かりますが、HTTP通信ではユーザ名とパスワードは平文で送信されていて、通信を傍受している悪い人からは丸見えです。

スクリーンショット 2018-11-02 19.26.56.png
最近はSSL/TLSプロトコルという暗号化された伝送路上でHTTP通信を行うHTTPS方式があります。これは、やりとりしている本人たちしか開け方を知らない金庫に家の鍵を入れてやりとりするということですから、通信を傍受されても比較的安全です。本番環境では必ずHTTPSの設定を行ってください。

また、サーバ側で受け取ったパスワードは決して平文で保管してはいけません。通信路上でパスワードを盗み見ることができないと分かれば、悪い人は必ず空き巣に入って鍵を盗みに来ます。要するにデータベースへの不正侵入を試みるわけで、マズいことにそれはときどき成功してしまいます。絶対無敵の防壁を作ることは不可能ですし、仮に作れたとしてもぶっちゃけ内部犯やスパイは原理的に防ぎようがないので、現実的には「盗み見られても構わないようにパスワードを加工する」という手段が用いられます。これにはbcryptという方式のハッシュが使われます。

bcrypt は計算量の大きい一方向ハッシュです。ここでSHA256などの高速なハッシュ関数を使ってしまうと、ありがちなパスワードに対するレインボーテーブル(逆引き辞書のようなもの)を作成されて破られてしまう可能性がありますが、bcrypt はわざと計算に時間をかけているため、レインボーテーブルによる攻撃に耐性があります。

bcrypt を使用したパスワード認証機能のコードを以下に示します。

crypto/crypto.go
package crypto

import (
    "golang.org/x/crypto/bcrypt"
)

func PasswordEncrypt(password string) (string, error) {
    hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(hash), err
}

func CompareHashAndPassword(hash, password string) error {
    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
config/dummy_db.go
package config

import (
    "sampleapp/crypto"

    "errors"
)

func NewDummyUser(username, email string) *DummyUserModel {
    return &DummyUserModel{
        Username: username,
        Email: email,
    }
}

type DummyUserModel struct {
    Username string
    Password string
    Email string
    authenticated bool
}

func (u *DummyUserModel) SetPassword(password string) error {
    hash, err := crypto.PasswordEncrypt(password)
    if err != nil {
        return err
    }
    u.Password = hash
    return nil
}

func (u *DummyUserModel) Authenticate() {
    u.authenticated = true
}

type DummyDatabase struct {
    database map[string]interface{}
}

var store DummyDatabase

func init() {
    store.database = map[string]interface{}{}
}

func DummyDB() *DummyDatabase {
    return &store
}

func (db *DummyDatabase) Exists(username string) bool {
    _, r := db.database[username]
    return r
}

func (db *DummyDatabase) SaveUser(username, email, password string) error {
    if db.Exists(username) {
        return errors.New("user \"" + username + "\" already exists")
    }

    user := NewDummyUser(username, email)
    if err := user.SetPassword(password); err != nil {
        return err
    }
    db.database[username] = user
    return nil
}

func (db *DummyDatabase) GetUser(username, password string) (*DummyUserModel, error) {
    buffer, exists := db.database[username]
    if !exists {
        return nil, errors.New("user \"" + username + "\" doesn't exists")
    }

    user := buffer.(*DummyUserModel)
    if err := crypto.CompareHashAndPassword(user.Password, password); err != nil {
        return nil, errors.New("user \"" + username + "\" doesn't exists")
    }

    return user, nil
}
routes/user_routes.go
package routes

import (
    "sampleapp/config"

    "net/http"
    "github.com/gin-gonic/gin"
)

func UserSignUp(ctx *gin.Context) {
    println("post/signup")
    username := ctx.PostForm("username")
    email := ctx.PostForm("emailaddress")
    password := ctx.PostForm("password")
    passwordConf := ctx.PostForm("passwordconfirmation")

    if password != passwordConf {
        println("Error: password and passwordConf not match")
        ctx.Redirect(http.StatusSeeOther, "//localhost:8080/")
        return
    }

    db := config.DummyDB()
    if err := db.SaveUser(username, email, password); err != nil {
        println("Error: " + err.Error())
    } else {
        println("Signup success!!")
        println("  username: " + username)
        println("  email: " + email)
        println("  password: " + password)
    }

    ctx.Redirect(http.StatusSeeOther, "//localhost:8080/")
}

func UserLogIn(ctx *gin.Context) {
    println("post/login")
    username := ctx.PostForm("username")
    password := ctx.PostForm("password")

    db := config.DummyDB()
    user, err := db.GetUser(username, password)
    if err != nil {
        println("Error: " + err.Error())
    } else {
        println("Authentication Success!!")
        println("  username: " + user.Username)
        println("  email: " + user.Email)
        println("  password: " + user.Password)
        user.Authenticate()
    }

    ctx.Redirect(http.StatusSeeOther, "//localhost:8080/")
}

実行してサインアップ->ログインしてみた結果を下図に示します。パスワードが暗号化されて保存されていることがわかります。
スクリーンショット 2018-11-02 20.42.17.png
本当はユーザからの入力がまともなものかどうかを確かめるバリデーションという処理が必要なのですが、今回はコードを簡略化するために追加していません。バリデーションを行わないとユーザー名やメールアドレスといった必須項目を空欄のまま登録してしまったり、XSSやSQLインジェクション攻撃を通過させてしまったりして危険です。

また、どれだけ対策をしてもパスワードは仕組みそのものが絶対安全ではありません。そもそもパスワードを設定するのは人間であり、人間の記憶力には限界がありますから、多くのユーザは自分で覚えやすいパスワードを設定したり、メモしたり、パスワードを使い回したりしてしまいます。つまりこちらでどれだけ気をつけても手帳や他のサービスからパスワードが漏洩するということばかりは、防ぎようがありません。セキュリティをより強固にするため、最近は二段階認証を採用しているサービスも増えてきています。

次の話

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
What you can do with signing up
31