TwilioとRailsで、1通1円のSMS認証を実装してみる


## 3行まとめ

- Twilioなら、自分だけの電話番号を簡単取得。
- SMS受信のタイミングで任意のAPIをキックしてくれます。送信者の情報も取得できるので、それを用いて認証機能を実現。
- コストは1通（1認証）あたり1円！（ただし最初にチャージが必要なので初期投資2000円から）

## 概要

### SMS認証とは

ユーザ登録を行うメディアにおいて、
不正なユーザ（いわゆる複アカなど）の防止策として有効な対策の一つです。

例えばこちら等。
FullCourt
https://www.fullcourt.co/ja/docs/samplecode/smsauthentication

ざっくり言うと、

1. ユーザは認証画面で電話番号を入力
1. メディアサーバは、SMS認証サービスへSMS送信依頼
1. SMS認証サービスはユーザのスマホ/携帯電話にSMSを送信
1. ユーザはSMSで送られてきた認証トークン（パスワード）を見て、認証画面に入力
1. サーバはユーザからの入力内容と、SMS送信時にDBに保管しておいたトークンを突き合わせ、認証

![スクリーンショット_2014_02_04_9_45.png](https://qiita-image-store.s3.amazonaws.com/0/8790/7c816c28-29b5-9277-81b3-fbc3198bb499.png "スクリーンショット_2014_02_04_9_45.png")

こんな流れです。
電話番号が取得出来るので、以後の複アカを防ぐこともできます。

導入事例の有名どころだとアメーバピグさん等。
【お知らせ】SMS認証について
http://ameblo.jp/pigg-staff/entry-11522530986.html

ですが、実際の導入にはなかなかのネックが存在します。

### 問題は・・

コスト。
1通（1認証）10円前後が相場だと思います。
上記のFullCourtさんだと[8円／日本国内1通](https://www.fullcourt.co/ja/sms)とのことでした。

けっこうなお値段ですが、SMS送信自体の価格を各キャリアが定めているので、
どうしても最低限そこはコストとなってしまいます。

### 対策

ということで、今回は電話・SMSを気軽に開発者が扱えるサービスTwilioを使って
もっと安く仕上げられそうな仕組みを実装してみました。



## Twilioとは
電話やSMSを扱えるAPIサービス。
http://twilio.kddi-web.com/
シリコンバレーの企業のサービスですが、日本ではKDDIが
代理店になっており、国内でも気軽に扱えます。

### 価格

料金（Price）
http://twilio.kddi-web.com/price/index.html

最初の表の下の方ですね。
送信料はやはり10円弱（Softbankだけ安いですが）。
しかし、<strong>受信料は1円</strong>です。

ということで、<strong>受信のみで成り立つSMS認証</strong>を考えてみたいと思います。

## 受信型認証のイメージ

これでどうでしょう。

1. 認証画面で、ユーザに電話番号とユーザ識別ID、認証トークン（パスワード）を表示
- ユーザはスマホ/携帯電話から、指定の電話番号宛に、ユーザ識別IDと認証トークンをSMS送信
- TwilioサーバはMediaAPIをキック
- サーバはTwilioから渡された認証トークンより、ユーザを認証。

![スクリーンショット_2014_02_04_9_42.png](https://qiita-image-store.s3.amazonaws.com/0/8790/b9aa3a99-d975-2e45-6332-7d6f619518e7.png "スクリーンショット_2014_02_04_9_42.png")

2の部分が<strong>受信</strong>になっていますね。これなら<strong>1通（1認証）1円</strong>です！

ということで、こちらで実装をすすめていきます。

## Twilioアカウントの作成と電話番号の取得

まずは、以下記事を参考にアカウントの作成を行います。

Twilio（トゥイリオ）を触ってみた
http://www.casleyconsulting.co.jp/blog-engineer/twilio/twilio%EF%BC%88%E3%83%88%E3%82%A5%E3%82%A4%E3%83%AA%E3%82%AA%EF%BC%89%E3%82%92%E8%A7%A6%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F/

記事内までで、トライアルの電話番号で電話利用ができるようになりますが、
<strong>SMSは利用できません。</strong>

次はSMSを利用できる電話番号を取得します。

## SMS用電話番号の取得

注：ここからはお金がかかります！
（電話番号維持費は月1ドル。ただしはじめに2000円のチャージが必要なため、実質2000円）

※有料アカウントの体系を詳しく知っておきたい方は一度以下に目を通しておくと良いかもしれません。
よくある質問(FAQ)/課金 & 料金
https://twilioforkwc.zendesk.com/forums/22006727-%E8%AA%B2%E9%87%91-%E6%96%99%E9%87%91


それでは行きましょう。
まず、「電話番号」のページから、「電話番号を購入」をクリック。


![スクリーンショット_2014_02_03_9_31-2.png](https://qiita-image-store.s3.amazonaws.com/0/8790/413b3d34-f5d3-44d4-0d07-a192024805c1.png "スクリーンショット_2014_02_03_9_31-2.png")


国は「アメリカ」を選択（※）し、特に何も入力せず「検索」をクリック。

![スクリーンショット_2014_02_03_9_36.png](https://qiita-image-store.s3.amazonaws.com/0/8790/b64425a6-08d7-cc1b-e660-12560d298413.png "スクリーンショット_2014_02_03_9_36.png")

※現在日本の電話番号ではSMSは使えないようです。
しかも海外の番号の方がずっと安いので、迷わず取得しましょう。

この後、一覧に出てきた番号をクリック（どれでも良いです。）すると、
「アップグレードが必要」と言われます。

そのまま指示に従い、クレカ情報を入力すればokです。
※途中で自動チャージのON/OFFを選択しますが、とりあえずOFFの方が無難でしょう。

この後、自分はだいぶハマったのですが、
しばらく時間が立たないとアップグレードされず、電話番号を購入できるようになりません。

しかもあるページではアップグレード済みの表示（※）なのに、
電話番号購入ページではトライアルアカウント表示のままになっていたり。

※グローバルナビ右の登録アドレスの下に、
トライアルアカウントの場合は「トライアル」、
アップグレードした場合はチャージ分の金額が表示されます。

ちなみに自分はブラウザを変えて閲覧したら上手くいきました。
セッション管理等の実装に不具合があるのかもしれません。


ともかく、これでSMS用電話番号が取得できました！

## SMS送信時にキックするAPIを用意する

Twilioでは、取得した電話番号にSMSが届いたタイミングでキックするAPIを設定することができます。

取得電話番号のページから簡単に設定できます。

![スクリーンショット_2014_02_04_23_29.png](https://qiita-image-store.s3.amazonaws.com/0/8790/b9cde726-9237-4f9a-e7c9-a47fc0146b48.png "スクリーンショット_2014_02_04_23_29.png")

緑の枠内にキックするAPIを設定すればok。

テストしてみましょう。
取得した電話番号宛にSMSを送信します。

![スクリーンショット_2014_02_04_9_53.png](https://qiita-image-store.s3.amazonaws.com/0/8790/241602da-6589-98f6-56b6-c79e99da7a8e.png "スクリーンショット_2014_02_04_9_53.png")


キックしたサーバのログを確認すると・・

![スクリーンショット_2014_02_04_23_10.png](https://qiita-image-store.s3.amazonaws.com/0/8790/8c782bdf-5144-ae2f-a284-962fd1f48436.png "スクリーンショット_2014_02_04_23_10.png")



来ています！

たったこれだけで、以下の図で言う2,3の箇所が実現出来てしまいました。

![スクリーンショット_2014_02_04_9_42.png](https://qiita-image-store.s3.amazonaws.com/0/8790/b9aa3a99-d975-2e45-6332-7d6f619518e7.png "スクリーンショット_2014_02_04_9_42.png")


あとは、投げられてくるパラメータを料理して、1,3,4の箇所を実装しましょう。

取得出来るパラメータはこうです。

```ruby
{
    "AccountSid"=>"hogehoge",
    "MessageSid"=>"SMbfea2d641027f6f3434bf656c0b55d97",
    "Body"=>"Hello, Qiita!",
    "ToZip"=>"60081",
    "ToCity"=>"FOX LAKE",
    "FromState"=>"",
    "ToState"=>"IL",
    "SmsSid"=>"SMbfea2d641027f6f3434bf656c0b55d97",
    "To"=>"+18xxxxxxxx",
    "ToCountry"=>"US",
    "FromCountry"=>"JP",
    "SmsMessageSid"=>"SMbfea2d641027f6f3434bf656c0b55d97",
    "ApiVersion"=>"2010-04-01",
    "FromCity"=>"",
    "SmsStatus"=>"received",
    "NumMedia"=>"0",
    "From"=>"+8180xxxxxxxx",
    "FromZip"=>""
}
```

ユーザの送信内容や電話番号などがPOSTパラメータとして取得できる訳ですね。

```ruby
params[:From] # ユーザの電話番号
params[:Body] # ユーザ入力値。ユーザidと認証トークンを入力してもらう。
```

※ユーザ入力値（params[:Body]）は、今回は

```ruby
user_id + '_' + token
```

の形式を指定（認証ページでユーザに依頼）すると都合が良さそうです。

さて、これらがあれば1,3,4は実装できそうですね。
こんなイメージでしょうか。

```ruby
class SmsAuthController < ApplicationController
  TWILIO_NUMBER = '+81xxxxxxxx'  # 電話番号

  # 認証ページ（GET）
  # viewでは@sender_num, @user.sms_token, @user.idを表示
  def index
    @user = User.find(current_user.id)
    @sender_num = TWILIO_NUMBER
    @user.sms_token = generate_token
    @user.save!
  end

  # 認証処理（POST）
  def verify
    begin
      params_check;format_params;user_check;token_check;
      logger.error("valid user!")
      render :json => { code: 0 }
    rescue => e
      logger.error("error: #{e.message}")
      render :json => { code: 400, message: e.message }
      return
    end
  end

  private
  # 認証token生成
  def generate_token
    ((0..9).to_a + ("a".."z").to_a + ("A".."Z").to_a).sample(4).join
  end

  # パラメータチェック
  def params_check
    raise 'Body is invalid' if params[:Body].nil?
    raise "Body isn't contain underscore" if !params[:Body].include?("_")
    raise 'From is invalid' if params[:From].nil?
  end

  # パラメータ整形
  # params[:Body]は「user_id + '_' + token」の形式を想定
  def format_params
    body = params[:Body].split("_")
    @user_id = body[0]
    @token = body[1]
    @user.mobile_number = params[:From].sub(/^\+81/, '0')
    raise 'user_id is nil' if @user_id.nil?
    raise 'token is nil' if @token.nil?
    raise 'mobile_number is nil' if @user.mobile_number.nil?
  end

  # ユーザチェック
  def user_check
    @user = User.find_by_id(@user_id)
    raise 'user is invalid' if @user.nil?
  end

  # 認証トークンチェック
  def token_check
    raise 'token is invalid' if @user.sms_token == @token
    @user.verify = true
    @user.save!
  end

end
```

indexが1、verifyが3,4の工程にあたりますね。

![スクリーンショット_2014_02_04_9_42.png](https://qiita-image-store.s3.amazonaws.com/0/8790/b9aa3a99-d975-2e45-6332-7d6f619518e7.png "スクリーンショット_2014_02_04_9_42.png")


ひとまず、これにて完成です。

Twilioを用いることで、本当に「1認証1円」が実現できました。
これまでコストの面でSMS認証の導入を見送っていた方も、ぜひ検討してみてください！

## 最後に

以下の点をブラッシュアップしていきたいなと思っています。

・ユーザビリティの検討（ユーザに一定の情報量（ユーザ識別ID/認証token）を入力してもらうことになるため）。
・電話番号を偽装してSMS送信されることはないか？その他不正行為の検討＆対策。
・ちょっと捻れば、「受信型SMS認証サービス」として第三者へ提供することも・・

今回は完全に手探りでの検討だったので、その他仕組みの不足点など
お気づきの点ありましたら突っ込み頂けますと幸いです！