2
2

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 5 years have passed since last update.

【Go】Go言語Web開発プロジェクト Part2 - Userテーブルを作成し、ログイン画面を実装するまで -

Last updated at Posted at 2018-10-08

前回のおさらい

前回Part1とPart1.5で環境構築を(ようやく)終わらせる事ができました。
今回から、いよいよコードゴリゴリ書いていこうと思います。
と、言いつつもDB、リダイレクト、テンプレートエンジン等、考えることが多いため少しづつ解決していきましょう。

前回からの変更点(先に読んでいただけると幸いです)

gooseのリポジトリ先変更

bitbucket経由でインストールしたgooseなのですが、どうもbitbucketのソースコードの更新が滞っている模様なため、githubリポジトリにフォークされたプロジェクトの方をインストールするように変更しました(そもそもbitbucketが使いづらいってのもあったのですが...)。

それに伴い、GomfileとGomfile.lockの内容を変更しました。

Gomfile
gom 'github.com/pressly/goose/cmd/goose'
Gomfile.lock
gom 'github.com/pressly/goose/cmd/goose'

こちらのForkリポジトリですが、まだyamlファイルによるDB接続のサポートを行っていないため、gooseコマンドを打つ際に直接DB指定をするようにしています。
大変お手数ですが、一旦bitbucketリポジトリのgooseからgithubリポジトリにforkされたgooseを入れ直して以降の記事をお読みください。

GOPATH, vendoringについて

docker-composeでmain.goを実行する際にgom経由でインストールしたパッケージ群へのPATHがおかしなことになっていたため、調べていると以下のような記事を見つけました。

https://h3poteto.hatenablog.com/entry/2016/11/30/231652

Go 1.6以降はvendoring機能により、プロジェクトの直下にvendorディレクトリがあった場合はimport文の依存解決順序が、 vendor > $GOROOT > $GOPATHという順序でパッケージを探してくれる模様です。
つまりは、vendoring機能によってimportの依存解決をvendoring優先にするためにはプロジェクトの配置を$GOPATHに置かなければいけません。
この事はgopathのhelpにも書かれていました。

go help gopath
Vendor Directories

Go 1.6 includes support for using local copies of external dependencies
to satisfy imports of those dependencies, often referred to as vendoring.

Code below a directory named "vendor" is importable only
by code in the directory tree rooted at the parent of "vendor",
and only using an import path that omits the prefix up to and
including the vendor element.

と言うことで、プロジェクトのディレクトリ構成を以下のように書き換えました。


├── Dockerfile
├── LICENSE
├── README.md
├── bin
│   └── gom
├── docker-compose.yml
└── src
    ├── dairy_report
    │   ├── Gomfile
    │   ├── Gomfile.lock
    │   ├── db
    │   │   ├── dbconf.yml
    │   │   └── migrations
    │   │       └── 20180930203149_create_users.sql
    │   ├── main.go
    │   ├── templates
    │   │   └── login.ace
    │   └── vendor
    │       ├── bin
    │       │   ├── ace
    │       │   └── goose

更に、docker-composeで構築したGOコンテナのデフォルトのGOPATHは/goなため、direnvで環境変数の設定考えていくと煩雑化していく恐れ大でした。そのため、デフォルトのGOPATHでアプリケーションが配置されるようにDockerfileを書き換えました。

Dockerfile
FROM golang:1.11.0

RUN apt update

# PostgreSQLのClientをインストール
RUN apt install -y postgresql-client

RUN mkdir -p /go/src/dairy_report/db
RUN cd /go/src/dairy_report
ADD src/dairy_report/Gomfile Gomfile
ADD src/dairy_report/Gomfile.lock Gomfile.lock
ADD src/dairy_report/db/dbconf.yml db/dbconf.yml
# gomだけgo getで取得
RUN go get github.com/mattn/gom

ADD . /go
WORKDIR /go/src/dairy_report
RUN gom install

最後に.dockerignoreを以下のように書き直しました。

.dockerignore
.envrc
bin/
src/dairy_report/vendor/

以上により、無事Goコンテナを無事起動することができました。
ようやくこれで第一歩って所ですね。

gooseによるテーブル作成

gooseでmigrationファイルを作成します。まずはUserテーブルを作成しましょう。

% goose create create_users
goose: created 
dairy_report/db/migrations/20180930203149_create_users.go
% mv dairy_report/db/migrations/20180930203149_create_users.go dairy_report/db/migrations/20180930203149_create_users.sql
dairy_report/db/migrations/20180930203149_create_users.sql
-- +goose Up
CREATE TABLE users (
id int NOT NULL PRIMARY KEY,
username text,
password text
);

-- +goose Down
DROP TABLE users;

とりあえず、UPでマイグレーションを実行するため、Usersテーブルを作成しユーザ名とパスワードを持たせるようにしました(一からこういうの作ろうとすると何が必要か調べなきゃいけないのが結構しんどいですね、、、まぁ仕方ないですが)

% goose postgres "host=$DAIRY_REPORT_DEVELOPMENT_HOST user=$DAIRY_REPORT_DEVELOPMENT_USERNAME password=$DAIRY_REPORT_DEVELOPMENT_PASSWORD dbname=$DAIRY_REPORT_DEVELOPMENT_DATABASE sslmode=disable" up
2018/09/30 14:41:52 OK    20180930203149_create_users.sql
2018/09/30 14:41:52 goose: no migrations to run. current version: 20180930203149

# psql -hdb -Udairy_report -ddairy_report_development
dairy_report_development=# \d 
                     List of relations
 Schema |          Name           |   Type   |    Owner     
--------+-------------------------+----------+--------------
 public | goose_db_version        | table    | dairy_report
 public | goose_db_version_id_seq | sequence | dairy_report
 public | users                   | table    | dairy_report
(3 rows)

dairy_report_development=# ~   
dairy_report_development-# \d users
      Table "public.users"
  Column  |  Type   | Modifiers 
----------+---------+-----------
 id       | integer | not null
 username | text    | 
 password | text    | 
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)

続いてフォームを作成するためにテンプレートエンジンを見ていくことにします。

HTMLテンプレートエンジンを利用してslimテンプレートを読み込む

Goの標準ライブラリとしてhtml/templateがあります。
こちらは、テンプレートを生成して書き込む事も出来ますが、テンプレートファイルを読み込む事もできます。

main.go
package main

import (
	"log"
	"net/http"
	"html/template"
)

func handler(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("templates/login.html")
	if err != nil {
		log.Fatal(err)
	}
	err = tmpl.Execute(w, "")
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("%+v¥n", r)
}
func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

ですが、今回はslimテンプレートエンジンでhtml文を書きたいため、slimやjadeからのインスパイヤテンプレートエンジンAceを使って書きたいと思います。

Gomfile
gom 'github.com/pressly/goose/cmd/goose'
gom 'github.com/yosssi/ace'

Aceではace拡張子のHTML文を書きますが、その構造はslimに似ています。

templates/login.ace
= doctype html
html lang=ja
  head
    meta charset=UTF-8
    title ログイン
  body
    h1 テスト
main.go
package main

import (
	"log"
	"net/http"
	"github.com/yosssi/ace"
)

func handler(w http.ResponseWriter, r *http.Request) {
	tmpl, err := ace.Load("templates/login", "", nil)
	if err != nil {
		log.Fatal(err)
	}
	err = tmpl.Execute(w, "")
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("%+v¥n", r)
}
func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

ではとりあえず、ログイン画面とログイン成功後のTOPページを作っていこうと思います。
取り急ぎ、BootStarp4関連のリソース(CSS,JS)をCDNで読み込んで、ログインフォームを作っていきます。
コードはhttps://getbootstrap.com/docs/4.1/examples/sign-in/のサンプルコードを適当に修正をかけました。

templates/login.ace
= doctype html
html lang=ja
  head
    meta charset=UTF-8
    meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"
    link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"
    link rel="stylesheet" href="https://getbootstrap.com/docs/4.1/examples/sign-in/signin.css"
    title ログイン
  body.text-center
    form.form-signin action="/login" method="POST"
      h1.h3.mb-3.font-weight-normal ログイン画面
      input#input_username.form-control type="text" placeholder="ユーザー名" required="" autofocus="" autocomplete="off" name="username"
      input#input_password.form-control type="password" placeholder="パスワード" required="" autocomplete="off" name="password"
      button.btn.btn-lg.btn-primary.btn-block type="submit" サインイン
    script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"
    script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"
    script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"

では、これをGET,POSTリクエストで分けられるようにイベントハンドラを作成していきます。

main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"github.com/yosssi/ace"
)

func login_handler(w http.ResponseWriter, r *http.Request) {
	fmt.Println("method:", r.Method) //リクエストを取得するメソッド
	// ログイン画面の取得(GET)もしくはログイン遷移の処理(POST)を行う
	switch r.Method {
	case "GET":
		tmpl, err := ace.Load("templates/login", "", nil)
		if err != nil {
			log.Fatal(err)
		}
		err = tmpl.Execute(w, "")
		if err != nil {
			log.Fatal(err)
		}
	case "POST":
		r.ParseForm()
		fmt.Println("username:", r.FormValue("username"))
		fmt.Println("password:", r.FormValue("password"))
	}
	log.Printf("%+v¥n", r)
}

func main() {
	http.HandleFunc("/login", login_handler)
	http.ListenAndServe(":8080", nil)
}

リクエストメソッドはr.Methodで取得できるため、これを利用してGET,POSTリクエスト時の処理を分けます。
また、r.ParseForm()によって、URLが渡すオプションを解析します。
今回はログイン画面なため、ユーザ名とパスワードの値がそれぞれ一つずつ取れればいいため、解析後にr.FormValueで直接フォームの値を取得します。

サンプル用のユーザを作成

% cd src/dairy_report/db/migrations/ 
% goose create insert_example_user
2018/10/07 23:41:16 Created new file: 20180930203150_insert_example_user.go
% mv src/dairy_report/db/migrations/20180930203150_insert_example_user.go src/dairy_report/db/migrations/20180930203150_insert_example_user.sql
20180930203150_insert_example_user.sql
-- +goose Up
INSERT INTO users VALUES
(1, 'test', 'test1234');

-- +goose Down
DELETE FROM users WHERE id = 1;
% /go/src/dairy_report/db/migrations
% ../../vendor/bin/goose postgres "host=$DAIRY_REPORT_DEVELOPMENT_HOST user=$DAIRY_REPORT_DEVELOPMENT_USERNAME password=$DAIRY_REPORT_DEVELOPMENT_PASSWORD dbname=$DAIRY_REPORT_DEVELOPMENT_DATABASE sslmode=disable" up

dairy_report_development=# select * from users;
 id | username | password 
----+----------+----------
  1 | test     | test1234
(1 row)

DBのORMマッパーを扱う

いよいよ、作成したユーザデータのパスワードを確認し、フォームに入力されたパスワードと一致すればTOPページにリダイレクトするようにします。DBからデータを取得する必要があるため、ORMマッパーを利用する必要があります。今回は、標準ライブラリであるdatabase/sqlが一番扱いやすいためこれを採用しました。
また、PostgreSQLのドライバを利用するため、github.com/lib/pqのドライバを利用しています。

Gomfile
gom 'github.com/pressly/goose/cmd/goose'
gom 'github.com/yosssi/ace'
gom 'github.com/lib/pq'
/main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"github.com/yosssi/ace"
	"database/sql"
	_ "github.com/lib/pq" //このアンダースコアを入れることでpqを直接使っているメソッドがないコードでエラーが発生しないようにしています
)

func login_handler(w http.ResponseWriter, r *http.Request) {
	fmt.Println("method:", r.Method) //リクエストを取得するメソッド
	// ログイン画面の取得(GET)もしくはログイン遷移の処理(POST)を行う
	switch r.Method {
	case "GET":
		tmpl, err := ace.Load("templates/login", "", nil)
		if err != nil {
			log.Fatal(err)
		}
		err = tmpl.Execute(w, "")
		if err != nil {
			log.Fatal(err)
		}
	case "POST":
		type User struct {
			UserName string
			Password string
		}
		var id int
		var username string
		r.ParseForm()
		fmt.Println("username:", r.FormValue("username"))
		fmt.Println("password:", r.FormValue("password"))

		user := User{ r.FormValue("username"), r.FormValue("password")}
		db, err := sql.Open("postgres", "host=db user=dairy_report password=dairy_report dbname=dairy_report_development sslmode=disable")
		if err != nil {
			fmt.Println(err)
		}
		query := `SELECT id, username FROM users where username = $1 AND password = $2`
		err = db.QueryRow(query, user.UserName, user.Password).Scan(&id, &username) // データの取得に成功した場合はidとusernameに各値を格納する
		if err != nil {
			fmt.Println(err)
			http.Redirect(w, r, "/login", http.StatusFound)
		}
		fmt.Println(id, username)
		http.Redirect(w, r, "/", http.StatusFound)
	}
	log.Printf("%+v¥n", r)
}

func main() {
	http.HandleFunc("/login", login_handler)
	http.ListenAndServe(":8080", nil)
}

以上で、ログイン画面の実装が完了しました(TOPページはまだaceテンプレートを用意していないため、NOT Foundです)。
Untitled.gif

まとめ

長くなりましたが、ログイン画面を実装するまでの記事でした。エラー処理のリファクタリングやPOSTリクエストの二重防止、CSRFトークンの処理など、フォーム処理に必要なものをまだ入れていませんが、エビデンスを調べるのにかなり時間がかかっているため、一旦ここでストップしておきます。
次回の実装内容については考え中です。それでは。

参考文献

シリーズ

Part1.5Part2Part2.5

2
2
0

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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?