umegusa's blog

備忘録

SwiftでTwitter OAuthを取得する oauth_token編

Swiftやらプログラミングのモチベが結構高まってきてます。
以前から苦戦していたTwitter OAuth認証Swiftでやりたいと思ってずっと試行錯誤してました。
今日やっとoatuh_tokenを取得できたので備忘録として残します。

Twitterアプリケーションの作成

https://dev.twitter.com/
ここのTOOLS -> Manage Your Appsへリンク辿ってクライアントを作成します。
必要なものはTwitterアカウントで、今から作成する場合は電話番号を登録しないと作成できないみたいです。

あとはCreateNewAppのボタンから必要な情報を入力して
クライアントを作成します。

callbackのアドレスを入力する欄があるのですが、
この欄に何かしら値を入れておかないと認証できないとか。
ですので今回はとりあえず自分のtwitterページを登録しておく・・・
カスタムURLスキーマを設定しておけばそこを無視してくれるので特に問題ないはず。

OAuth認証

OAuth認証がいまいち理解できなかったのでメモ。
ちゃんと後から勉強して正しい情報載せたいと思うけど、
とりまTwitterの認証フローを参考にまとめてみる。

クライアントを登録するとConsumer KeyとConsumer Secretの2つが発行される。
これを使ってoauth_tokenを取得し、access_tokenを取得してAPIにアクセスする。

request発行 -> oauth_token取得 -> access_token取得 -> apiアクセス
こんな流れ。
詳しいことはちゃんともろもろ取得してからその際に・・・

oauth_token取得

access_tokenを取得するためのoauth_token, oauth_token_secretを取得するプログラムを書いた。
画面側はとりあえずButtonと書かれたボタンをクリックするとトークンを取得するようにしてる。
最初のテンプレートはSingle View Application。

ViewController.swift

import UIKit
import Foundation

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
    }

    @IBAction func clickButton(sender: AnyObject) {
        doTwitter()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // Twitter OAuth認証
    func doTwitter(){
        let oauth = OAuthSwift(
            consumerKey:    Twitter["consumerKey"]!,
            consumerSecret: Twitter["consumerSecret"]!,
            requestTokenUrl: "https://api.twitter.com/oauth/request_token",
            authorizeUrl:    "https://api.twitter.com/oauth/authorize",
            accessTokenUrl:  "https://api.twitter.com/oauth/access_token"
        )
        
        // 通信開始
        oauth.start()
    }
}

OAuthSwift.swfit

import Foundation

class OAuthSwift{
    // oauthリクエスト参考
    // http://developer.yahoo.co.jp/other/oauth/signinrequest.html
    // http://www.pressmantech.com/tech/programming/1137
    
    
    var dataEncoding: NSStringEncoding = NSUTF8StringEncoding
    
    var data : NSMutableData? = nil
    
    var consumer_key: String
    var consumer_secret: String
    var request_token_url: String
    var authorize_url: String
    var access_token_url: String
    
    // コンストラクタ
    init(consumerKey: String, consumerSecret: String, requestTokenUrl: String, authorizeUrl: String, accessTokenUrl: String){
        self.consumer_key = consumerKey
        self.consumer_secret = consumerSecret
        self.request_token_url = requestTokenUrl
        self.authorize_url = authorizeUrl
        self.access_token_url = accessTokenUrl
    }
    
    func start() -> Void{
        // クライアントアプリケーションの作成
        // 流れとして request_token投げる -> ouath_token取得(今ここ) -> authrize_request投げる -> access_token取得 -> API使用

        // リクエストURL設定
        let twitterURL : NSURL = NSURL(string: "https://api.twitter.com/oauth/request_token")!

        // request
        var request : NSMutableURLRequest = NSMutableURLRequest(URL: twitterURL);
        request.HTTPMethod = "POST"
        request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData

        // oauth_request設定
        var param = Dictionary<String, String>()
        // バージョン
        param["oauth_version"] = "1.0"
        // 証明書アルゴリズム(Twitterでは固定)
        param["oauth_signature_method"] = "HMAC-SHA1"
        // ConsumerKey
        param["oauth_consumer_key"] = self.consumer_key
        // Unixタイムスタンプ
        param["oauth_timestamp"] = String(Int64(NSDate().timeIntervalSince1970))
        // ランダムな文字列
        param["oauth_nonce"] = (NSUUID().UUIDString as NSString).substringToIndex(8)
        // コールバック
        param["oauth_callback"] = "oauth-swift://"
        // 証明書
        param["oauth_signature"] = self.oauthSignatureForMethod("POST", url: twitterURL, 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])\"")
            }
        }
        
        
        // 最終的にリクエストするのは下記文字列
        // "OAuth oauth_callback=\"swift-oauth%3A%2F%2Fswift-oauth%2F\", oauth_consumer_key=\"CXubzXLR2vzqbCf1d9maSJ4ob\", oauth_nonce=\"46B623F4\", oauth_signature=\"%2Brx8F1ofGHhQe0iN%2Ff7MArz05F4%3D\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"1422107013\", oauth_version=\"1.0\""
        
        // リクエスト設定
        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)!)
        }
    }
    
    // サーバからレスポンスを受け取ったときのデリゲート
    func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
        // Recieved a new request, clear out the data object
        self.data! = NSMutableData()
    }
    
    // サーバからデータが送られてきたときのデリゲート
    func connection(connection: NSURLConnection!, didReceiveData data: NSData!){
        self.data!.appendData(data)
    }
    
    // データロードが完了したときのデリゲート
    func connectionDidFinishLoading(connection: NSURLConnection!){
        // バイナリデータが発行される
        let html : String = NSString(data: self.data!, encoding: NSUTF8StringEncoding)!
        // コンソールに出力
        println(html)
        
    }
    
    // signature作成
    func oauthSignatureForMethod(method: String, url: NSURL, parameters: Dictionary<String, String>) -> String {
        let signingKey : String = "\(self.consumer_secret)&"
        let 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)
    }

    // Dictionary内のデータをエンコード
    func urlEncodedQueryStringWithEncoding(params:Dictionary<String, String>) -> String {
        var parts = [String]()
        
        for (key, value) in params {
            let keyString = urlEncodedStringWithEncoding(key)
            let valueString = urlEncodedStringWithEncoding(value)
            let query = "\(keyString)=\(valueString)" as String
            parts.append(query)
        }
        
        return "&".join(parts) as String
    }

    // URLエンコード
    func urlEncodedStringWithEncoding(str: String) -> String {
        let charactersToBeEscaped = ":/?&=;+!@#$()',*" as CFStringRef
        let charactersToLeaveUnescaped = "[]." as CFStringRef
        
        var raw: NSString = str
        
        let result = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, raw, charactersToLeaveUnescaped, charactersToBeEscaped, CFStringConvertNSStringEncodingToEncoding(dataEncoding)) as NSString
        
        return result
    }
    
    // SHA1署名のハッシュ値を作成
    func SHA1DigestWithKey(base: String, key: String) -> NSData {
        let str = base.cStringUsingEncoding(dataEncoding)
        let strLen = UInt(base.lengthOfBytesUsingEncoding(dataEncoding))
        let digestLen = Int(CC_SHA1_DIGEST_LENGTH)
        let result = UnsafeMutablePointer<CUnsignedChar>.alloc(digestLen)
        let keyStr = key.cStringUsingEncoding(NSUTF8StringEncoding)
        let keyLen = UInt(key.lengthOfBytesUsingEncoding(NSUTF8StringEncoding))
        
        CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA1), keyStr!, keyLen, str!, strLen, result)
        
        return NSData(bytes: result, length: digestLen)
    }
}

はい、https://api.twitter.com/oauth/request_token にPOSTでリクエスト送るだけ。
ちょっと制限があって、query stringのパラメータ名が昇順じゃないといけないとかハッシュ値作成しなきゃいけないとか。
その辺はプログラムソース見てもらえれば、全部できたら改めて書き直すと思う。

これから

ライブラリ使えばこの辺一発でできるんだけどそれだと理解できないから作っただけ、自己満足。
とりあえずツイート取得、投稿、その他昨日を提供できるようにすることとOAuthTwitterを綺麗にしたりHTTPClient作ったりすることを進めていく。
あとは忘れないようにちゃんとこの辺の知見のまとめをこのブログに残しておく。

最低限これをやっていこうと思う。
近いうちにまずはツイート取得を目指していこう!