mirror of
https://github.com/czp3009/bilibili-api.git
synced 2025-02-19 20:50:28 +08:00
添加几个拦截器
This commit is contained in:
parent
ac9d86b401
commit
6acaad0445
30
build.gradle
30
build.gradle
@ -25,11 +25,37 @@ repositories {
|
||||
|
||||
//kotlin
|
||||
dependencies {
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
|
||||
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8
|
||||
compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: '1.3.21'
|
||||
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
|
||||
compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.1.1'
|
||||
}
|
||||
compileKotlin {
|
||||
kotlinOptions.jvmTarget = jvm_target
|
||||
kotlinOptions {
|
||||
jvmTarget = jvm_target
|
||||
freeCompilerArgs = ["-Xjvm-default=enable"]
|
||||
}
|
||||
}
|
||||
compileTestKotlin {
|
||||
kotlinOptions.jvmTarget = jvm_target
|
||||
}
|
||||
|
||||
//logging
|
||||
dependencies {
|
||||
// https://mvnrepository.com/artifact/io.github.microutils/kotlin-logging
|
||||
compile group: 'io.github.microutils', name: 'kotlin-logging', version: '1.6.25'
|
||||
// https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12
|
||||
testCompile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25'
|
||||
}
|
||||
|
||||
//http
|
||||
dependencies {
|
||||
// https://mvnrepository.com/artifact/com.squareup.retrofit2/retrofit
|
||||
compile group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.5.0'
|
||||
// https://mvnrepository.com/artifact/com.squareup.retrofit2/converter-gson
|
||||
compile group: 'com.squareup.retrofit2', name: 'converter-gson', version: '2.5.0'
|
||||
// https://mvnrepository.com/artifact/com.jakewharton.retrofit/retrofit2-kotlin-coroutines-adapter
|
||||
compile group: 'com.jakewharton.retrofit', name: 'retrofit2-kotlin-coroutines-adapter', version: '0.9.2'
|
||||
// https://mvnrepository.com/artifact/com.squareup.okhttp3/logging-interceptor
|
||||
compile group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: '3.13.1'
|
||||
}
|
||||
|
16
src/main/kotlin/com/hiczp/bilibili/api/BaseUrl.kt
Normal file
16
src/main/kotlin/com/hiczp/bilibili/api/BaseUrl.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package com.hiczp.bilibili.api
|
||||
|
||||
/**
|
||||
* 各个站点的域名
|
||||
*/
|
||||
object BaseUrl {
|
||||
/**
|
||||
* passport 站, 用于登录
|
||||
*/
|
||||
val passport = "https://passport.bilibili.com"
|
||||
|
||||
/**
|
||||
* 直播站
|
||||
*/
|
||||
val live = "https://api.live.bilibili.com"
|
||||
}
|
@ -1,22 +1,85 @@
|
||||
package com.hiczp.bilibili.api
|
||||
|
||||
import com.hiczp.bilibili.api.passport.PassportAPI
|
||||
import com.hiczp.bilibili.api.retrofit.interceptor.CommonHeaderInterceptor
|
||||
import com.hiczp.bilibili.api.retrofit.interceptor.CommonParamInterceptor
|
||||
import com.hiczp.bilibili.api.retrofit.interceptor.ParamType
|
||||
import com.hiczp.bilibili.api.retrofit.interceptor.SortAndSignInterceptor
|
||||
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
|
||||
import mu.KotlinLogging
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.time.Instant
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* 此类表示一个模拟的 Bilibili 客户端(Android), 所有调用由此开始.
|
||||
* 多个 BilibiliClient 实例之间不共享登陆状态.
|
||||
* 不能严格保证线程安全.
|
||||
*
|
||||
* @param billingClientProperties 客户端的固有属性, 是一种常量
|
||||
* @param autoRefreshToken 当 Token 过期时是否自动重新登录
|
||||
* @param debug 是否打印请求日志
|
||||
*/
|
||||
class BilibiliClient(
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
var billingClientProperties: BilibiliClientProperties = BilibiliClientProperties()
|
||||
val billingClientProperties: BilibiliClientProperties = BilibiliClientProperties(),
|
||||
private val autoRefreshToken: Boolean = true,
|
||||
private val debug: Boolean = false
|
||||
) {
|
||||
/**
|
||||
* 客户端被打开的时间(BilibiliClient 被实例化的时间)
|
||||
*/
|
||||
val clientInitTime = Instant.now().epochSecond
|
||||
private val initTime = Instant.now().epochSecond
|
||||
|
||||
/**
|
||||
* 是否已登录
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
var isLogin = false
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
val passportAPI: PassportAPI by lazy {
|
||||
val okHttpClient = OkHttpClient.Builder().apply {
|
||||
addInterceptor(CommonHeaderInterceptor(
|
||||
"Display-ID" to {
|
||||
if (isLogin) {
|
||||
"${billingClientProperties.buildVersionId}-$initTime"
|
||||
} else {
|
||||
billingClientProperties.buildVersionId
|
||||
}
|
||||
},
|
||||
"Buvid" to { billingClientProperties.buildVersionId },
|
||||
"User-Agent" to { "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)" },
|
||||
"Device-ID" to { billingClientProperties.hardwareId }
|
||||
))
|
||||
addInterceptor(CommonParamInterceptor(ParamType.FORM_URL_ENCODED,
|
||||
"appkey" 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() }
|
||||
))
|
||||
addInterceptor(SortAndSignInterceptor(ParamType.FORM_URL_ENCODED, billingClientProperties.appSecret))
|
||||
|
||||
//debug
|
||||
if (debug) {
|
||||
addNetworkInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
|
||||
}
|
||||
}.build()
|
||||
|
||||
Retrofit.Builder()
|
||||
.baseUrl(BaseUrl.passport)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(CoroutineCallAdapterFactory())
|
||||
.client(okHttpClient)
|
||||
.build()
|
||||
.create(PassportAPI::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* 登陆
|
||||
|
@ -2,6 +2,7 @@ package com.hiczp.bilibili.api
|
||||
|
||||
/**
|
||||
* 客户端固有属性. 包括版本号, 密钥以及硬件编码.
|
||||
* 默认值对应 5.37.0(release-b220051) 版本.
|
||||
*/
|
||||
class BilibiliClientProperties {
|
||||
/**
|
||||
@ -16,19 +17,34 @@ class BilibiliClientProperties {
|
||||
var appSecret = "560c52ccd288fed045859ed18bffd973"
|
||||
|
||||
/**
|
||||
* 硬件 ID, 尚不明确生成算法. 在每台手机上固定
|
||||
* 客户端平台
|
||||
*/
|
||||
var platform = "android"
|
||||
|
||||
/**
|
||||
* 客户端类型
|
||||
* 此属性在旧版客户端不存在
|
||||
*/
|
||||
var channel = "html5_app_bili"
|
||||
|
||||
/**
|
||||
* 硬件 ID, 尚不明确生成算法
|
||||
*/
|
||||
@Suppress("SpellCheckingInspection")
|
||||
var hardwareId = "JxdyESFAJkcjEicQbBBsCTlbal5uX2Y"
|
||||
var hardwareId = "aBRoDWAVeRhsA3FDewMzS3lLMwM"
|
||||
|
||||
/**
|
||||
* 屏幕尺寸, 大屏手机(已经没有小屏手机了)统一为 xxhdpi
|
||||
*/
|
||||
var scale = "xxhdpi"
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
var version = "5.37.0.5370000"
|
||||
|
||||
/**
|
||||
* 构建版本号
|
||||
* 默认值对应 5.37.0(release-b220051) 版本
|
||||
*/
|
||||
var build = "5370000"
|
||||
|
||||
@ -36,15 +52,4 @@ class BilibiliClientProperties {
|
||||
* 构建版本 ID, 可能是某种 Hash
|
||||
*/
|
||||
var buildVersionId = "XXD9E43D7A1EBB6669597650E3EE417D9E7F5"
|
||||
|
||||
/**
|
||||
* 客户端类型(似乎只有 H5 一种类型)
|
||||
* 此属性在旧版客户端不存在
|
||||
*/
|
||||
var channel = "html5_app_bili"
|
||||
|
||||
/**
|
||||
* 客户端平台
|
||||
*/
|
||||
var platform = "android"
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
package com.hiczp.bilibili.api.passport
|
||||
|
||||
import com.hiczp.bilibili.api.passport.model.GetKeyResponse
|
||||
import com.hiczp.bilibili.api.passport.model.LoginResponse
|
||||
import kotlinx.coroutines.Deferred
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.POST
|
||||
|
||||
@Suppress("DeferredIsResult")
|
||||
interface PassportAPI {
|
||||
@POST("/api/oauth2/getKey")
|
||||
fun getKey(): Deferred<GetKeyResponse>
|
||||
|
||||
@POST("/api/v3/oauth2/login")
|
||||
@FormUrlEncoded
|
||||
fun login(@Field("username") username: String, @Field("password") password: String): Deferred<LoginResponse>
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.hiczp.bilibili.api.passport.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class GetKeyResponse(
|
||||
@SerializedName("code")
|
||||
var code: Int, // 0
|
||||
@SerializedName("message")
|
||||
var message: String,
|
||||
@SerializedName("data")
|
||||
var `data`: Data,
|
||||
@SerializedName("ts")
|
||||
var ts: Int // 1550219688
|
||||
) {
|
||||
data class Data(
|
||||
@SerializedName("hash")
|
||||
var hash: String, // 93ac6f60b4789952
|
||||
@SerializedName("key")
|
||||
var key: String // -----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCdScM09sZJqFPX7bvmB2y6i08JbHsa0v4THafPbJN9NoaZ9Djz1LmeLkVlmWx1DwgHVW+K7LVWT5FV3johacVRuV9837+RNntEK6SE82MPcl7fA++dmW2cLlAjsIIkrX+aIvvSGCuUfcWpWFy3YVDqhuHrNDjdNcaefJIQHMW+sQIDAQAB-----END PUBLIC KEY-----
|
||||
)
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.hiczp.bilibili.api.passport.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class LoginResponse(
|
||||
@SerializedName("code")
|
||||
var code: Int, // 0
|
||||
@SerializedName("data")
|
||||
var `data`: Data,
|
||||
@SerializedName("message")
|
||||
var message: String,
|
||||
@SerializedName("ts")
|
||||
var ts: Int // 1550219689
|
||||
) {
|
||||
data class Data(
|
||||
@SerializedName("cookie_info")
|
||||
var cookieInfo: CookieInfo,
|
||||
@SerializedName("sso")
|
||||
var sso: List<String>,
|
||||
@SerializedName("status")
|
||||
var status: Int, // 0
|
||||
@SerializedName("token_info")
|
||||
var tokenInfo: TokenInfo
|
||||
) {
|
||||
data class CookieInfo(
|
||||
@SerializedName("cookies")
|
||||
var cookies: List<Cookie>,
|
||||
@SerializedName("domains")
|
||||
var domains: List<String>
|
||||
) {
|
||||
data class Cookie(
|
||||
@SerializedName("expires")
|
||||
var expires: Int, // 1552811689
|
||||
@SerializedName("http_only")
|
||||
var httpOnly: Boolean, // 1
|
||||
@SerializedName("name")
|
||||
var name: String, // SESSDATA
|
||||
@SerializedName("value")
|
||||
var value: String // 5ff9ba24%2C1552811689%2C04ae9421
|
||||
)
|
||||
}
|
||||
|
||||
data class TokenInfo(
|
||||
@SerializedName("access_token")
|
||||
var accessToken: String, // fd0303ff75a6ec6b452c28f4d8621021
|
||||
@SerializedName("expires_in")
|
||||
var expiresIn: Int, // 2592000
|
||||
@SerializedName("mid")
|
||||
var mid: Int, // 20293030
|
||||
@SerializedName("refresh_token")
|
||||
var refreshToken: String // 6a333ebded3c3dbdde65d136b3190d21
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.hiczp.bilibili.api.retrofit
|
||||
|
||||
enum class HttpMethod {
|
||||
GET,
|
||||
POST,
|
||||
PATCH,
|
||||
PUT,
|
||||
DELETE,
|
||||
OPTION
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.hiczp.bilibili.api.retrofit
|
||||
|
||||
import okhttp3.FormBody
|
||||
|
||||
inline fun FormBody.forEach(block: (Pair<String, String>) -> Unit) {
|
||||
for (i in 0..size()) {
|
||||
block(encodedName(i) to encodedValue(i))
|
||||
}
|
||||
}
|
||||
|
||||
fun FormBody.raw() =
|
||||
StringBuilder().apply {
|
||||
repeat(size()) {
|
||||
if (it != 0) append('&')
|
||||
append(encodedName(it))
|
||||
append('=')
|
||||
append(encodedValue(it))
|
||||
}
|
||||
}.toString()
|
||||
|
||||
fun FormBody.sortedRaw(): String {
|
||||
val nameAndValue = ArrayList<String>()
|
||||
repeat(size()) {
|
||||
nameAndValue.add("${encodedName(it)}=${encodedValue(it)}")
|
||||
}
|
||||
return nameAndValue.sorted().joinToString(separator = "&")
|
||||
}
|
||||
|
||||
fun FormBody.Builder.addAll(formBody: FormBody): FormBody.Builder {
|
||||
formBody.forEach { (encodedName, encodedValue) ->
|
||||
addEncoded(encodedName, encodedValue)
|
||||
}
|
||||
return this
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.hiczp.bilibili.api.retrofit.interceptor
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
/**
|
||||
* 为请求添加公共 Header
|
||||
*
|
||||
* @param additionHeaders HeaderName to HeaderValueExpression
|
||||
*/
|
||||
class CommonHeaderInterceptor(private vararg val additionHeaders: Pair<String, () -> String>) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request().newBuilder().apply {
|
||||
additionHeaders.forEach { (headerName, headerValueExpression) ->
|
||||
addHeader(headerName, headerValueExpression())
|
||||
}
|
||||
}.build()
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package com.hiczp.bilibili.api.retrofit.interceptor
|
||||
|
||||
import com.hiczp.bilibili.api.retrofit.HttpMethod
|
||||
import com.hiczp.bilibili.api.retrofit.addAll
|
||||
import mu.KotlinLogging
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* 为请求添加公共参数
|
||||
*
|
||||
* @param paramType 参数类型, Query 或 FormUrlEncoded
|
||||
* @param additionParams ParamName to ParamValueExpression
|
||||
*/
|
||||
class CommonParamInterceptor(
|
||||
private val paramType: ParamType,
|
||||
private vararg val additionParams: Pair<String, () -> String>
|
||||
) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
if (paramType == ParamType.QUERY) {
|
||||
val httpUrl = request.url().newBuilder().apply {
|
||||
additionParams.forEach { (paramName, paramValueExpression) ->
|
||||
addQueryParameter(paramName, paramValueExpression())
|
||||
}
|
||||
}.build()
|
||||
return chain.proceed(
|
||||
request.newBuilder().url(httpUrl).build()
|
||||
)
|
||||
} else if (paramType == ParamType.FORM_URL_ENCODED && request.body() is FormBody ||
|
||||
paramType == ParamType.FORM_URL_ENCODED && request.method() == HttpMethod.POST.toString() && request.body()?.contentType() == null
|
||||
) {
|
||||
//TODO 原有的 Body 为空的情况
|
||||
val formBody = FormBody.Builder().apply {
|
||||
//添加原有的参数
|
||||
addAll(request.body() as FormBody)
|
||||
//添加公共参数
|
||||
additionParams.forEach { (paramName, paramValueExpression) ->
|
||||
add(paramName, paramValueExpression())
|
||||
}
|
||||
}.build()
|
||||
return chain.proceed(
|
||||
request.newBuilder().post(formBody).build()
|
||||
)
|
||||
} else {
|
||||
//如果 body 不为 FormBody 将无法添加 FormUrlEncoded 参数
|
||||
logger.error {
|
||||
"Impossible add FormUrlEncoded params to ${request.body()?.javaClass}. Request: " +
|
||||
"${request.method()} ${request.url()}"
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.hiczp.bilibili.api.retrofit.interceptor
|
||||
|
||||
enum class ParamType {
|
||||
QUERY,
|
||||
FORM_URL_ENCODED
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.hiczp.bilibili.api.retrofit.interceptor
|
||||
|
||||
import com.hiczp.bilibili.api.retrofit.addAll
|
||||
import com.hiczp.bilibili.api.retrofit.sortedRaw
|
||||
import mu.KotlinLogging
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.security.MessageDigest
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* 排序参数并添加签名
|
||||
*/
|
||||
class SortAndSignInterceptor(private val paramType: ParamType, private val appSecret: String) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
if (paramType == ParamType.QUERY) {
|
||||
//这里认为 Query 一定不为 null
|
||||
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()
|
||||
)
|
||||
} else if (paramType == ParamType.FORM_URL_ENCODED && request.body() is FormBody) {
|
||||
val sign = calculateSign((request.body() as FormBody).sortedRaw(), appSecret)
|
||||
val formBody = FormBody.Builder().apply {
|
||||
//添加原有的参数
|
||||
addAll(request.body() as FormBody)
|
||||
//添加 sign
|
||||
addEncoded("sign", sign)
|
||||
}.build()
|
||||
return chain.proceed(
|
||||
request.newBuilder().post(formBody).build()
|
||||
)
|
||||
} else {
|
||||
//如果 body 不为 FormBody 将无法添加签名
|
||||
logger.error {
|
||||
"Impossible add sign to ${request.body()?.javaClass}. Request: " +
|
||||
"${request.method()} ${request.url()}"
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* 签名算法为 "$排序后的参数字符串$appSecret".md5()
|
||||
*/
|
||||
private fun calculateSign(string: String, appSecret: String) =
|
||||
MessageDigest.getInstance("MD5")
|
||||
.digest((string + appSecret).toByteArray())
|
||||
.joinToString(separator = "") {
|
||||
String.format("%02x", it)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user