読者です 読者をやめる 読者になる 読者になる

まったり技術ブログ

Technology is power.

【Go言語】パスワードをハッシュ化(bcrypt)

Go言語

f:id:motikan2010:20170213221701p:plain
・bcrypt.GenerateFromPassword
・bcrypt.CompareHashAndPassword
を使ってみる話です。

Go言語を使ったWeb開発で認証機能を実装したくて調べてみたらこんなリポジトリが見つかった。

github.com

よく見てみるとパスワード格納はライブラリに含まれておらず、サンプルコードでも無残にパスワードが平文で格納されているではありませんか・・・。

Go言語でのパスワードハッシュに関して調べてみると下記の記事が見つかった。

hachibeechan.hateblo.jp

ハッシュ化のライブラリには主に2つ用意されているらしい。
・scrypt
・bcrypt

本記事では、bcryptライブラリを使ってハッシュ化する方法を紹介します。

bcrypt - GoDoc

パスワードのハッシュ化

hash, err := bcrypt.GenerateFromPassword([]byte("パスワード"), bcrypt.DefaultCost)

// Byteで返されるので文字列に変換して表示
fmt.Println(string(hash))

// 毎回値の異なるハッシュ値が取得できる(ちなみに"password"のハッシュ値)
// $2a$10$iuJaubQvGTawiwa6UFa08uvOGwFaa25Wz29llEKEFHyPT3w262Qw6
// $2a$10$HFZ4bmj98bEePKO3gNsbZO3XsgXORvjFhexZV6HADm46/CuaE6M/m
// $2a$10$BSzyPPKOOs0YwC1h6UoD2eNFAyWYVfS.hmZQuQLLTRyC/Z.z3fzsy

パスワード文字列とハッシュ値を比較

認証部分は下記のように記述します。

err = bcrypt.CompareHashAndPassword([]byte("ハッシュ値"), []byte("パスワード"))
// 一致している場合はerrにnilが返されます。一致していない場合はエラーが返されます。

コードにまとめるとこのような感じ

下記のコードでは「Success」が表示されます。

package main

import (
    "fmt"

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

func main() {
    storePass := "password"
    loadPass := "password"

    hash, err := bcrypt.GenerateFromPassword([]byte(storePass), bcrypt.DefaultCost)
    if err != nil {
        return
    }
    hash_str := string(hash)

    err = bcrypt.CompareHashAndPassword([]byte(hash_str), []byte(loadPass))
    if err != nil {
        fmt.Println("Failure")
    } else {
        fmt.Println("Success")
    }

}

認証のデモ

今回はパスワード認証機能が動作確認をしたいだけなので、データベースなどは用意せずにログインIDとパスワードを格納することができるUser構造体を使って動作確認を行う。

package main

import (
    "fmt"
    "time"

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

type User struct {
    LoginId  string
    Password string
}

type Users []*User

var users Users

func register(login_id, pass string) {
    /*
      bcrypt.MinCost = 4
      bcrypt.MaxCost = 31
      bcrypt.DefaultCost = 10
   */
    hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
    if err != nil {
        return
    }
    users = append(users, &User{LoginId: login_id, Password: string(hash)})
}

func login(login_id, password string) {
    var hash_str = ""
    start := time.Now()
    for _, user := range users {
        if login_id == user.LoginId {
            hash_str = user.Password
            break
        }
    }
    err := bcrypt.CompareHashAndPassword([]byte(hash_str), []byte(password))
    end := time.Now()
    fmt.Printf("%fs\t", (end.Sub(start)).Seconds())
    if err != nil {
        fmt.Print("Failure")
    } else {
        fmt.Print("Success")
    }
    fmt.Printf("\t%s/%s\n", login_id, password)
}

func main() {
    users = Users{}
  // 登録
    register("user1", "password1")
    register("user2", "password2")
    register("user3", "password3")
    register("user4", "password4")
    register("user5", "password5")
  fmt.Println()

  // 認証
    login("user1", "password1")
    login("user2", "password2")
    login("user3", "password3")
    login("user4", "password4")
    login("user5", "password5")
    login("user6", "password1")
    login("user1", "")
    login("user3", "password1")
    login("user3", "password2")
    login("user3", "password3")
    login("user3", "password4")

}
出力
0.106827s    Success user1 / password1
0.096496s   Success user2 / password2
0.099798s   Success user3 / password3
0.099758s   Success user4 / password4
0.100966s   Success user5 / password5
0.000001s   Failure user6 / password1
0.125239s   Failure user1 /
0.097884s   Failure user3 / password1
0.097816s   Failure user3 / password2
0.097296s   Success user3 / password3
0.096882s   Failure user3 / password4

処理時間を見てみると、ログインIDに「user6」を指定した時だけ処理時間が異様に短いことが分かる。
存在しないユーザであり「CompareHashAndPassword」の処理がとてつもなく短くなるからである。
レスポンス時間からユーザの有無を知られるなんて気分が悪いので、このように"hash_str"を初期化してみる。

var hash_str = "$2a$10$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

こうすれば常に「CompareHashAndPassword」の処理時間が同等になる。

0.097851s Success user1 / password1
0.095720s  Success user2 / password2
0.097851s  Success user3 / password3
0.097147s  Success user4 / password4
0.104292s  Success user5 / password5
0.099853s  Failure user6 / password1
0.098755s  Failure user1 /
0.094102s  Failure user3 / password1
0.093256s  Failure user3 / password2
0.100131s  Success user3 / password3
0.097537s  Failure user3 / password4

これでレスポンス時間からユーザの有無を知られることはなくなるかと思う。

Webフレームワーク『Gin』を使ってみる

Go言語

f:id:motikan2010:20170211223100p:plain
Go言語でWebアプリ開発をしていみたいと思っていましたので、調べてみたらいろいろあるらしい。

概観からGoのWebFrameworkを選ぶ(2016/02) - Qiita その中で速度が速く、人気もある『Gin』を手始めにさわってみることにします。

github.com

作成するものは「SQLiteを使ったTODOリストアプリ」です。
こちらの記事を参考にして作成しました。

Go言語製WAF GinでWebアプリを作ってみる【準備編】 | eureka tech blog

作成順序としては
 ビュー → コントローラ → モデル
です。

下記のコマンドでGinをインストールすることができます。

$ go get gopkg.in/gin-gonic/gin.v1

HTMLテンプレートを呼び出す

まずは「/」にアクセスしたら、「index.tmpl」を呼び出し、出力するだけのコードを書いていきます。

$ vim main.go
package main

import (
    "net/http"

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

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("views/*")

    router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.tmpl", gin.H{
                    "title": "Hello Gin!",
                })
    })

    router.Run(":8080")
}

HTMLテンプレート作成

main.goで

gin.H{
    "title": "Hello Gin!",
}

と定義しているので、テンプレート側で「{{ .title }}」と記述してレンダリングさせることができます。

$ mkdir views
$ vim views/index.tmpl
<!DOCTYPE html>
<html>
    <head>
        <title>{{ .title }}</title>
    </head>
    <body>
        <h3>{{ .title }}</h3>
    </body>
</html>

ここまでできたら起動させてアクセスしてみます。

動作確認

$ go run main.go

http://127.0.0.1:8080」にアクセスしてみます。
f:id:motikan2010:20170211215514p:plain

コントローラを作る

ビューとモデル(DB)を橋渡しをするコントローラを作成していきます。

タスク一覧を渡すコントローラ作成

今はタスク一覧はDBから持ってくるのではなく、コントローラ内でをタスク配列として用意し、
そのタスク一覧をテンプレート側に渡す処理を書いていきます。

$ mkdir controllers
$ vim controllers/task.go
package task

import "strconv"

// idとテキストを保持する構造体
type Task struct {
    ID   int
    Text string
}

func NewTask() Task {
    return Task{}
}

// タスク構造体一覧を返す
func (c Task) GetAll() interface{} {

    // テストデータとして5つタスクを作成
    tasks := make([]*Task, 5)
    for i := 1; i <= 5; i++ {
        tasks[i-1] = &Task{ID: i, Text: "Task Text " + strconv.Itoa(i)}
    }

    return tasks
}

コントローラの呼び出し

$ vim main.go
//・・・

router.GET("/", func(c *gin.Context) {
    controller := task.NewTask()
    tasks := controller.GetAll()

    c.HTML(http.StatusOK, "index.tmpl", gin.H{
        "title": "TODO List",
        "tasks": tasks,    // 追記 テンプレートにタスクを渡す
    })
})

//・・・

テンプレートを修正

タスク一覧をリストとして表示するforを記述していきます。

$ vim views/index.tmpl
//・・・

<body>
<h3>{{ .title }}</h3>
    <ul>
    {{ range $index, $task := .tasks }}
        <li>{{ $task.ID }}: {{ $task.Text }} </li>
    {{ end }}
    </ul>
</body>

//・・・

f:id:motikan2010:20170211215616p:plain

モデルを作成して、DBにタスクを保存する

タスク内容をDB内に保存できるようにします。
今回DBはSQLiteを使っていきます。

Modelを作成

タスクを登録するためにCreate関数を用意しています。
文字列の引数を受け取り、その文字列をDBに挿入するような動作を行います。

$ mkdir models
$ vim models/task.go
package task

import (
    "github.com/jinzhu/gorm"
    _ "github.com/mattn/go-sqlite3"
)

var db *gorm.DB

func init() {
    var err error

    db, err = gorm.Open("sqlite3", "task.db")

    db.DropTableIfExists(&Task{})
    db.CreateTable(&Task{})

    if err != nil {
        panic(err)
    }
}

type Task struct {
    ID   int    `gorm:"primary_key"`
    Text string `gorm:"size:140"`
}

type Tasks []Task

type TaskRepository struct {
}

func NewTaskRepository() TaskRepository {
    return TaskRepository{}
}

// データベースに一行登録する
func (m TaskRepository) Create(text string) {
    var task = Task{Text: text}
    db.NewRecord(task)
    db.Create(&task)
    db.Save(&task)
}

コントローラ − Create関数を追加

こちらでもCreate関数を定義しています。
モデル内に定義されているCreate関数に対して、ユーザが送信したタスク文字列を渡しています。

$ vim controllers/task.go
import (
    "strconv"

    task "../models"
)

//・・・

func (c Task) Create(text string) {
    repo := task.NewTaskRepository()
    repo.Create(text)
}

POSTデータの受け取り

「text := c.PostForm(“text”)」でユーザが送信したパラメータを取得することができます。

$ vim main.go
func main() {

    //・・・

    router.POST("/", func(c *gin.Context) {
        text := c.PostForm("text")
        ctrl := task.NewTask()
        ctrl.Create(text)

        c.Redirect(http.StatusMovedPermanently, "/")
    })

登録フォームの作成

アプリケーションにタスク文字列を送信するためにフォームを追加します。

$ vim views/index.tmpl
//・・・

<ul>
<form action="/" method="post">
  <input type="text" name="text"></input>
  <input type="submit" value="送信">
</form>
{{ range $index, $task := .tasks }}
    <li>{{ $task.ID }}: {{ $task.Text }} </li>
{{ end }}
</ul>

//・・・

タスクを登録

$ go run main.go

f:id:motikan2010:20170211215637p:plain

登録されたタスクの確認

$ sqlite3 task.db

sqlite> .tables
tasks

sqlite> select * from tasks;
1|Test task

データベースに登録したタスクを出力

モデル − GetAll関数を追加

GetAll関数はDBに登録されているタスクを全て返します。

$ vim models/task.go
func (m TaskRepository) GetAll() Tasks {
    var tasks = Tasks{}
    db.Find(&tasks)

    return tasks
}

コントローラ − GetAll関数を修正

$ vim controllers/task.go
func (c Task) GetAll() interface{} {
    repo := task.NewTaskRepository()
    tasks := repo.GetAll()

    return tasks
}

f:id:motikan2010:20170211215706p:plain

idを指定してタスクを取得

モデル - GetByID関数を追加

GetByID関数はタスクIDを引数として受け取り、該当するタスクを返します。

$ vim models/task.go
func (m TaskRepository) GetByID(id int) Tasks {
    var tasks = Tasks{}
    db.Find(&tasks, id)
    return tasks
}

コントローラ - Get関数を追加

$ vim controllers/task.go
func (c Task) Get(n int) interface{} {
    repo := task.NewTaskRepository()
    tasks := repo.GetByID(n)

    return tasks
}

 Getを呼び出す

func main() {

    //・・・

    router.GET("/:id", func(c *gin.Context) {
            var id, _ = strconv.Atoi(c.Param("id"))
            ctrl := task.NewTask()
            tasks := ctrl.Get(id)

            c.HTML(http.StatusOK, "index.tmpl", gin.H{
                "tasks": tasks,
            })
        })

動作確認

http://127.0.0.1:8080/2」にアクセスすると、idが2のタスクが表示されます。 f:id:motikan2010:20170211215739p:plain

phpbrewでGDライブラリを入れようとしたらエラーになった

PHP

結論から言いますとGDライブラリはyumでインストールすることができなかったので、
結局ソースからインストールしました。

“phpbrew install"が失敗した

phpbrewでGDライブラリを入れようとしたら下記のようなエラーが表示された。

$ phpbrew install 5.6.26 +default +gd +openssl=/usr -- --with-libdir=lib64
*WARNING* You're runing phpbrew as root/sudo. Unless you're going to install
system-wide phpbrew or this might cause problems.
===> phpbrew will now build 5.6.26

(省略)

checking for gdSetErrorMethod in -lgd... no

configure: error: Unable to find libgd.(a|so) >= 2.1.0 anywhere under /usr

Please checkout the build log file for more details:
     tail /root/.phpbrew/build/php-5.6.26/build.log

「Unable to find libgd.(a|so) >= 2.1.0 anywhere under」というエラーを見る限り、
libgdのバージョン2.1.0以上が必要らしい。

yumからlibgdをインストール

$ yum install gd
パッケージ gd-2.0.35-11.el6.x86_64 はインストール済みか最新バージョンです
何もしません

デフォルトのyumリポジトリでは、バージョン2.1.0以上を入れられない。

ソースからlibgdをインストール

少し手間だがソースからインストールしてみる。 http://d.hatena.ne.jp/end0tknr/20150313/1426224469 バージョン2.1.1は古いと思われるが、2.1.0の用件は満たしているのでこのバージョンを入れることにする。
ちなみに最新バージョンは、2.2.4です。(2017/2/6現在) https://libgd.github.io/

$ wget https://bitbucket.org/libgd/gd-libgd/downloads/libgd-2.1.1.tar.gz 
$ tar -zxvf libgd-2.1.1.tar.gz 
$ cd libgd-2.1.1
$ ./configure --prefix=/usr/local/gd
$ make
$ make install

libにシンボリックリンク

これでlibgdは"/usr/local/gd/“以下に配置されているので、

$ phpbrew install 5.6.26 +default +openssl=/usr +gd=/usr/local/gd -- --with-libdir=lib

として実行したのだが、今度はopensslがインストールされなかった。"–with-libdir=lib64"と指定しないといけなさそう。 なので

$ cd /usr/local/gd
$ ln -s lib lib64

を実行してlib64という名でlibにアクセスするようにする。

phpbrewでGDライブラリをインストール

$ phpbrew install 5.6.26 +default +openssl=/usr +gd=/usr/local/gd -- --with-libdir=lib64
*WARNING* You're runing phpbrew as root/sudo. Unless you're going to install
system-wide phpbrew or this might cause problems.
===> phpbrew will now build 5.6.26

(省略)

---> Found date.timezone, patching config timezone with Asia/Tokyo
Congratulations! Now you have PHP with 5.6.26 as php-5.6.26

phpbrewでGDライブラリを入れることができた。

Bluetoothプロキシ『btproxy』を使う

IoT Bluetooth Raspberry Pi

f:id:motikan2010:20170204200342p:plain
IoTというワードが流行りつつあり、Bluetoothの通信を見てみたいという欲求が出てきたので、Bluetooth通信のプロキシを探してみました。
このキャプチャ技術を応用したら
Bluetoothバイス開発のデバッグ
Bluetoothバイスに対してのセキュリティ診断 などの場面で使えそう

github.com

f:id:motikan2010:20170204200304p:plain “RaspberryPi3”(以下ラズパイ)にbtproxyをインストールしてみて、
Macbook Proからスマートフォンへのファイル転送をキャプチャします。

今回ラズパイのOSは2016-03-18-raspbian-jessie.imgを使います。
では、早速導入していきましょう。

btproxyをRaspberryPi3に導入

BlueZをインストールする前の下準備

Linux上でBluetoothを扱うにはBlueZをインストールする必要があります。
まずはBlueZをインストールするために必要な依存パッケージをインストールします。

必要パッケージのインストール(※失敗)

pi@raspberrypi:~ $ sudo apt-get -y install libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev
(省略)
E: Failed to fetch http://mirrordirector.raspbian.org/raspbian/pool/main/s/systemd/libudev-dev_215-17+deb8u3_armhf.deb  404  Not Found [IP: 5.153.225.207 80]
E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?

ラズパイにOSをインストールしただけでは、パッケージリストが古い状態らしい。
下記のコマンドで解決します。

APTのパッケージリストの更新

pi@raspberrypi:~ $ sudo apt-get update

改めて依存パッケージのインストール

再び必要パッケージのインストール

pi@raspberrypi:~ $ sudo apt-get -y install libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev

今度は成功しました。

BlueZ − インストール

BlueZの最新版は5系ですがbtproxyが動作しなかったので、4系のBlueZを使う。
インストール作業は"/usr/src/“ディレクトリで行う。

pi@raspberrypi:~ $ cd /usr/src/
$ sudo wget http://www.kernel.org/pub/linux/bluetooth/bluez-4.101.tar.xz
$ sudo tar xvf bluez-4.101.tar.xz
$ cd bluez-4.101/
$ sudo ./configure --disable-systemd
$ sudo make
$ sudo make install

btproxy − インストール

btproxyはGitHub上で管理されており、インストールを非常に簡単に行えるようになっています。

pi@raspberrypi:/usr/src $ sudo apt-get -y install python2.7-dev
$ sudo git clone https://github.com/conorpp/btproxy.git
$ cd btproxy
$ sudo python setup.py install

btproxyを使う

動作確認

ヘルプが表示されれば正常にインストールされています。

pi@raspberrypi:~ $ sudo btproxy
usage: btproxy [-h] [-a SET_ADDRESS] [-n] [-c] [-i INTERFACE] [-s SCRIPT] [-l]
               [-1 MASTER_NAME] [-2 SLAVE_NAME] [-C] [-v] [-z] [-q]
               [addr_master] [addr_slave]

Bluetooth MiTM Proxy. For analyzing bluetooth connections actively.

positional arguments:
  addr_master           Bluetooth address of target master device
  addr_slave            Bluetooth address of target slave device

optional arguments:
  -h, --help            show this help message and exit
  -a SET_ADDRESS, --set-address SET_ADDRESS
                        Address to set for Bluetooth adaptor (requires -i)
(省略)
  -q, --inquire-again   Inquire the services again, don't reuse saved
                        settings.

通信のキャプチャ

まずはBluetooth機器のMACアドレスをスキャンします。
MacスマートフォンBluetooth機能をONに設定しておきます。
f:id:motikan2010:20170204194936p:plain:w400

まずは、通信を行う同士のデバイスのMACアドレスを取得します。
下記のコマンドの流れで、周囲にあるBluetooth機器のMACアドレスをスキャンします。

pi@raspberrypi:~ $ bluetoothctl
[NEW] Controller B8:27:EB:0A:55:1C raspberrypi [default]
[bluetooth]# scan on
Discovery started
[CHG] Controller B8:27:EB:0A:55:1C Discovering: yes
[NEW] Device 6C:76:60:8A:23:21 KCP01K
[NEW] Device B8:E8:56:2E:23:37 XXXXXX の MacBook Pro

[bluetooth]# quit // 終了

2つのBluetooth機器のMACアドレスを取得できたので、2つのデバイスを中継する『btproxy』を起動させます。
$ btproxy <マスターデバイスのMACアドレス> <スレーブデバイスのMACアドレス>

pi@raspberrypi:/usr/src/btproxy $ sudo btproxy B8:E8:56:2E:23:37 6C:76:60:8A:23:21
Running proxy on master  B8:E8:56:2E:23:37  and slave  6C:76:60:8A:23:21
Using shared adapter
Slave adapter:  hci0
Master adapter:  hci0
Looking up info on slave (6C:76:60:8A:23:21)
Looking up info on master (B8:E8:56:2E:23:37)
Spoofing master name as  KCP01K_btproxy
paired
Spoofing master name as  KCP01K_btproxy
Proxy listening for connections for "None"
Proxy listening for connections for "Headset Gateway"
Proxy listening for connections for "Handsfree Gateway"
Proxy listening for connections for "AV Remote Control Target"
Proxy listening for connections for "Advanced Audio"
Proxy listening for connections for "Android Network Access Point"
Proxy listening for connections for "MAP SMS/MMS"
Proxy listening for connections for "MAP EMAIL"
Proxy listening for connections for "OBEX Phonebook Access Server"
Proxy listening for connections for "OBEX Object Push"
Attempting connections with 10 services on slave
Now you're free to connect to "KCP01K_btproxy" from master device.
Connected to service "OBEX Object Push"

ペアリングの確認ダイアログが表示されるので許可します。
・ノートPC側
f:id:motikan2010:20170204194945p:plain:w300f:id:motikan2010:20170204194950p:plain:w300
スマートフォン
f:id:motikan2010:20170204194939p:plain:w300f:id:motikan2010:20170204194941p:plain:w300

ノートPC側からスマートフォン側へテキストファイルを送ってみます。

下記のファイルを送ってみます。
TEST.txt

Hello btproxy!!

f:id:motikan2010:20170204194954p:plain:w400
送信と同時にbtproxyによってキャプチャされた内容が表示されます。

Accepted connection from  ('B8:E8:56:2E:23:37', 12)
<<  '\x80\x00\x07\x10\x00\x1f@'
>>  '\xa0\x00\x0c\x10\x00\xff\xfe\xcb\x00\x00\x00\x01'
<<  '\x82\x00/\x01\x00\x15\x00T\x00E\x00X\x00T\x00.\x00t\x00x\x00t\x00\x00\xc3\x00\x00\x00\x0fI\x00\x12Hello btproxy!!'
>>  '\xa0\x00\x0b\xcb\x00\x00\x00\x01I\x00\x03'
<<  '\x81\x00\x03'
>>  '\xa0\x00\x08\xcb\x00\x00\x00\x01'
(104, 'Connection reset by peer') socket slave reconnecting...
Reconnecting...
(104, 'Connection reset by peer') socket master reconnecting...

「Hello btproxy!!」という文字が表示されておりキャプチャができていることを確認することができます。
“/libbtproxy/replace.py"ファイルを編集することによって、通信内容を改ざんすることも可能です。

『Goutte』を使って文字列からHTML解析

PHP

“通信を発生させずにHTMLを解析"をしたい・・・

 今までPHPでHTML解析するときは「Simple HTML DOM Parser」を利用しており、数年ぶりの利用で使い方を確認しているとこのような記事が。

localdisk.hatenablog.com

「Simple HTML DOM Parser」はパフォーマンス面に優れておらず、今では「Goutte」を使った方がいいとか。

github.com

いざ「Goutte」の使い方を簡単に調べてみると「Simple HTML DOM Parser」のようにHTML文字列を引数に与えて解析対象のオブジェクトを作成することができなさそう。 - Simple HTML DOM Parser

// Simple HTML DOM Parserを使った文字列からオブジェクトを生成
$html = str_get_html( '<html><body>Hello!</body></html>' );
  • Goutte
    オブジェクトを生成するためにはリクエスト通信を発生させないといけなさそう。
$client = new Client();
$crawler = $client->request('GET', 'https://www.symfony.com/blog/');
// 通信で取得したオブジェクトが解析対象になる

通信を発生させずにHTMLを解析!!

Goutteでもファイル内に記載されているHTMLを解析できそう。

ja.stackoverflow.com

下記のようにすると、通信を発生せずに引数に与えたHTML文字列を解析することができます。localhost:80に対して、通信を発生させているため、該当のサーバ上で80番ポートをListenさせている必要があります。

<?php
use Goutte\Client;

$client = new Client();
$crawler = $client->request('HEAD', null);
$crawler->clear();

// addHtmlContentの引数にHTML文字列
$crawler->addHtmlContent('<html><body>Hello!</body></html>');

echo $crawler->filter('html')->html();
  //=> <body>Hello!</body>
  
echo $crawler->filter('body')->text();
  //=> Hello!

『Laravel Collective』でのHTML生成を簡単にまとめてみる

Laravel

f:id:motikan2010:20170128223017p:plain LaravelでRailsの"link_to"のようにHTMLを生成できないのかと思い、探してみたら『Laravel Collective』というものがありました。
導入方法などは下記を参照。

Laravel Collective

HTML出力のイメージができないところがありましたので、実際に出力させて、まとめてみました。

Formタグ

基本

Form::open([‘url’ => ‘home’])

自動的にCSRFトークンが追加されて出力されます。

<form method="POST" action="http://localhost:3000/home" accept-charset="UTF-8">
<input name="_token" type="hidden" value="eAFowzR2efsBNQYrVzQ4VymRbYQB2afVyEDijzqd">

Form::close()

</form>

送信先にルーティング名

Form::open([‘route’ => ‘users.index’])

action属性にルーティングを適用することができます。

<form method="POST" action="http://localhost:3000/users" accept-charset="UTF-8">
<input name="_token" type="hidden" value="eAFowzR2efsBNQYrVzQ4VymRbYQB2afVyEDijzqd">

送信先にURLパラメータ

Form::model($user, [‘route’ => [‘users.update’, $user->id]])

ユーザIDといったURLパラメータをaction属性に含めることもできます。

<form method="POST" action="http://localhost:3000/users/1" accept-charset="UTF-8">
<input name="_token" type="hidden" value="eAFowzR2efsBNQYrVzQ4VymRbYQB2afVyEDijzqd">

PUTメソッド

Form::open([‘url’ => ‘home’, ‘method’ => ‘put’])

メソッドを区別するため「name=“_method"」属性を持っているinputタグが一緒に出力されます。

<form method="POST" action="http://localhost:3000/home" accept-charset="UTF-8">
<input name="_method" type="hidden" value="PUT">
<input name="_token" type="hidden" value="eAFowzR2efsBNQYrVzQ4VymRbYQB2afVyEDijzqd">

マルチパートフォーム

Form::open([‘url’ => ‘home’, ‘files’ => true])

<form method="POST" action="http://localhost:3000/home" accept-charset="UTF-8" enctype="multipart/form-data">
<input name="_token" type="hidden" value="eAFowzR2efsBNQYrVzQ4VymRbYQB2afVyEDijzqd">

ラベル

Form::label(‘email’, ‘E-Mail Address’)

<label for="email">E-Mail Address</label>

入力フォーム

テキスト

Form::text(‘username’)

第1引数にname属性の値を指定します。

<input name="username" type="text">

Form::text(‘email’, ‘example@example.com’)

第2引数に初期値(value属性の値)を指定します。

<input name="email" type="text" value="example@example.com" id="email">

パスワード

Form::password(‘password’, [‘class’ => ‘awesome’])

<input class="awesome" name="password" type="password" value="">

メールアドレス

Form::email(‘mail’‘, $value = null, $attributes = [])

<input name="mail" type="email">

数値

Form::number(‘name’, ‘value’)

<input name="name" type="number" value="value">

日付

Form::date(‘name’, \Carbon\Carbon::now())

<input name="name" type="date" value="2017-01-28">

ファイル

Form::file(‘name’, $attributes = [])

<input name="name" type="file">

チェックボックス

Form::checkbox(‘name’, ‘value’)

<input checked="checked" name="name" type="checkbox" value="value">

ラジオボタン

Form::radio(‘name’, ‘value’)

<input name="name" type="radio" value="value">

セレクトボックス

基本

Form::select(‘size’, [‘L’ => ‘Large’, ’S' => ‘Small’])

<select name="size">
  <option value="L">Large</option>
  <option value="S">Small</option>
</select>

初期値を指定

Form::select(‘size’, [‘L’ => ‘Large’, ’S' => ‘Small’], ’S')

第3引数に初期値を指定することができます。

<select name="size">
  <option value="L">Large</option>
  <option value="S" selected="selected">Small</option>
</select>

空の初期値を指定

Form::select(‘size’, [‘L’ => ‘Large’, ’S' => ‘Small’], null, [‘placeholder’ => ‘Pick a size…’])

空の初期値を設定することもできます。

<select name="size">
  <option selected="selected" value="">Pick a size...</option>
  <option value="L">Large</option>
  <option value="S">Small</option>
</select>

multiple属性を付与

Form::select(‘size’, [‘L’ => ‘Large’, ’S' => ‘Small’], null, [‘multiple’ => true])

multiple属性を付与するし、複数選択することができます。

<select multiple="1" name="size">
  <option value="L">Large</option>
  <option value="S">Small</option>
</select>

選択肢をグループ化

Form::select(‘animal’, [ ‘Cats’ => [‘leopard’ => ‘Leopard’], ‘Dogs’ => [‘spaniel’ => ‘Spaniel’], ])

<select name="animal">
  <optgroup label="Cats">
    <option value="leopard">Leopard</option>
  </optgroup>
  <optgroup label="Dogs">
    <option value="spaniel">Spaniel</option>
  </optgroup>
</select>

連続した値の選択肢

Form::selectRange(‘number’, 10, 20)

10 〜 20の値を指定することができます。

<select name="number">
  <option value="10">10</option>
  <option value="11">11</option>
  <option value="12">12</option>
  <option value="13">13</option>
  <option value="14">14</option>
  <option value="15">15</option>
  <option value="16">16</option>
  <option value="17">17</option>
  <option value="18">18</option>
  <option value="19">19</option>
  <option value="20">20</option>
</select>

月の選択肢

Form::selectMonth(‘month’)

<select name="month">
  <option value="1">January</option>
  <option value="2">February</option>
  <option value="3">March</option>
  <option value="4">April</option>
  <option value="5">May</option>
  <option value="6">June</option>
  <option value="7">July</option>
  <option value="8">August</option>
  <option value="9">September</option>
  <option value="10">October</option>
  <option value="11">November</option>
  <option value="12">December</option>
</select>

ボタン

Form::submit(‘Click Me!’)

<input type="submit" value="Click Me!">

オリジナルのフィールド

Form::myField()

フィールドを作成し、任意の名前で定義するこができます。

Form::macro('myField', function()
{
    return '<input type="awesome">';
});
<input type="awesome">

リンク

基本

<a href="http://localhost:3000/foo/bar">http://localhost:3000/foo/bar</a>

リンク文字列を指定

<a href="http://localhost:3000/home">Home</a>

リンク先をHTTPS

<a href="https://localhost:3000/foo/bar">https://localhost:3000/foo/bar</a>
<a href="http://localhost:3000/foo/bar.zip">http://localhost:3000/foo/bar.zips</a>

リンク先にルーティング名を使用

<a href="http://localhost:3000/login">http://localhost:3000/login</a>

URLパラメータを付与

“$parameters"引数に配列を与えることで、リンク先にURLパラメータを付与することができます。

<a href="http://localhost:3000/login?id=1&name=motikan2010">http://localhost:3000/login?id=1&name=motikan2010</a>

リンク先に"コントローラ@アクション"形式

<a href="http://localhost:3000/login">http://localhost:3000/login</a>

ScaffoldとAuthを使ってアプリケーションを作る

PHP Laravel

f:id:motikan2010:20170124011356p:plain
PHP製のWebフレームワークである『Laravel』を使って"認証あり"のアプリケーションを作成していきます。
今回はLaravelバージョン5.3系を使います。

プロジェクトの作成

$ laravel new auth_scaffold
$ cd auth_scaffold
$ php artisan --version
Laravel Framework version 5.3.29

Scaffoldの導入

github.com

下記のコマンドで容易に導入することができます。

$ composer require 'laralib/l5scaffold' --dev

サービスプロバイダーの追加

“config/app.php"内の"providers"に対して、
「Laralib\L5scaffold\GeneratorsServiceProvider::class」を追加します。
$ vim config/app.php

<?php
(省略)
    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
(省略)
        Laralib\L5scaffold\GeneratorsServiceProvider::class,
    ],
(以下省略)

上記の追加が完了したら、Scaffoldの準備完了です。

scaffoldの実行

下記のコマンドで コントローラ・モデル・ビュー の作成が完了します。
相変わらず便利すぎ!

$ php artisan make:scaffold Tweet --schema="title:string:default('Tweet #1'), body:text"

Configuring Tweet...
Migration created successfully
Seed created successfully.
Model created successfully.
Controller created successfully.
Layout created successfully.
Error created successfully.
Views created successfully.
Dump-autoload...
Route::resource("tweets","TweetController"); // Add this line in routes.php

予定通りに作成されたのかを確認してみます。

// コントローラ
$ ls app/Http/Controllers/TweetController.php
app/Http/Controllers/TweetController.php

// モデル
$ ls app/Tweet.php
app/Tweet.php

// ビュー
$ ls resources/views/tweets/
create.blade.php  edit.blade.php  index.blade.php  show.blade.php

しっかりと作成されているようです。
相変わらず早い!!

ルーティングの設定

作成されたコントローラを呼び出せないと意味がありませんので、 ルーティングの設定を行っていきます。
“routes/web.php"の末尾に「Route::resource("tweets”,“TweetController”);」を記述します。
$ vim routes/web.php

<?php
(省略)

Route::get('/', function () {
    return view('welcome');
});

Route::resource("tweets","TweetController"); // 追記

ルーティングの設定が反映されているか確認してみます。

$ php artisan route:list

+--------+-----------+---------------------+----------------+----------------------------------------------+--------------+
| Domain | Method    | URI                 | Name           | Action                                       | Middleware   |
+--------+-----------+---------------------+----------------+----------------------------------------------+--------------+
|        | GET|HEAD  | /                   |                | Closure                                      | web          |
|        | GET|HEAD  | api/user            |                | Closure                                      | api,auth:api |
|        | POST      | tweets              | tweets.store   | App\Http\Controllers\TweetController@store   | web          |
|        | GET|HEAD  | tweets              | tweets.index   | App\Http\Controllers\TweetController@index   | web          |
|        | GET|HEAD  | tweets/create       | tweets.create  | App\Http\Controllers\TweetController@create  | web          |
|        | DELETE    | tweets/{tweet}      | tweets.destroy | App\Http\Controllers\TweetController@destroy | web          |
|        | PUT|PATCH | tweets/{tweet}      | tweets.update  | App\Http\Controllers\TweetController@update  | web          |
|        | GET|HEAD  | tweets/{tweet}      | tweets.show    | App\Http\Controllers\TweetController@show    | web          |
|        | GET|HEAD  | tweets/{tweet}/edit | tweets.edit    | App\Http\Controllers\TweetController@edit    | web          |
+--------+-----------+---------------------+----------------+----------------------------------------------+--------------+

データベースの作成

ここの説明は簡単に流します。
データベースの名前は「scaffold_app」にしておきます。 mysql> create database scaffold_app; データベースの設定は下記のファイルに行います。 - ユーザ名:laravelusr - パスワード:himitsu
にしています。
$ vim .env

(省略)

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=scaffold_app
DB_USERNAME=laravelusr
DB_PASSWORD=himitsu

(省略)

上記設定が完了したらテーブルの作成を行います。

マイグレーション

下記のコマンドを実行することによって、テーブルが作成されます。
ここでは、tweetsテーブル以外にも作成されていますが、無視しておきます。

$ php artisan migrate

Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2017_01_23_143819_create_tweets_table

Webアプリケーションへアクセス

これで、Webアプリケーションとして利用することが可能になりました。

$ php artisan serve --host 0.0.0.0 --port 3000

http://127.0.0.1:3000/tweets"にアクセス f:id:motikan2010:20170124004456p:plain
上記のような画面が表示されたら成功です。
試しになにか投稿してみましょう。「Create」を押して投稿画面へ遷移できます。
f:id:motikan2010:20170124004459p:plain
投稿することができました。
「View」を押すことで詳細画面へ遷移することもできます。

f:id:motikan2010:20170124004502p:plain

これはこれで良いアプリケーションとなっていますが、これから認証機能を実装してきます。

認証機能(Auth)の導入

魔法の呪文『make:auth』

下記のコマンドを実行します。
$ php artisan make:auth
認証に必要な コントローラ・モデル・ビュー の作成が完了します。
Scaffold同様、作成されているかを確認します。

// コントローラ
$ ls app/Http/Controllers/Auth/
ForgotPasswordController.php  LoginController.php  RegisterController.php  ResetPasswordController.php

// モデル
$ ls app/User.php
app/User.php

// ビュー
$ ls resources/views/auth/
login.blade.php     passwords/      register.blade.php

作成されているのが確認できます。
次にルーティングの設定ですが、Authの場合は自動的に記述されます。
念のために確認してみます。
$ cat routes/web.php

<?php
(省略)

Route::get('/', function () {
    return view('welcome');
});

Route::resource("tweets","TweetController");

Auth::routes();    // 自動的に追加されている

Route::get('/home', 'HomeController@index');

ルーティングが反映されているか改めて確認してみます。

$ php artisan route:list

+--------+-----------+------------------------+----------------+------------------------------------------------------------------------+--------------+
| Domain | Method    | URI                    | Name           | Action                                                                 | Middleware   |
+--------+-----------+------------------------+----------------+------------------------------------------------------------------------+--------------+
|        | GET|HEAD  | /                      |                | Closure                                                                | web          |
|        | GET|HEAD  | api/user               |                | Closure                                                                | api,auth:api |
|        | GET|HEAD  | home                   |                | App\Http\Controllers\HomeController@index                              | web,auth     |
|        | POST      | login                  |                | App\Http\Controllers\Auth\LoginController@login                        | web,guest    |
|        | GET|HEAD  | login                  | login          | App\Http\Controllers\Auth\LoginController@showLoginForm                | web,guest    |
|        | POST      | logout                 | logout         | App\Http\Controllers\Auth\LoginController@logout                       | web          |
|        | POST      | password/email         |                | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail  | web,guest    |
|        | GET|HEAD  | password/reset         |                | App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm | web,guest    |
|        | POST      | password/reset         |                | App\Http\Controllers\Auth\ResetPasswordController@reset                | web,guest    |
|        | GET|HEAD  | password/reset/{token} |                | App\Http\Controllers\Auth\ResetPasswordController@showResetForm        | web,guest    |
|        | POST      | register               |                | App\Http\Controllers\Auth\RegisterController@register                  | web,guest    |
|        | GET|HEAD  | register               | register       | App\Http\Controllers\Auth\RegisterController@showRegistrationForm      | web,guest    |
|        | GET|HEAD  | tweets                 | tweets.index   | App\Http\Controllers\TweetController@index                             | web          |
(省略)
|        | GET|HEAD  | tweets/{tweet}/edit    | tweets.edit    | App\Http\Controllers\TweetController@edit                              | web          |
+--------+-----------+------------------------+----------------+------------------------------------------------------------------------+--------------+

問題なく認証機能へアクセスできそうです。

認証の確認

認証を行っていないユーザはログイン画面へ遷移させるようにしましょう。 $ vim app/Http/Controllers/TweetController.php

<?php namespace App\Http\Controllers;

use App\Http\Requests;
use App\Http\Controllers\Controller;

use App\Tweet;
use Illuminate\Http\Request;

class TweetController extends Controller {

        public function __construct(){
            $this->middleware('auth');
        }
(省略)

これで認証を行っていないユーザはTweetControllerのアクションへアクセスできないようになります。

認証後の遷移先を変更

実は認証に必要なアカウントの「登録後」や「認証後」には"/home"へ遷移されるように、Authのコントローラで設定されています。 “/home"には特になにもアプリケーションがありませんので、そこへ遷移されても楽しくありません。  事前に「登録後」や「認証後」には”/tweets"へ遷移されるように設定します。
行う作業は非常に簡単です。
下記2つのファイルを修正するだけです。
$ vim app/Http/Controllers/Auth/RegisterController.php

<?php

namespace App\Http\Controllers\Auth;

use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;

class RegisterController extends Controller
{

(省略)

    protected $redirectTo = '/tweets'; // "home" を "tweets" に変更
(省略)

$ vim app/Http/Controllers/Auth/LoginController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;

class LoginController extends Controller
{

(省略)
    
    protected $redirectTo = '/tweets'; // "home" を "tweets" に変更
(省略)

これで完成です。

アプリケーションを確認

下記のコマンドでアプリケーションを起動しましょう。
$ php artisan serve –host 0.0.0.0 –port 3000

http://127.0.0.1:3000/」にアクセスすると下記のような画面が表示されます。
f:id:motikan2010:20170124004509p:plain
ためしに「http://127.0.0.1:3000/tweets」に直接アクセスしてみましょう。
当然ながら認証を行っていませんので、ログイン画面へ遷移されますf:id:motikan2010:20170124004523p:plain
まだ、ユーザは存在していないので「Register」からユーザの作成を行ってみます。 f:id:motikan2010:20170124004517p:plain
ユーザの作成が完了すると、認証処理が行われTweets画面へ遷移されます。 f:id:motikan2010:20170124004526p:plain

こんな短時間でこのようなアプリケーションが作成できるとは驚きです。
今回は誰でもがアカウントを作成することができ、認証後のページにアクセスすることが可能となっていますが、コードをいじってみることで自分だけがアクセスできるサイトを作れそうですね。