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()
}
おわりに
ユーザ認証を行う場合は、認証用データを作成する必要があるため敷居が高いですが、作成方法が分かれば他にも応用が効くと思います。ユーザ認証に興味があれば、一度、トライしてみてください。