Rails5から使えるhas_secure_tokenをMySQLで使うときのまとめ

# has_secure_token

Rails5からAPI modeが実装されたのに伴って、has_secure_tokenがRailsにマージされて標準機能になりました。平たくいうとhas_secure_passwordのtokenバージョン、と思えば大丈夫です。

実装については該当のPullRequestを貼っておくのでご参照してください。
https://github.com/rails/rails/pull/18217

また、元々公開されていたGithub URLも合わせて貼っておきます。
https://github.com/robertomiranda/has_secure_token

## 使い方

下記のように定義すれば `token`カラムに対して、`has_secure_token`メソッドで生成されるtokenがUserオブジェクトを生成するときに挿入されるようになります。

```rb
class User < ApplicationRecord
  has_secure_token
end
```

挿入されるカラムを変更したいときは明示的に指定すればOKです。
下記の例だと `auth_token`カラムに対して動作するようになります。

```rb
class User < ApplicationRecord
  has_secure_token :auth_token
end
```

オブジェクトが保存されると自動でtokenが生成されます。

```rb
>> user = User.new(name: 'kobito')
>> user.save
=> true

>> user.auth_token
=> "fppFh6xAsGfNTiDxzreW283f"
```

tokenの再生成もできます。

```rb
>> user.regenerate_auth_token
=> true

>> user.auth_token
=> "Nr8MmABvZSceLLdaEf3ALmW4"
```

Generatorから生成するときは`:token`と定義してやれば、ModelファイルとMigrationファイルが生成できます。

```bash
$ rails g model user name:string auth_token:token
```

```rb
class CreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :auth_token

      t.timestamps
    end
    add_index :users, :auth_token, unique: true
  end
end
```

## MySQLにおける問題点

MySQLは文字列の照合順序というパラメータを持っていて、下記の3パターンが用意されています。

|name|description|
|----|---|
|ci|Case Insensitive: 大文字小文字を区別しない|
|cs|Case Sensitive: 大文字小文字を区別する|
|bin|Binary: バイナリ比較をする（大文字小文字を区別する）|

日本語を表せる文字コードの場合は`ci`と`bin`のみが用意され、かつデフォルトが`ci`になっています。そのためデフォルトでは大文字小文字を区別せず比較してしまいます。[^1]

[^1]: http://qa.atmarkit.co.jp/q/2036

使い方の例で示したようにhas_secure_tokenで生成されるtokenは大文字小文字を区別する前提で発行されているので`ci`のままで利用するとtokenの衝突が発生する原因となってしまいます。

```sql
mysql> SELECT auth_token FROM users where id = 1
+--------------------------+
| auth_token               |
+--------------------------+
| p7YL2xQL2vaod85sn7j8Mui8 |
+--------------------------+

mysql> SELECT id from users where auth_token = 'p7YL2xQL2vaod85sn7j8Mui8'
+----+
| id |
+----+
|  1 |
+----+

-- 全て大文字にしたtokenでも取得できてしまう
mysql> SELECT id from users where auth_token = 'P7YL2XQL2VAOD85SN7J8MUI8'
+----+
| id |
+----+
|  1 |
+----+
```

そのため、tokenを挿入するカラムを`bin`に指定してやる必要があります。（特に影響がないのであれば、テーブル単位で設定しても良いと思います）
Rails5から@kamipoさんのご尽力でMySQLで文字列カラムに対して`charset`や`collation`が指定できるようになりました[^2]

```rb
class CreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :auth_token, charset: 'utf8', collation: 'utf8_bin'

      t.timestamps
    end
    add_index :users, :auth_token, unique: true
  end
end
```

[^2]: http://qiita.com/mitz/items/b2295e440585ba63b083#mysql%E3%81%A7string%E3%82%84text%E3%82%AB%E3%83%A9%E3%83%A0%E3%81%AB%E5%AF%BE%E3%81%97%E3%81%A6-collation-%E3%82%84-charset-%E3%82%92%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88

https://github.com/rails/rails/issues/20133 のPullRequestで上記で挙げた問題があるので`SecureRandom.hex`使った方がいいんじゃないか、という提案がされています。
PRに対する反応を見た感じ、case sensitiveで対応すればいいよね、という流れなのでMySQLでhas_secure_tokenを利用される場合はcollationの指定を忘れずにすることが必須となりそうです。

# まとめ

- Rails5からActiveRecordでhas_secure_tokenというtoken生成メソッドが使えるようになった
- has_secure_passwordと同じ感覚で使える
- MySQLで利用する場合は文字列の照合順序を`bin`にすることを忘れずに！
- @kamipoさんに感謝しましょう