Twitter API v2.0について – ユーザ認証編 –
Monacaチームの小田川です。
前回、アプリ認証でのTwitter API v2.0の利用方法を紹介しまし たが、今回は、ユーザ認証での利用方法を紹介したいと思います。
ユーザ認証について調べてみると、PHP等からユーザ認証でアクセスする方法は見つかるのですが、Android(Kotlin)から利用する方法がなかったので、今回は、Android(Kotlin)からユーザ認証でTwitter API v2.0を利用する方法を紹介してみたいと思います。
今回使用する、
- APIキー
- APIシークレット
- アクセストークン
- アクセストークン シークレット
は、あらかじめ取得できているものとします。
Twitter API v2.0の利用(ユーザ認証)
アプリ認証の場合は、Bearer Tokenを取得することで、簡単に利用することが出来ましたが、ユーザ認証の場合は、OAuth 1.0を利用するため、署名に必要なデータを作成する必要があります。
今回は、前回のアプリ認証で使用したTwitter API v2.0のtweetsを例にしています。全体のソースコードを掲載しています。コード量が多いので、コードを確認しながら読み進めてください。
HTMLの解析には、Jsoupを使用しています。
署名用データ
tweetsオプション
リクエストにtweetsオプションを設定する場合は、このtweetsオプション情報も署名用データに  含める必要があります。ソースコードのtweetsオプションに設定されているtweetsオプション情報を使用して署名用データを作成する場合は、Mapでコレクションを作成し、
- キー: オプション名
- 値: オプションの値
という形式でデータを作成します。
OAuthパラメータ
OAuthパラメータのMapには、以下の情報を設定します。
- oauth_token: アクセストークン
- oauth_consumer_key: APIキー
- oauth_signature_method: 署名メソッド
- oauth_timestamp: UNIXタイムスタンプ
- oauth_nonce: ユニークな値
- oauth_version: OAuthバージョン
署名用データの作成
tweetsオプションとOAuthパラメータを署名用データとして一つのMapにまとめます。署名用データを作成する場合は、以下の設定が必要になります。
- キーでデータをソートする。
- キーと値をエンコードする。
- エンコードしたキーと値を=でつなぐ。
- キーと値が複数ある場合は&でつなぐ。
次に、エンコードされた+と~は、署名で使用できるように文字列を変換する必要があります。変換内容は、replaceParams()を参照していください。
署名ベース文字列の作成
署名ベース文字列を作成する場合は、以下の情報が必要になります。
- リクエストメソッド名
- リクエストURL
- 署名用データ
エンコードしたリクエストメソッド名、リクエストURL、署名用データを&でつなげることで、署名ベース文字列を作成することが出来ます。
署名の作成
作成する署名はHMAC-SHA1で作成する必要があります。処理の流れは、以下になります。
- 秘密鍵の作成。
- 秘密鍵から署名を作成。
- 作成した署名を取得。
- 取得した署名をbase64エンコード。
署名の作成については、ソースコードのcreateSignature()を参照してください。
リクエストヘッダー用データ
リクエストヘッダー用のデータも作成する必要があります。リクエストヘッダー用のデータを作成する場合は、tweetsオプションとOAuthパラメータを一つにまとめたMapで、以下の設定が必要になります。
- キーでデータをソートする
- キーと値をエンコードする
- エンコードしたキーと値を=でつなぐ
- キーと値が複数ある場合は,でつなぐ
次に、エンコードされた+と~は、署名用データと同じく文字列を変換する必要があります。変換内容は、replaceParams()を参照していください。
リクエストヘッダーに設定するOAuthデータの作成
リクエストヘッダーに設定するOAuthデータは、リクエストヘッダー用データの後ろに署名を追加する形で作成します。以下は、ソースコードの抜粋です。
"OAuth $headerParams,oauth_signature=$signature"リクエストを送信する
リクエストの送信は、HttpURLConnectionで行なっています。署名の設定とリクエストヘッダーの設定が正しく行われていれば、ユーザ認証でツイートを取得することができます。
今回のソースコードを使用してアプリ認証を行いたい場合は、httpURLConnection.setRequestProperty()で、Bearer Tokenの設定を行うことで対応できます。
ソースコード
fun getTweets() {
    val requestUrl = "https://api.twitter.com/2/tweets/1312028311011438592" // エンドポイント
    val requestMethod = "GET" // リクエストメソッド
    val apiKey = "APIキー" // APIキー
    val apiSecret = "APIシークレット" // APIシークレット
    val accessToken = "アクセストークン" // アクセストークン
    val accessTokenSecret = "アクセストークンシークレット" // アクセストークンシークレット
    // tweetsオプション
    val expansions = "expansions=attachments.poll_ids,attachments.media_keys,author_id,entities.mentions.username,geo.place_id,in_reply_to_user_id,referenced_tweets.id,referenced_tweets.id.author_id"
    val mediaFields = "media.fields=duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics"
    val placeFields = "place.fields=contained_within,country,country_code,full_name,geo,id,name,place_type"
    val pollFields = "poll.fields=duration_minutes,end_datetime,id,options,voting_status"
    val tweetFields = "tweet.fields=attachments,author_id,context_annotations,conversation_id,created_at,entities,geo,id,in_reply_to_user_id,lang,public_metrics,possibly_sensitive,referenced_tweets,source,text,withheld"
    val userFields = "user.fields=created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,withheld"
    val param: MutableMap<String, String> = mutableMapOf() // 各種データ作成用コレクション
    // tweetsオプション用データの作成
    val expansionsSplit = expansions.split("=")
    val mediaFieldsSplit = mediaFields.split("=")
    val placeFieldsSplit = placeFields.split("=")
    val pollFieldsSplit = pollFields.split("=")
    val tweetFieldsSplit = tweetFields.split("=")
    val userFieldsSplit = userFields.split("=")
    val param1: Map<String, String> = mapOf(
            expansionsSplit[0] to expansionsSplit[1],
            mediaFieldsSplit[0] to mediaFieldsSplit[1],
            placeFieldsSplit[0] to placeFieldsSplit[1],
            pollFieldsSplit[0] to pollFieldsSplit[1],
            tweetFieldsSplit[0] to tweetFieldsSplit[1],
            userFieldsSplit[0] to userFieldsSplit[1],
    )
    // OAuthパラメータの作成
    val timestamp = System.currentTimeMillis() / 1000 // UNIXタイムスタンプ
    val randomInt = Random.nextInt() // oauth_nonce用乱数
    val param2: Map<String, String> = mapOf(
            "oauth_token" to accessToken,
            "oauth_consumer_key" to apiKey,
            "oauth_signature_method" to "HMAC-SHA1",
            "oauth_timestamp" to "$timestamp",
            "oauth_nonce" to "$randomInt+$timestamp",
            "oauth_version" to "1.0"
    )
    // tweetsオプション用データと署名用データをマージ
    param.putAll(param1)
    param.putAll(param2)
    // 署名データ用パラメータ情報の作成(キーでソートする必要があります)
    var requestParams = ""
    for (item in param.toSortedMap()) {
        if (requestParams.isEmpty()) {
            requestParams = URLEncoder.encode(item.key, "UTF-8") + "=" + URLEncoder.encode(item.value, "UTF-8")
        } else {
            requestParams += "&" + URLEncoder.encode(item.key, "UTF-8") + "=" + URLEncoder.encode(item.value, "UTF-8")
        }
    }
    requestParams = replaceParams(requestParams) // 署名データ用に文字列を変換
    // 署名の作成
    val encodedRequestMethod = URLEncoder.encode(requestMethod, "UTF-8")
    val encodedRequestURL= URLEncoder.encode(requestUrl, "UTF-8")
    val encodedRequestParams = URLEncoder.encode(requestParams, "UTF-8")
    val signatureBase = "$encodedRequestMethod&$encodedRequestURL&$encodedRequestParams" // 署名ベース文字列
    val signatureKey = URLEncoder.encode(apiSecret, "UTF-8") + "&" + URLEncoder.encode(accessTokenSecret, "UTF-8")
    val signature = createSignature(signatureBase, signatureKey) // 署名
    // リクエストヘッダー用のデータ作成(キーでソートする必要があります)
    var headerParams = ""
    for (item in param.toSortedMap()) {
        if (headerParams.isEmpty()) {
            headerParams = URLEncoder.encode(item.key, "UTF-8") + "=" + URLEncoder.encode(item.value, "UTF-8")
        } else {
            headerParams += "," + URLEncoder.encode(item.key, "UTF-8") + "=" + URLEncoder.encode(item.value, "UTF-8")
        }
    }
    headerParams = replaceParams(headerParams) // リクエストヘッダー用のデータ文字列を変換。
    // HttpURLConnectionのheaderに設定するOAuthデータを作成
    val oAuth = "OAuth $headerParams,oauth_signature=$signature"
    // tweetsを取得
    val url = URL("$requestUrl?$expansions&$mediaFields&$placeFields&$pollFields&$tweetFields&$userFields")
    // val bearerToken = "Bearer {Bearer Token}"
    val httpURLConnection: HttpURLConnection
    httpURLConnection = url.openConnection() as HttpURLConnection
    httpURLConnection.requestMethod = requestMethod
    // httpURLConnection.setRequestProperty("Authorization", bearerToken)
    httpURLConnection.setRequestProperty("Authorization", oAuth)
    httpURLConnection.connect()
    if (httpURLConnection.responseCode == HttpURLConnection.HTTP_OK) {
        val readStream = httpURLConnection.inputStream.use { readInputStream(it) }
        val document = Jsoup.parse(readStream)
        val body = JSONObject(document.body().text())
        if (BuildConfig.DEBUG) {
            Log.d("tweets id: ", JSONObject(body.getString("data")).getLong("id").toString())
            Log.d("tweets text: ", JSONObject(body.getString("data")).getString("text"))
        }
    }
    httpURLConnection.disconnect()
}
fun createSignature(signatureBase: String, signatureKey: String): String {
    // 署名アルゴリズム
    val algorithm = "HmacSHA1"
    // 秘密鍵の作成
    val secretKey: SecretKey = SecretKeySpec(signatureKey.toByteArray(), algorithm)
    // 秘密鍵から署名を作成
    val mac = Mac.getInstance(algorithm)
    mac.init(secretKey)
    mac.update(signatureBase.toByteArray())
    // 署名を取得
    val signature = mac.doFinal()
    // 署名をbase64エンコード
    return URLEncoder.encode(Base64.encodeToString(signature, Base64.NO_WRAP), "UTF-8")
}
fun replaceParams(params: String): String {
    return params.replace("%2B", "+").replace("+", "%20").replace("%7E", "~")
}
fun readInputStream(inputStream: InputStream): String {
    val stringBuilder = StringBuilder()
    BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8)).use {
        var line : String?
        do {
            line = it.readLine()
            line ?: break
            stringBuilder.append(line)
        } while (true)
    }
    return stringBuilder.toString()
}おわりに
ユーザ認証を行う場合は、認証用データを作成する必要があるため敷居が高いですが、作成方法が分かれば他にも応用が効くと思います。ユーザ認証に興味があれば、一度、トライしてみてください。







