diff --git a/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt b/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt index 8a0136d..8a43e22 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt @@ -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(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(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(BaseUrl.member, defaultCommonHeaderInterceptor, defaultCommonQueryParamInterceptor, - defaultQuerySignInterceptor + defaultSortAndSignInterceptor ) } diff --git a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/HttpConstant.kt b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/HttpConstant.kt index e4e06fa..247f000 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/HttpConstant.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/HttpConstant.kt @@ -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" +} diff --git a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/RetrofitExtension.kt b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/RetrofitExtension.kt index 11f9e32..73c1fbb 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/RetrofitExtension.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/RetrofitExtension.kt @@ -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()) { diff --git a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/CommonParamInterceptor.kt b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/CommonParamInterceptor.kt index 458c256..cfb89c1 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/CommonParamInterceptor.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/CommonParamInterceptor.kt @@ -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?> ) : 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 ?: ""}. " + - "Request: ${request.method()} ${request.url()}" - } return chain.proceed(request) } } diff --git a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/SortAndSignInterceptor.kt b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/SortAndSignInterceptor.kt index 27a5b5c..10cf6c8 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/SortAndSignInterceptor.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/retrofit/interceptor/SortAndSignInterceptor.kt @@ -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 ?: ""}. " + - "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() } }