使用 Ktor 调用 Twitter OAuth 1.0A
Posted on
我最近在做一个新项目,牵涉到许多提供通过社交媒体登录的提供方。这意味着我需要学习什么是 Oauth 以及如何创建 API 来与 Oauth 提供商进行通信,如Facebook、Google、Github和…推特。对于前三个提供方,工作一切顺利,但Twitter不是。我花了大约五个小时阅读Twitter的文档,浏览博客文章和库,编写代码。我是这么操作的。
步骤1 - 配置 OAuth URL 和参数
我用的是Ktor的OAuth包,你需要更改urlProvider
,consumerKey
和consumerSecret
.其他字段应该保持不变。
oauth("auth-oauth-twitter") {
urlProvider = { "${config.appConfig.backEndBaseUrl}/auth/callback/twitter" }
providerLookup = {
OAuthServerSettings.OAuth1aServerSettings(
name = "twitter",
requestTokenUrl = "https://api.twitter.com/oauth/request_token",
authorizeUrl = "https://api.twitter.com/oauth/authorize",
accessTokenUrl = "https://api.twitter.com/oauth/access_token",
consumerKey = "CONSUMER_KEY_FROM_TWITTER_DEV_PORTAL",
consumerSecret = "CONSUMER_SECRET_FROM_TWITTER_DEV_PORTAL",
)
}
client = httpClient
}
步骤2 - 创建触发器和回调端点
您需要设置一个处理程序来触发 Oauth 流,Ktor的包能替你完成这一任务。更有趣的部分是回调 URL。Twitter调用回调 URL 时使用token
和atokenSecret
。当您向 Twitter API 发出请求时,您需要用这些令牌签名。对于我来说,我只想要登录用户的个人资料,但您可以选择代表用户发推。
authenticate("auth-oauth-twitter") {
get("/auth/login/twitter") {
// 自动重定向至'authorizeUrl'
}
get("/auth/callback/twitter") {
val response = twitterTransport.getUser(principal.tokenSecret, principal.token)
?: throw BadRequestException(ErrorType.FAILED_TO_FETCH_TWITTER_USER, "Failed to fetch twitter user")
val (email, name) = response
logger.info("{} logged in with Twitter.", response.email)
call.respondRedirect("/somewhere")
}
}
到目前为止,代码相当简单,Twitter 调用到我的回调端点,我使用他们的 tokenSecret 和 token 来获取登录用户的配置文件。不过在这里TwitterTransport.getUser
,发生了什么?
步骤3 - 创建一个 Nonce
fun createNonce(): String {
val charPool : List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
val randomString = ThreadLocalRandom.current()
.ints(32, 0, charPool.size)
.asSequence()
.map { charPool[it] }
.joinToString("")
return randomString
}
它所做的就是创建一个32个字符的随机字母数字字符串。
步骤4 - 以秒为单位获取时间戳
val timestamp = Instant.now().epochSecond
步骤5 - 收集参数
include_email
是参数集的一部分,因为它是我请求的URL(https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true)上的参数。如果您准备访问一个不同的URL,请记住将该URL的查询参数包含到下面的参数集中。
val params = listOf(
"include_email" to "true",
"oauth_consumer_key" to consumerKey,
"oauth_nonce" to nonce,
"oauth_signature_method" to "HMAC-SHA1",
"oauth_timestamp" to timestamp.toString(),
"oauth_token" to oAuthToken,
"oauth_version" to "1.0"
)
步骤6 - 创建paramater字符串
我的代码使用com.google.common.net.PercentEscaper
。创建此参数字符串的算法这里有解释,在 collecting parameters 标题下。
private val escaper = PercentEscaper("_-.", false)
fun createParamString(params: List<Pair<String, String>>): String {
return params.map { (key, value) ->
escaper.escape(key) to escaper.escape(value)
}.sortedBy {
it.first
}.map { (key, value) ->
"$key=$value"
}.joinToString("&")
}
步骤7 - 创建签名基字符串
fun createSignatureBaseString(method: String, url: String, parameterString: String): String {
val buffer = StringBuilder()
buffer.append(method.uppercase())
buffer.append("&")
buffer.append(escaper.escape(url))
buffer.append("&")
buffer.append(escaper.escape(parameterString))
return buffer.toString()
}
步骤8 - 创建签名密钥
fun createSigningKey(consumerSecret: String, oauthTokenSecret: String): String {
val buffer = StringBuilder()
buffer.append(escaper.escape(consumerSecret))
buffer.append("&")
buffer.append(escaper.escape(oauthTokenSecret))
return buffer.toString()
}
步骤9 - 创建签名
我使用 org.apache.commons.codec.digest 中的 HmacUtils 为我执行此操作,然后通过 Base64ed 得到的字节数组
fun hmacSign(signingKey: String, signatureBaseString: String): String {
val hmacSha1 = HmacUtils.hmacSha1(signingKey, signatureBaseString)
return Base64.getEncoder().encodeToString(hmacSha1)
}
步骤10 - 创建授权请求头
这一步骤包括合并所有不同零碎的字符并添加转义,然后用“,”把它们连接在一起。
fun createAuthHeader(consumerKey: String, timestamp: Long, nonce: String, signature: String, token: String): String {
val pairs = listOf(
"oauth_consumer_key" to consumerKey,
"oauth_nonce" to nonce,
"oauth_signature" to signature,
"oauth_signature_method" to "HMAC-SHA1",
"oauth_timestamp" to timestamp.toString(),
"oauth_token" to token,
"oauth_version" to "1.0",
)
val collected = pairs.map { (key, value) ->
"${escaper.escape(key)}=\"${escaper.escape(value)}\""
}.joinToString(", ")
return "OAuth $collected"
}
有了一个授权请求头,现在您可以向 Twitter 的API发送HTTP请求了!
oauthencoder.kt
class OAuthEncoder {
private val escaper = PercentEscaper("_-.", false)
fun createNonce(): String {
val charPool : List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
val randomString = ThreadLocalRandom.current()
.ints(32, 0, charPool.size)
.asSequence()
.map { charPool[it] }
.joinToString("")
return randomString
}
fun createAuthHeader(consumerKey: String, timestamp: Long, nonce: String, signature: String, token: String): String {
val pairs = listOf(
"oauth_consumer_key" to consumerKey,
"oauth_nonce" to nonce,
"oauth_signature" to signature,
"oauth_signature_method" to "HMAC-SHA1",
"oauth_timestamp" to timestamp.toString(),
"oauth_token" to token,
"oauth_version" to "1.0",
)
val collected = pairs.map { (key, value) ->
"${escaper.escape(key)}=\"${escaper.escape(value)}\""
}.joinToString(", ")
return "OAuth $collected"
}
fun hmacSign(signingKey: String, signatureBaseString: String): String {
val hmacSha1 = HmacUtils.hmacSha1(signingKey, signatureBaseString)
return Base64.getEncoder().encodeToString(hmacSha1)
}
fun createSigningKey(consumerSecret: String, oauthTokenSecret: String): String {
val buffer = StringBuilder()
buffer.append(escaper.escape(consumerSecret))
buffer.append("&")
buffer.append(escaper.escape(oauthTokenSecret))
return buffer.toString()
}
fun createSignatureBaseString(method: String, url: String, parameterString: String): String {
val buffer = StringBuilder()
buffer.append(method.uppercase())
buffer.append("&")
buffer.append(escaper.escape(url))
buffer.append("&")
buffer.append(escaper.escape(parameterString))
return buffer.toString()
}
fun createParamString(params: List<Pair<String, String>>): String {
return params.map { (key, value) ->
escaper.escape(key) to escaper.escape(value)
}.sortedBy {
it.first
}.map { (key, value) ->
"$key=$value"
}.joinToString("&")
}
fun createSignature(
consumerSecret: String,
oAuthTokenSecret: String,
method: String,
url: String,
params: List<Pair<String, String>>
): String {
val paramString = createParamString(params)
val signatureBaseString = createSignatureBaseString(method, url, paramString)
val signingKey = createSigningKey(consumerSecret, oAuthTokenSecret)
return hmacSign(signingKey, signatureBaseString)
}
}
twittertransport.kt
interface ITwitterTransport {
data class User(val email: String, val name: String)
fun getUser(oAuthTokenSecret: String, oAuthToken: String): User?
}
class TwitterTransport : ITwitterTransport {
private val logger = LoggerFactory.getLogger(this::class.java)
private val client = HttpClient.newBuilder().build()
private val consumerKey = "CONSUMER_KEY"
private val consumerSecret = "CONSUMER_SECRET"
private val encoder = OAuthEncoder()
override fun getUser(oAuthTokenSecret: String, oAuthToken: String): ITwitterTransport.User {
val url = "https://api.twitter.com/1.1/account/verify_credentials.json"
val nonce = encoder.createNonce()
val timestamp = Instant.now().epochSecond
val params = listOf(
"include_email" to "true",
"oauth_consumer_key" to consumerKey,
"oauth_nonce" to nonce,
"oauth_signature_method" to "HMAC-SHA1",
"oauth_timestamp" to timestamp.toString(),
"oauth_token" to oAuthToken,
"oauth_version" to "1.0"
)
logger.info("oAuthToken = {}", oAuthToken)
val signature = encoder.createSignature(
consumerSecret = consumerSecret,
oAuthTokenSecret = oAuthTokenSecret,
method = "get",
url = url,
params = params
)
val authHeader = encoder.createAuthHeader(
consumerKey = consumerKey,
timestamp = timestamp,
nonce = nonce,
signature = signature,
token = oAuthToken
)
val request = HttpRequest.newBuilder()
.uri(URI.create("$url?include_email=true"))
.header("Authorization", authHeader)
.GET()
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
val body = parse<JsonNode>(response.body())
logger.info("${response.statusCode()} ${response.body()}")
val email = body["email"].asText()
val name = body["name"].asText()
return ITwitterTransport.User(email, name)
}
}
参考文献
[1] - 授权请求, https://developer.twitter.com/en/docs/authentication/oauth-1-0a/authorizing-a-request
[2] - 创建签名, https://developer.twitter.com/en/docs/authentication/oauth-1-0a/creating-a-signature