意外に簡単!LINEのような電話番号認証の作り方(Twilio-SMS編)
LINEで使われているような電話番号による認証を試しに作ってみたので、ブログにまとめておきます。
電話番号認証とはこんなのです。
まず、電話番号を用いた認証ですが、方式が2通りありますので、それらの概要を説明します。
- SMS利用パターン
電話番号をアプリ側で入力すると、サーバから4桁の暗証番号入りのSMSが届き、その4桁の暗証番号をアプリ側で入力することで電話番号が正しいことを証明するやりかたです。
SMSを送信するには送信元の番号が必要になります。また、例えばアメリカ、カナダのようにショートコード(SMSで利用される事業者コード)を取得する必要のある国もあります。ショートコードは審査が入るため、数ヶ月取得に要することがあります。今回は送信者番号は通常の電話番号にします。
- IVR利用パターン
IVRとはInteractive Voice Responseの略で、自動音声応答による認証です。
事前にサービスに電話番号を登録しておき、ユーザにサービスの電話番号へ電話をかけさせることで、その番号を認証するInbound方式と、サービス側から登録した電話番号に電話をかけるOutbound方式があります。これはサービスを展開する国の電話料金、国際電話、国内電話の条件にあわせて使い分けることになります。
SMS認証だけでは、SMSが到達しなかったりとエラーをハンドリングした運用が必要になるため、補足的にIVRをリカバリ手段として提供するケースが多いです。
今回はSMS利用パターンで電話番号認証を作ってみました。
まずは、SMSを送信するためのサービスプロバイダーが何社かありますので、そのなかの一つを使ってみます。
SMS/IVR両方可能 無料枠が$20あり、開発用に自分に電話するだけであればある程度無料で利用可。
IVRのみ可能。日本のサービスなのでサポートが安心?
日本企業ですが、B2B型なので、個人サービサーがさくっとはじめるには敷居が高いかも。
ということで、Twilioの無料枠内で実験してみました。
SMS送信元番号で利用する電話番号はTwilio内で取得可能なので、それを使います。
Twilioで日本の番号を取得しようとするとSMSがno availableです。月$5で電話番号が取得できます。
なので、USでてきとーに取得します。USの番号はSMS利用可能です。USは月$1で電話番号が取得できます・・・。すごい。
次にSMS送信方法ですが、以下のAPIで簡単にSMSが送信できます。
curl -X POST 'https://api.twilio.com/2010-04-01/Accounts/{AccountSid}/SMS/Messages.json' \ -d 'From={+81のような国際電話形式の送信元電話番号}' \ -d 'To={+81のような国際電話形式の送信先電話番号}' \ -d 'Body={メッセージ本文}' \ -u {AccountSid}:{AuthToken}
更にTwilioは各言語のためのSDKを用意してくれているので、それを使えばさくっとAPI利用できます。
Javaでは以下の感じです。
// Install the Java helper library from twilio.com/docs/libraries import com.twilio.sdk.TwilioRestClient; import com.twilio.sdk.TwilioRestException; import com.twilio.sdk.resource.factory.SmsFactory; import com.twilio.sdk.resource.instance.Sms; import com.twilio.sdk.resource.list.SmsList; import java.util.HashMap; import java.util.Map; public class Example { // Find your Account Sid and Token at twilio.com/user/account public static final String ACCOUNT_SID = "SIDを設定してね"; public static final String AUTH_TOKEN = "{{ auth_token }}"; public static void main(String[] args) throws TwilioRestException { TwilioRestClient client = new TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN); // Build a filter for the SmsList Map<String, String> params = new HashMap<String, String>(); params.put("Body", "メッセージ本文"); params.put("To", "+81901234XXXX"); params.put("From", "+81909876XXXX"); SmsFactory messageFactory = client.getAccount().getSmsFactory(); Sms message = messageFactory.create(params); System.out.println(message.getSid()); } }
これを利用して、AWS環境でSMS認証サーバを構築しました。下図に処理フローをまとめてあります。認証用の一時データ保存領域としてMemcache(ElastiCache)を、QueueとしてSQSを使いました。
基本的には図に書いてある通りです。
補足としては... Twilioはショートコードを利用するとSMSの一括大量送信が可能になるのですが、今回はショートコードを取得していない関係上、Rate Limitで1送信元電話番号あたり1秒に1通しか送信できないという制限にひっかかってしまいます。そこで、どうせ月1ドルなので、スケールさせたい数だけ電話番号を取得して、取得した番号の数だけ、スレッドプールにSMS送信スレッドをプールする方式をとりました。それで購入した電話番号の数だけ並列送信が可能になります。そのためにQueueを利用しています。
- 実装環境
サーバ : AWS EC2
Memcache : AWS ElastiCache
Queue : AWS SQS
FW : Play2.0.4
言語 : Java
今更だけどEnumでSingleton
Java1.5からEnum型が追加され、定数の定義等に利用されていますが、EffectiveJava久々に読み直していたら、EnumでSingletonをやるのがおしゃれさんだよと書いてあったので、ブログにメモしときます。
周知の事実すぎるので、今更かよ感ありますが、知らなかった人は明日からやってみてください。
Java1.5より昔は以下の様にSingletonにしていたと思います。ここでは遅延初期化とか、スレッドセーフかどうかとか、シリアライズで複製されちゃうじゃんとかそういうことは分かり難くなるので、省きます。そうすると、こんな感じですね。
package singleton; public class TraditionalSingleton { private static final TraditionalSingleton INSTANCE = new TraditionalSingleton(); private TraditionalSingleton(){} public static TraditionalSingleton getInstance(){ return INSTANCE; } public void method(){ System.out.println("this is singleton"); } }
それをEnumでやると以下のようになります。Enumで定数化したときに、コンパイルすると実際は、public static final hogeになっているので、結局は同じことなんですが、可読性及び、シリアライズに対応していたり、リフレクションによるprivateコンストラクタ実行への防御とか、モロモロいい感じにしてくれます。
package singleton; public enum ModernSingleton { INSTANCE; public void method(){ System.out.println("this is singleton"); } }
実行は以下。
TraditionalSingleton.getInstance().method(); ModernSingleton.INSTANCE.method();
今まで知らなかったのが恥ずかしい。
ReverseAuthの使い方 #twtr_hack
Account.frameworkで既にTwitter認証済みのACAccountを使い回してiOS側からSLRequestやTWRequestでAPIコールをしているだけであれば、必要ないのですが、どうしてもサーバにもAccsessTokenやAccessTokenSecretを渡したい場合があります。
しかしながら、ServiceTypeがTwitterの場合、ACAccountにはACCredentialがあるもののその中のoauthTokenは空っぽです。Facebookの場合は、入っています。
つまり、ACAccountStoreでのTwitter認証の場合、tokenの生データに触れることができません。
そこで必要になるのがReverseAuthになります。
詳細はこちらにかいてあります。
https://dev.twitter.com/docs/ios/using-reverse-auth
事前準備
ReverseAuthをはじめるまえに、TwitterDeveloperページにてアプリケーション登録を済ませ、コンシューマーキーとコンシューマーシークレットを取得しておいてください。
2つのリクエストをTwitterに対して投げるとトークンを取得することが出来ます。
Step1 Special Request Tokenの取得
HTTP HEADERに認証や署名情報を含んだSingedRequestをbodyにx_auth_modeをreverseauthとして
https://api.twitter.com/oauth/request_token
に対して送信すると、AccessToken取得時に使用するtoken類一式を取得できます。
Step2 Access Tokenの取得
step1で取得したresponseの内容をそのままinputとして
https://api.twitter.com/oauth/access_token
に対して送信するとoauth_token、oauth_tokenSecret、screen_name、user_id等を取得できます。
Step1 Special Request Tokenの取得(詳細)
Special Request Tokenの取得では、Singed Requestを送信する必要があります。
例えば以下のリクエストのようなヘッダを付与する必要があります。
https://dev.twitter.com/docs/auth/authorizing-request より転載
POST /1/statuses/update.json?include_entities=true HTTP/1.1 Accept: */* Connection: close User-Agent: OAuth gem v0.4.4 Content-Type: application/x-www-form-urlencoded Authorization: OAuth oauth_consumer_key="xvz1evFS4wEEPTGEFPHBog", oauth_nonce="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", oauth_signature="tnnArxj06cWHq44gCs1OSKk%2FjLY%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1318622958", oauth_token="370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", oauth_version="1.0" Content-Length: 76 Host: api.twitter.com status=Hello%20Ladies%20%2b%20Gentlemen%2c%20a%20signed%20OAuth%20request%21
作り方は以下になります。
Building the header string
To build the header string, imagine writing to a string named DST.
Append the string "OAuth " (including the space at the end) to DST.
For each key/value pair of the 7 parameters listed above:
Percent encode the key and append it to DST.
Append the equals character '=' to DST.
Append a double quote '"' to DST.
Percent encode the value and append it to DST.
Append a double quote '"' to DST.
If there are key/value pairs remaining, append a comma ',' and a space ' ' to DST.
Pay particular attention to the percent encoding of the values when building this string. For example, the oauth_signature value of tnnArxj06cWHq44gCs1OSKk/jLY= must be encoded as tnnArxj06cWHq44gCs1OSKk%2FjLY%3D.
Performing these steps on the parameters collected above results in the following string:
OAuth oauth_consumer_key="xvz1evFS4wEEPTGEFPHBog", oauth_nonce="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", oauth_signature="tnnArxj06cWHq44gCs1OSKk%2FjLY%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1318622958", oauth_token="370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", oauth_version="1.0"
This value should be set as the Authorization header for the request.
この一連のSingnedRequestを作る処理をTwitterの中の方?がGithubにサンプルを公開してくれていますので、それを使うと凄く簡単です。
https://github.com/seancook/TWReverseAuthExample
Step2 Access Tokenの取得(詳細)
#define TW_X_AUTH_MODE_KEY @"x_auth_mode" #define TW_X_AUTH_MODE_REVERSE_AUTH @"reverse_auth" #define TW_X_AUTH_REVERSE_PARMS @"x_reverse_auth_parameters" #define TW_X_AUTH_REVERSE_TARGET @"x_reverse_auth_target" #define TW_OAUTH_URL_REQUEST_TOKEN @"https://api.twitter.com/oauth/request_token" #define TW_OAUTH_URL_AUTH_TOKEN @"https://api.twitter.com/oauth/access_token" - (IBAction)reverseAuth:(id)sender { // Step 1) Ask Twitter for a special request_token for reverse auth NSURL *url = [NSURL URLWithString:TW_OAUTH_URL_REQUEST_TOKEN]; // "reverse_auth" is a required parameter NSDictionary *dict = [NSDictionary dictionaryWithObject:TW_X_AUTH_MODE_REVERSE_AUTH forKey:TW_X_AUTH_MODE_KEY]; TWSignedRequest *signedRequest = [[TWSignedRequest alloc] initWithURL:url parameters:dict requestMethod:TWSignedRequestMethodPOST]; [signedRequest performRequestWithHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!data) { [self dismissProgress:@"Error occurred in Step 1."]; } else { NSString *signedReverseAuthSignature = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; // Step 2) Ask Twitter for the user's auth token and secret // include x_reverse_auth_target=CK2 and x_reverse_auth_parameters=signedReverseAuthSignature parameters dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSDictionary *step2Params = [NSDictionary dictionaryWithObjectsAndKeys:[TWSignedRequest consumerKey], TW_X_AUTH_REVERSE_TARGET, signedReverseAuthSignature, TW_X_AUTH_REVERSE_PARMS, nil]; NSURL *authTokenURL = [NSURL URLWithString:TW_OAUTH_URL_AUTH_TOKEN]; //-- iOS5 ----- //TWRequest *step2Request = [[TWRequest alloc] initWithURL:authTokenURL parameters:step2Params requestMethod:TWRequestMethodPOST]; //------------- //-- iOS6 ----- SLRequest *step2Request = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodPOST URL:authTokenURL parameters:step2Params]; //------------- [step2Request setAccount:self.selectedAccount]; [step2Request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) { if (!responseData || ((NSHTTPURLResponse*)response).statusCode >= 400) { [self dismissProgress:@"Error occurred in Step 2."]; } else { NSString *responseStr = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; NSLog(@"AuthData : %@",responseStr); } }]; }); } }]; }
サンプルアプリをGithubに置いているのでご利用ください。
該当部分は、ACAccountDetailViewControllerあたりになります。
https://github.com/i2key/SocialframeworkExample
Play2系でplayコマンド実行時にsbtが無いと言われる場合の対処法
下記のようなエラーが発生したときは、project/build.propertiesのsbtのバージョンを確認すること。
ソースは古いplayのままでframework側のsbtが新しいかもしれない。
project/build.properties内のsbtバージョンを正しいバージョンに書き換えれば動く。
$ play run Picked up _JAVA_OPTIONS: -Xms1024m -Xmx1024m Getting org.scala-sbt sbt 0.11.3 ... :: problems summary :: :::: WARNINGS module not found: org.scala-sbt#sbt;0.11.3 ==== local: tried /Users/Shared/play-2.1-RC3/repository/local/org.scala-sbt/sbt/0.11.3/ivys/ivy.xml ==== Maven2 Local: tried file:///Users/kurodaitsuki/.m2/repository/org/scala-sbt/sbt/0.11.3/sbt-0.11.3.pom ==== typesafe-ivy-releases: tried http://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt/0.11.3/ivys/ivy.xml ==== Maven Central: tried http://repo1.maven.org/maven2/org/scala-sbt/sbt/0.11.3/sbt-0.11.3.pom :::::::::::::::::::::::::::::::::::::::::::::: :: UNRESOLVED DEPENDENCIES :: :::::::::::::::::::::::::::::::::::::::::::::: :: org.scala-sbt#sbt;0.11.3: not found :::::::::::::::::::::::::::::::::::::::::::::: :: USE VERBOSE OR DEBUG MESSAGE LEVEL FOR MORE DETAILS unresolved dependency: org.scala-sbt#sbt;0.11.3: not found Error during sbt execution: Error retrieving required libraries (see /Users/Shared/play-2.1-RC3/framework/sbt/boot/update.log for complete log) Error: Could not retrieve sbt 0.11.3
hatena blogのデザイン変更メモ(横幅を広げる)
hatena blogデフォルトだと横幅が狭く、ソースコード等が見難いのでカスタマイズしたのでメモ。
/* <system section="theme" selected="report"> */ @import "/css/theme/report/report.css"; /* </system> */ /* <system section="background" selected="fff"> */ body{background:#fff;} /* </system> */ pre.code{ font-size: 70%; background:#EEEEEE; max-height: 500px; white-space: pre; overflow: auto; } #container { width: 1200px; } #content { padding-left: 70px; } #main { width: 810px; }
Social.framework & Account.framework
社内のスマホエンジニア勉強会で発表したのでブログにしときます。
サンプルアプリはGitHubに公開しているのでどうぞ。
https://github.com/i2key/SocialframeworkExample
最初のほうは当たり前なツイートシートとかの内容なので、試してて引っかかったところのみ掲載します。
Facebookに対してAPIコールを行う(SLRequestを使う)場合、パーミッションをFacebookの指定する順序で取得しないといけません。
基本的には以下の順にパーミッションをとる必要があります。
Step 1: Request basic profile information
ex) email, user_birthday , user_location
Step 2: Request read permissions
ex) user_about_me, read_stream
Step 3: Request publish permissions
ex) publish_actions, publish_stream
参考:https://developers.facebook.com/docs/howtos/ios-6/
そのため、最初からemail,publish_action のような組み合わせではパーミッション取得時に以下のようなエラーがでます。
The Facebook server could not fulfill this access request: The app must ask for a basic read permission at install time.
例えば、FacebookのAPIをSLRequestで叩いてウォールポストを行いたい場合。
準備として・・・
FacebookのAPP登録画面(https://developers.facebook.com/apps)にてAppIDを発行します。
・「アプリをFacebookに統合する方法を選択」にて「ネイティブiOSアプリ」のバンドルIDを入力します。
・「詳細設定」の「認証」でAppTypeを「Native/Desktop」。AppSecretinClientを「いいえ」
初めにemailのパーミッションを取得します。(上記step1)
ACAccountType *facebookType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook]; NSDictionary *options = @{ ACFacebookAppIdKey : @"AppID", ACFacebookPermissionsKey : @[@"email"], ACFacebookAudienceKey : ACFacebookAudienceOnlyMe}; [self.accountStore requestAccessToAccountsWithType:facebookType options:options completion:^(BOOL granted, NSError *error) { if(granted){ //ActionSheet表示する処理 } }]
次にpublish_actionsのパーミッションを取得して(上記step3)、ポストします。
ACAccountStore *accountStore = [[ACAccountStore alloc] init]; ACAccountType *facebookType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook]; NSDictionary *options = @{ACFacebookAppIdKey : @"AppID", ACFacebookPermissionsKey : @[@"publish_actions"], ACFacebookAudienceKey : ACFacebookAudienceOnlyMe}; [accountStore requestAccessToAccountsWithType:facebookType options:options completion:^(BOOL granted, NSError *error) { if(granted){ NSString *urlStr = [NSString stringWithFormat:@"https://graph.facebook.com/me/feed"]; NSURL *url = [NSURL URLWithString:urlStr]; NSDictionary *params = @{@"message" : @"hogehoge"}; SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeFacebook requestMethod:SLRequestMethodPOST URL:url parameters:params]; [request setAccount:self.selectedAccount]; [request performRequestWithHandler:^(NSData *response, NSHTTPURLResponse *urlResponse, NSError *error){ NSLog(@"response:%@",[[NSString alloc]initWithData:response encoding:NSUTF8StringEncoding]); }]; } }];
これでエラーが出ずにFacebookAPIを利用したポストが出来ます。
また、上記のようにやってもどうしてもエラーが出る場合は、iPhone側の設定にて、アプリとFacebookの連携の許可を一旦解除して、再度アプリからaccountStoreにアクセスすると直る場合があります。
Amazon Linux にJenkins入れたときのメモ
yumにJenkinsリポジトリ登録してインストール
$ sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo $ sudo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key $ sudo yum install jenkins
cf : https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+RedHat+distributions
HOST名/jenkinsで開けるように
/etc/sysconfig/jenkins内で
JENKINS_ARGS="--prefix=/jenkins";
<Location /jenkins> Order allow,deny Allow from all ProxyPass http://localhost:8080/jenkins ProxyPassReverse http://localhost:8080/jenkins </Location>
設定後 httpdリスタート
Jenkinsのユーザ設定(デフォルトだとJenkinsになるが、任意に変えたい場合)
/etc/sysconfig/jenkins
JENKINS_USER="hogeUser" //例えばhogeUserでJenkins動かす場合
パーミッションの変更
$ chown -R hogeUser /var/lib/jenkins $ chown -R hogeUser /var/log/jenkins $ chown -R hogeUser /var/cache/jenkins
Jenkins strat/stop
$ sudo service jenkins start/stop/restart