umegusa's blog

備忘録

SwiftでTwitter OAuthを取得する access_token編

ついにTwitterAPIの認証部分をクリアしてタイムラインを取得するところまでできましたのでメモ。
ここまでできればあとは整えてクリアだ!!とか思ってたらまた壁にぶつかったので今日はここまでにしてこれまでの内容をまとめます。

oauth_tokenを使用してaccess_tokenを取得する

前回の記事ではrequest_tokenにパラメータを投げてトークンを取得するところまでやりました。

SwiftでTwitter OAuthを取得する oauth_token編 - umegusa's blog

今回はここの続きです。

request_tokenを取得して認証とaccess_tokenを取得するところまでやります。

まず、トークンを取得します。
ここは前回のコードを参照してください。

 // 非同期通信開始
        NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()){
            response, data, error in
            
            if(error != nil){
                // エラー文言表示
                println(error.description)
            }
            // oauth_token表示
             print(NSString(data: data, encoding: self.dataEncoding)!)
            
            // Authorize
            self.authorizeClient(NSString(data: data, encoding: self.dataEncoding)!)

        }

トークンを取得したらAuthorizeClientメソッドに飛びます。
Authorizeは下記のように実装します。

    // Authorize
    func authorizeClient(param: String)
    {
        var divideParam = param.componentsSeparatedByString("&")
        var oauthToken = divideParam[0].componentsSeparatedByString("=")[1]
        

        let queryURL = NSURL(string: self.authorize_url + "?oauth_token=\(oauthToken)")

        // safariで認証
        UIApplication.sharedApplication().openURL(queryURL!)
    }

本当はWebViewを使って認証したかったのですが、今回はsafariを使用してそのまま認証させます。
Authorizeはrequest_tokenで取得したパラメータをgetメソッドで投げるだけです。
そうすると認証画面に飛ぶことができます。
ただし、ここでの注意点は下記2点です。

  • アプリケーション作成時に何でもいいからコールバックURLを入力する
    • これを入れないとoauth_callbackパラメータが無効になってしまう
  • oauth_callbackパラメータはプロトコル部分だけだとうまくいかない
    • 前準備としてカスタムURLスキーマを設定
    • ex) oatuh-swift://で設定している場合
      • oauth-swift:// ×
      • oauth-swift:/// ○
      • 尚、URLは自由に設定できる

うまくいくとsafariが立ち上がって下記認証画面へ進み、アカウントで認証が成功したらアプリに戻ってきます。
よく見る画面ですね。
f:id:umegusa:20150216005137p:plain

コールバックされるとoauth_token, oauth_verifierのパラメータが付与されたURLが返ってきます。

oauth-swift://hogehoge/?oauth_token=sakldsjalkdjdkljfasldf&oauth_verifier=klajlfhjagdkasgf

※ 上記パラメータはダミーです

カスタムURLでアクセスされた情報を取得する

AppDelegate.swiftに下記のようにapplicationメソッドを用意することで取得することが可能です
AppDelegate.swift

    func application(application: UIApplication!, openURL url: NSURL!, sourceApplication: String!, annotation: AnyObject!) -> Bool {
        ViewController.handleOpenURL(url)
        return true
    }

今回はViewControllerにhandleOpenURLという静的メソッドを実装してそちらに飛ぶように実装しています。
(内容がひどいのでNSNotificationCenterをうまく使って実装したい。。。)
尚、静的メソッドは下記のように実装できます。

    class func handleOpenURL(url: NSURL) {
        println("OK!!")
    }

class func メソッド名 で静的メソッドの実装ができます。
ここでoauth_token, oauth_verifierを使用してaccess_tokenを取得しにいきます。

access_tokenを取得する

ここまで来たらもう少しです!
access_tokenの取得方法は基本的にリクエストトークンの取得と同じです。
ただし、証明書の発行や送信に使用するパラメータに差異があります。

    // get access token
    func postAccessTokenWithRequestToken(url: NSURL){
        // 基本的にリクエストトークンと同じだが、リクエストに差異がある
        
        // ここで設定するのは下記パラメータ
        // - oauth_token
        // - oauth_verifier
        var param = Dictionary<String, String>()
        var query : String = url.query!
        var divideParam = query.componentsSeparatedByString("&")
        for paramStr in divideParam {
            var splitParam = paramStr.componentsSeparatedByString("=")
            param[splitParam[0]] = splitParam[1]
        }
        
        // oauth_param
        param["oauth_version"] = "1.0"
        param["oauth_signature_method"] = "HMAC-SHA1"
        param["oauth_consumer_key"] = self.consumer_key
        param["oauth_timestamp"] = String(Int64(NSDate().timeIntervalSince1970))
        param["oauth_nonce"] = (NSUUID().UUIDString as NSString).substringToIndex(8)
        param["oauth_signature"] = self.oauthSignatureForMethod("POST", url: NSURL(string:self.access_token_url)!, parameters: param)
        
        // アルファベット順に並べ替える
        var authorizationParameterComponents = urlEncodedQueryStringWithEncoding(param).componentsSeparatedByString("&") as [String]
        authorizationParameterComponents.sort { $0 < $1 }
        
        // リクエスト文字列作成
        var headerComponents = [String]()
        for component in authorizationParameterComponents {
            let subcomponent = component.componentsSeparatedByString("=") as [String]
            if subcomponent.count == 2 {
                headerComponents.append("\(subcomponent[0])=\"\(subcomponent[1])\"")
            }
        }
        
        // request
        var request : NSMutableURLRequest = NSMutableURLRequest(URL: NSURL(string: self.access_token_url)!);
        request.HTTPMethod = "POST"
        request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData
        
        // カスタムヘッダ設定
        request.setValue("OAuth " + ", ".join(headerComponents), forHTTPHeaderField: "Authorization")
        
        // 非同期通信開始
        NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()){
            response, data, error in
            
            if(error != nil){
                // エラー文言表示
                println(error.description)
            }
            // oauth_token表示
            print(NSString(data: data, encoding: self.dataEncoding)!)

        }

    }

上記により、access_token(パラメータ上はoauth_token, oauth_token_secret)を取得できます。
リクエストトークンの取得と差異があったのは先ほど取得したパラメータを使用するというところのみになります。

ここで取得したoauth_token, oauth_token_secretを使用して、TwitterAPIにアクセスします。

home_timelineを取得する

Twitter OAuth v1.1になって毎回カスタムヘッダを作成しなければ情報を取得できなくなってしまったみたいですね。
ということでタイムラインを取得するAPIを使用して取得してみましょう。

    // タイムライン取得
    func getUserTimeline(tokenParam: String) -> Void{
        var param = Dictionary<String, String>()
        var divideParam = tokenParam.componentsSeparatedByString("&")
        for paramStr in divideParam {
            var splitParam = paramStr.componentsSeparatedByString("=")
            if(splitParam[0] == "oauth_token"){
                self.access_token = splitParam[1]
            }else if(splitParam[0] == "oauth_token_secret"){
                self.access_token_secret = splitParam[1]
            }
        }
        
        // 取得に必要なパラメータ
        
        // oauth_tokenが新規に追加される
        param["oauth_token"] = self.access_token
        param["oauth_version"] = "1.0"
        param["oauth_signature_method"] = "HMAC-SHA1"
        param["oauth_consumer_key"] = self.consumer_key
        param["oauth_timestamp"] = String(Int64(NSDate().timeIntervalSince1970))
        param["oauth_nonce"] = (NSUUID().UUIDString as NSString).substringToIndex(8)
        param["oauth_signature"] = self.oauthSignatureForMethod("GET", url: NSURL(string: "https://api.twitter.com/1.1/statuses/home_timeline.json")!, parameters: param)
        
        
        // http header用パラメータ作成
        // アルファベット順に並べ替える
        var authorizationParameterComponents = urlEncodedQueryStringWithEncoding(param).componentsSeparatedByString("&") as [String]
        authorizationParameterComponents.sort { $0 < $1 }
        
        // リクエスト文字列作成
        var headerComponents = [String]()
        for component in authorizationParameterComponents {
            let subcomponent = component.componentsSeparatedByString("=") as [String]
            if subcomponent.count == 2 {
                headerComponents.append("\(subcomponent[0])=\"\(subcomponent[1])\"")
            }
        }
        
        // request
        var request : NSMutableURLRequest = NSMutableURLRequest(URL: NSURL(string: "https://api.twitter.com/1.1/statuses/home_timeline.json")!);
        request.HTTPMethod = "GET"
        request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData
        
        // リクエスト設定
        request.setValue("OAuth " + ", ".join(headerComponents), forHTTPHeaderField: "Authorization")
        
        // 非同期通信開始
        NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()){
            response, data, error in
            
            if(error != nil){
                // エラー文言表示
                println(error.description)
            }
            // oauth_token表示
            print(NSString(data: data, encoding: self.dataEncoding)!)
            
        }
    }

HMAC-SHA1でパラメータとキーを渡してSHA1ハッシュ値にしてbase64エンコードするところは同じなのですが、ユーザ情報を取得する場合、下記のような変更があります。

// signature作成
    func oauthSignatureForMethod(method: String, url: NSURL, parameters: Dictionary<String, String>) -> String {

        // URLとパラメータをHMAC-SHA1ハッシュ値に変換してbase64エンコードする
        var signingKey : String = "\(self.consumer_secret)&"
        var signingKeyData = signingKey.dataUsingEncoding(dataEncoding)
        
            // access tokenを取得したらkeyにaccess token secretを付加する必要がある
            signingKey = "\(self.consumer_secret)&\(self.access_token_secret)"
            signingKeyData = signingKey.dataUsingEncoding(dataEncoding)
        
        // パラメータ取得してソート
        var parameterComponents = urlEncodedQueryStringWithEncoding(parameters).componentsSeparatedByString("&") as [String]
        parameterComponents.sort { $0 < $1 }
        
        // query string作成
        let parameterString = "&".join(parameterComponents)
        
        // urlエンコード
        let encodedParameterString = urlEncodedStringWithEncoding(parameterString)
        
        let encodedURL = urlEncodedStringWithEncoding(url.absoluteString!)
        
        // signature用ベース文字列作成
        let signatureBaseString = "\(method)&\(encodedURL)&\(encodedParameterString)"
        let signatureBaseStringData = signatureBaseString.dataUsingEncoding(dataEncoding)
        
        // signature作成
        return SHA1DigestWithKey(signatureBaseString, key: signingKey).base64EncodedStringWithOptions(nil)
    }

consumer_key_secretだけでなく取得したoaut_token_secretも連結してキーに渡して証明書を作成する必要があります。
ここで引っかかって全然取得できなかったのは良い思い出です。。。

成功すると下記のようなjsonが返ってきます。

[
    {
        "contributors": null, 
        "coordinates": null, 
        "created_at": "Sun Feb 15 09:09:26 +0000 2015", 
        "entities": {
            "hashtags": [], 
            "media": [
                {
                    "display_url": "pic.twitter.com/Jsu0oYxcGK", 
                    "expanded_url": "http://twitter.com/NIKEiD_JP/status/566884628070682624/photo/1", 
                    "id": 566884626841747456, 
                    "id_str": "566884626841747456", 
                    "indices": [
                        139, 
                        140
                    ], 
......
                }
            ], 
// 長いので省略 
            "favourites_count": 0, 
            "follow_request_sent": false, 
            "followers_count": 169270, 
            "following": true, 
            "friends_count": 15, 
            "geo_enabled": false, 
            "id": 1946049950, 
            "id_str": "1946049950", 
            "is_translation_enabled": false, 
            "is_translator": false, 
            "lang": "ja", 
            "listed_count": 483, 
            "location": "NIKE", 
            "name": "NikeStoreJapan", 
            "notifications": false, 
            "profile_background_color": "131516", 
            "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/378800000106403467/c19a3d26691f9eb4bb431ad3f87676ad.jpeg", 
            "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/378800000106403467/c19a3d26691f9eb4bb431ad3f87676ad.jpeg", 
            "profile_background_tile": true, 
            "profile_banner_url": "https://pbs.twimg.com/profile_banners/1946049950/1418890257", 
            "profile_image_url": "http://pbs.twimg.com/profile_images/472164680534732800/brtYH1Lj_normal.jpeg", 
            "profile_image_url_https": "https://pbs.twimg.com/profile_images/472164680534732800/brtYH1Lj_normal.jpeg", 
            "profile_link_color": "009999", 
            "profile_location": null, 
            "profile_sidebar_border_color": "FFFFFF", 
            "profile_sidebar_fill_color": "EFEFEF", 
            "profile_text_color": "333333", 
            "profile_use_background_image": false, 
            "protected": false, 
            "screen_name": "NikeStoreJapan", 
            "statuses_count": 1393, 
            "time_zone": "Tokyo", 
            "url": "http://t.co/nKcfAyJlq0", 
            "utc_offset": 32400, 
            "verified": true
        }
    }
]

成功しましたね!
あとはjsonをパースして表示するだけです。

終わりに

一応home_timelineは取得できましたが、また次の壁に出会いました。
カスタムヘッダではなくAPIのパラメータを付加して何かする(つぶやくなど)ことがまだ実現できていません。
そのほか色々やることがまだまだある予感です。

  • safariじゃなくて WebViewで認証画面出してもろもろ操作したい
  • つぶやくなどパラメータを付加してデータ取得したい
  • そもそもタイムライン並べてない(jsonパーサ作ろう)
  • ソースコードももっと汎用性を持たせてライブラリ化しておきたい

などなど・・・
先は長そうですね・・・・

でも認証周りが成功したということはほぼ終わったも同然だと考えていますので、
さくさく進められたらいいなと思う今日この頃です!