使用 Ktor 调用 Twitter OAuth 1.0A

我最近在做一个新项目,牵涉到许多提供通过社交媒体登录的提供方。这意味着我需要学习什么是 Oauth 以及如何创建 API 来与 Oauth 提供商进行通信,如Facebook、Google、Github和…推特。对于前三个提供方,工作一切顺利,但Twitter不是。我花了大约五个小时阅读Twitter的文档,浏览博客文章和库,编写代码。我是这么操作的。

步骤1 - 配置 OAuth URL 和参数

我用的是Ktor的OAuth包,你需要更改urlProvider,consumerKeyconsumerSecret.其他字段应该保持不变。

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

Join The Mailing List