重构共用参数的添加和签名算法

This commit is contained in:
czp3009 2019-02-22 16:45:05 +08:00
parent fa906093a6
commit 1bc0bf61c2
5 changed files with 118 additions and 86 deletions

View File

@ -6,6 +6,7 @@ import com.hiczp.bilibili.api.member.MemberAPI
import com.hiczp.bilibili.api.message.MessageAPI
import com.hiczp.bilibili.api.passport.PassportAPI
import com.hiczp.bilibili.api.passport.model.LoginResponse
import com.hiczp.bilibili.api.retrofit.Param
import com.hiczp.bilibili.api.retrofit.ParamType
import com.hiczp.bilibili.api.retrofit.exception.BilibiliApiException
import com.hiczp.bilibili.api.retrofit.interceptor.CommonHeaderInterceptor
@ -75,8 +76,8 @@ class BilibiliClient(
@Suppress("SpellCheckingInspection")
private val defaultCommonParamArray = arrayOf(
"access_key" to { token },
"appkey" to { billingClientProperties.appKey },
Param.ACCESS_KEY to { token },
Param.APP_KEY to { billingClientProperties.appKey },
"build" to { billingClientProperties.build },
"channel" to { billingClientProperties.channel },
"mobi_app" to { billingClientProperties.platform },
@ -85,7 +86,7 @@ class BilibiliClient(
)
private val defaultCommonQueryParamInterceptor = CommonParamInterceptor(ParamType.QUERY, *defaultCommonParamArray)
private val defaultQuerySignInterceptor = SortAndSignInterceptor(ParamType.QUERY, billingClientProperties.appSecret)
private val defaultSortAndSignInterceptor = SortAndSignInterceptor(billingClientProperties.appSecret)
/**
* 用户鉴权相关的接口
@ -95,14 +96,14 @@ class BilibiliClient(
createAPI<PassportAPI>(BaseUrl.passport,
defaultCommonHeaderInterceptor,
CommonParamInterceptor(ParamType.FORM_URL_ENCODED,
"appkey" to { billingClientProperties.appKey },
Param.APP_KEY to { billingClientProperties.appKey },
"build" to { billingClientProperties.build },
"channel" to { billingClientProperties.channel },
"mobi_app" to { billingClientProperties.platform },
"platform" to { billingClientProperties.platform },
"ts" to { Instant.now().epochSecond.toString() }
),
SortAndSignInterceptor(ParamType.FORM_URL_ENCODED, billingClientProperties.appSecret)
defaultSortAndSignInterceptor
)
}
@ -117,7 +118,7 @@ class BilibiliClient(
"actionKey" to { "appkey" },
"has_up" to { "1" }
),
defaultQuerySignInterceptor
defaultSortAndSignInterceptor
)
}
@ -129,7 +130,7 @@ class BilibiliClient(
createAPI<AppAPI>(BaseUrl.app,
defaultCommonHeaderInterceptor,
defaultCommonQueryParamInterceptor,
defaultQuerySignInterceptor
defaultSortAndSignInterceptor
)
}
@ -147,7 +148,7 @@ class BilibiliClient(
"Device-ID" to { billingClientProperties.hardwareId }
),
defaultCommonQueryParamInterceptor,
defaultQuerySignInterceptor
defaultSortAndSignInterceptor
)
}
@ -166,7 +167,7 @@ class BilibiliClient(
"uid" to { userId?.toString() },
"version" to { billingClientProperties.version }
),
defaultQuerySignInterceptor
defaultSortAndSignInterceptor
)
}
@ -177,7 +178,7 @@ class BilibiliClient(
createAPI<MemberAPI>(BaseUrl.member,
defaultCommonHeaderInterceptor,
defaultCommonQueryParamInterceptor,
defaultQuerySignInterceptor
defaultSortAndSignInterceptor
)
}

View File

@ -1,5 +1,7 @@
package com.hiczp.bilibili.api.retrofit
//该文件用于防止拼写错误
object Method {
const val GET = "GET"
const val POST = "POST"
@ -13,3 +15,10 @@ object ContentType {
const val JSON = "application/json"
const val FORM_URLENCODED = "application/x-www-form-urlencoded; charset=utf-8"
}
object Param {
const val ACCESS_KEY = "access_key"
@Suppress("SpellCheckingInspection")
const val APP_KEY = "appkey"
const val SIGN = "sign"
}

View File

@ -26,6 +26,13 @@ fun FormBody.sortedRaw(): String {
return nameAndValue.sorted().joinToString(separator = "&")
}
fun FormBody.containsEncodedName(name: String): Boolean {
repeat(size()) {
if (encodedName(it) == name) return true
}
return false
}
fun FormBody.Builder.addAllEncoded(formBody: FormBody): FormBody.Builder {
with(formBody) {
repeat(size()) {

View File

@ -1,6 +1,5 @@
package com.hiczp.bilibili.api.retrofit.interceptor
import com.hiczp.bilibili.api.retrofit.Method
import com.hiczp.bilibili.api.retrofit.ParamType
import com.hiczp.bilibili.api.retrofit.addAllEncoded
import com.hiczp.bilibili.api.retrofit.forEachNonNull
@ -22,46 +21,49 @@ class CommonParamInterceptor(
private vararg val additionParams: Pair<String, () -> String?>
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var request = chain.request()
val body = request.body()
//如果欲添加的参数类型为 Query 则直接添加
if (paramType == ParamType.QUERY) {
val httpUrl = request.url().newBuilder().apply {
additionParams.forEachNonNull { name, value ->
addQueryParameter(name, value)
}
}.build()
return chain.proceed(
request.newBuilder().url(httpUrl).build()
)
}
request = when {
//如果欲添加参数的位置为 Query 则直接添加, paramType 为 FORM_URL_ENCODED 则继续下面的判断
paramType == ParamType.QUERY -> {
val httpUrl = request.url().newBuilder().apply {
additionParams.forEachNonNull { name, value ->
addQueryParameter(name, value)
}
}.build()
request.newBuilder().url(httpUrl).build()
}
//如果请求方式为 POST(只要方式为 POST, body 就一定不为 null)
if (request.method() == Method.POST) {
val body = request.body()!!
val newFormBody = {
FormBody.Builder().apply {
//BODY 不存在或者是空的
body == null || body.contentLength() == 0L -> {
val formBody = FormBody.Builder().apply {
additionParams.forEachNonNull { name, value ->
add(name, value)
}
}
}.build()
request.newBuilder().method(request.method(), formBody).build()
}
if (body.contentType() == null) { //如果 body 为空
return chain.proceed(
request.newBuilder().post(newFormBody().build()).build()
)
} else if (body is FormBody) { //如果 body 为 FormBody
return chain.proceed(
request.newBuilder().post(newFormBody().addAllEncoded(body).build()).build()
)
//只要 BODY 为 FormBody, 那么里面一定有内容
body is FormBody -> {
val formBody = FormBody.Builder().apply {
addAllEncoded(body)
additionParams.forEachNonNull { name, value ->
add(name, value)
}
}.build()
request.newBuilder().method(request.method(), formBody).build()
}
else -> {
logger.error {
"Cannot add params to request: ${request.method()} ${request.url()} ${body.javaClass.simpleName}"
}
request
}
}
//其他情况, 例如请求方式为 GET 或 Body 为 Json
logger.error {
"Impossible add FormUrlEncoded params to ${request.body()?.javaClass?.simpleName ?: "<NullBody>"}. " +
"Request: ${request.method()} ${request.url()}"
}
return chain.proceed(request)
}
}

View File

@ -1,7 +1,7 @@
package com.hiczp.bilibili.api.retrofit.interceptor
import com.hiczp.bilibili.api.retrofit.Method
import com.hiczp.bilibili.api.retrofit.ParamType
import com.hiczp.bilibili.api.retrofit.Param
import com.hiczp.bilibili.api.retrofit.containsEncodedName
import com.hiczp.bilibili.api.retrofit.sortedRaw
import mu.KotlinLogging
import okhttp3.FormBody
@ -13,60 +13,73 @@ private val logger = KotlinLogging.logger {}
/**
* 排序参数并添加签名
* 必须保证在进入此拦截器前, 公共参数已被添加
* 此拦截器将自动识别 appKey Query 还是在 FormBody 并添加 sign 到相应位置
*
* @param appSecret 密钥
*/
class SortAndSignInterceptor(private val paramType: ParamType, private val appSecret: String) : Interceptor {
class SortAndSignInterceptor(private val appSecret: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
//如果欲签名的参数类型为 Query
if (paramType == ParamType.QUERY) {
//如果代码是正确的, 那么传到这里的 Query 参数一定包含了公共参数
val sortedEncodedQuery = request.url().encodedQuery()!!
.split('&')
.sorted()
.joinToString(separator = "&")
val sign = calculateSign(sortedEncodedQuery, appSecret)
return chain.proceed(
request.newBuilder()
.url(request.url().newBuilder()
.encodedQuery("$sortedEncodedQuery&sign=$sign")
.build()
)
.build()
)
}
var request = chain.request()
val url = request.url()
val body = request.body()
if (request.method() == Method.POST && body is FormBody) {
val sortedRaw = body.sortedRaw()
val sign = calculateSign(sortedRaw, appSecret)
val newFormBody = FormBody.Builder().apply {
sortedRaw.split('&').forEach {
val (name, value) = it.split('=')
addEncoded(name, value)
}
addEncoded("sign", sign)
}.build()
return chain.proceed(request.newBuilder().post(newFormBody).build())
request = when {
//判断 appKey 是否在 Query 里
url.queryParameter(Param.APP_KEY) != null -> {
val sortedEncodedQuery = url.encodedQuery()!!
.split('&')
.sorted()
.joinToString(separator = "&")
val sign = calculateSign(sortedEncodedQuery, appSecret)
request.newBuilder()
.url(url.newBuilder()
.encodedQuery("$sortedEncodedQuery&${Param.SIGN}=$sign")
.build()
).build()
}
//在 FormBody 里
body is FormBody && body.containsEncodedName(Param.APP_KEY) -> {
val sortedRaw = body.sortedRaw()
val sign = calculateSign(sortedRaw, appSecret)
val formBody = FormBody.Builder().apply {
sortedRaw.split('&').forEach {
val (name, value) = it.split('=')
addEncoded(name, value)
}
addEncoded(Param.SIGN, sign)
}.build()
request.newBuilder()
.method(request.method(), formBody)
.build()
}
//不存在 accessKey
else -> {
logger.error { "Fail to sign request because no ${Param.APP_KEY} found in request" }
request
}
}
//其他情况, 例如请求方式为 GET 或 Body 为 Json
logger.error {
"Impossible add sign to ${request.body()?.javaClass?.simpleName ?: "<NullBody>"}. " +
"Request: ${request.method()} ${request.url()}"
}
return chain.proceed(request)
}
companion object {
private val md5Instance = MessageDigest.getInstance("MD5")
/**
* 签名算法为 "$排序后的参数字符串$appSecret".md5()
*/
private fun calculateSign(string: String, appSecret: String) =
MessageDigest.getInstance("MD5")
.digest((string + appSecret).toByteArray())
.joinToString(separator = "") {
"%02x".format(it)
}
StringBuilder(32).apply {
//优化过的 md5 字符串生成算法
md5Instance.digest((string + appSecret).toByteArray()).forEach {
val value = it.toInt() and 0xFF
val high = value / 16
val low = value - high * 16
append(if (high <= 9) '0' + high else 'a' - 10 + high)
append(if (low <= 9) '0' + low else 'a' - 10 + low)
}
}.toString()
}
}