获取用户个人信息

This commit is contained in:
czp3009 2019-02-20 14:55:07 +08:00
parent 2d6e249c05
commit e083859b1a
12 changed files with 285 additions and 46 deletions

View File

@ -2,6 +2,8 @@
该项目提供 Bilibili API 的 JVM 调用, 协议来自 Bilibili Android APP 的逆向工程以及截包分析.
# 登录和登出
https://passport.bilibili.com
登陆和登出均为异步方法, 需要在协程上下文中执行.
runBlocking {
@ -27,7 +29,7 @@
{"ts":1550569982,"code":-105,"data":{"url":"https://passport.bilibili.com/register/verification.html?success=1&gt=b6e5b7fad7ecd37f465838689732e788&challenge=9a67afa4d42ede71a93aeaaa54a4b6fe&ct=1&hash=105af2e7cc6ea829c4a95205f2371dc5"},"message":"验证码错误!"}
自行访问 `data.url` 将打开一个极验弹窗, 通过验证码后再次调用登陆接口:
自行访问 `commonResponse.data.obj.url.string` 将打开一个极验弹窗, 通过验证码后再次调用登陆接口:
login(username, password, challenge, secCode, validate)
@ -41,6 +43,21 @@
登陆后, 可以访问全部 API.
# app
https://app.bilibili.com
BilibiliClient().appAPI()
为 app 提供通用接口, 例如获取个人信息. 完整示例如下
runBlocking {
val bilibiliClient = BilibiliClient().apply {
login(username, password)
}
val myInfo = bilibiliClient.appAPI().myInfo().await()
println(myInfo)
}
# 主站
//TODO

View File

@ -54,6 +54,8 @@ dependencies {
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.github.salomonbrys.kotson/kotson
compile group: 'com.github.salomonbrys.kotson', name: 'kotson', 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

View File

@ -9,6 +9,11 @@ object BaseUrl {
*/
const val passport = "https://passport.bilibili.com"
/**
* 提供通用功能, 例如获取用户信息
*/
const val app = "https://app.bilibili.com"
/**
* 直播站
*/

View File

@ -1,15 +1,17 @@
package com.hiczp.bilibili.api
import com.hiczp.bilibili.api.exception.BilibiliApiException
import com.hiczp.bilibili.api.app.AppAPI
import com.hiczp.bilibili.api.passport.PassportAPI
import com.hiczp.bilibili.api.passport.model.LoginResponse
import com.hiczp.bilibili.api.retrofit.ParamType
import com.hiczp.bilibili.api.retrofit.exception.BilibiliApiException
import com.hiczp.bilibili.api.retrofit.interceptor.CommonHeaderInterceptor
import com.hiczp.bilibili.api.retrofit.interceptor.CommonParamInterceptor
import com.hiczp.bilibili.api.retrofit.interceptor.FailureResponseInterceptor
import com.hiczp.bilibili.api.retrofit.interceptor.SortAndSignInterceptor
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import mu.KotlinLogging
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
@ -55,39 +57,57 @@ class BilibiliClient(
val isLogin
get() = loginResponse != null
//快捷方式
val userId get() = loginResponse?.userId
val token get() = loginResponse?.token
/**
* 用户鉴权相关的接口
*/
@Suppress("SpellCheckingInspection")
val passportAPI: PassportAPI by lazy {
val okHttpClient = OkHttpClient.Builder().apply {
addInterceptor(CommonHeaderInterceptor(
"Display-ID" to { "${billingClientProperties.buildVersionId}-$initTime" },
"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))
addInterceptor(FailureResponseInterceptor)
val passportAPI by lazy {
createAPI<PassportAPI>(BaseUrl.passport, logLevel,
CommonHeaderInterceptor(
"Display-ID" to { "${billingClientProperties.buildVersionId}-$initTime" },
"Buvid" to { billingClientProperties.buildVersionId },
"User-Agent" to { "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)" },
"Device-ID" to { billingClientProperties.hardwareId }
),
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() }
),
SortAndSignInterceptor(ParamType.FORM_URL_ENCODED, billingClientProperties.appSecret)
)
}
//log
if (logLevel != HttpLoggingInterceptor.Level.NONE) {
addNetworkInterceptor(HttpLoggingInterceptor().setLevel(logLevel))
}
}.build()
Retrofit.Builder()
.baseUrl(BaseUrl.passport)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.client(okHttpClient)
.build()
.create(PassportAPI::class.java)
/**
* 提供一些通用信息
*/
@Suppress("SpellCheckingInspection")
val appAPI by lazy {
createAPI<AppAPI>(BaseUrl.app, logLevel,
CommonHeaderInterceptor(
"Display-ID" to { "${billingClientProperties.buildVersionId}-$initTime" },
"Buvid" to { billingClientProperties.buildVersionId },
"User-Agent" to { "Mozilla/5.0 BiliDroid/5.37.0 (bbcallen@gmail.com)" },
"Device-ID" to { billingClientProperties.hardwareId }
),
CommonParamInterceptor(ParamType.QUERY,
"access_key" to { token },
"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() }
),
SortAndSignInterceptor(ParamType.QUERY, billingClientProperties.appSecret)
)
}
/**
@ -145,3 +165,27 @@ class BilibiliClient(
loginResponse = null
}
}
@Suppress("SpellCheckingInspection")
private val gsonConverterFactory = GsonConverterFactory.create()
private val coroutineCallAdapterFactory = CoroutineCallAdapterFactory()
private inline fun <reified T : Any> createAPI(
baseUrl: String,
logLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.NONE,
vararg interceptors: Interceptor
) = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(coroutineCallAdapterFactory)
.client(OkHttpClient.Builder().apply {
interceptors.forEach {
addInterceptor(it)
}
addInterceptor(FailureResponseInterceptor)
//log
if (logLevel != HttpLoggingInterceptor.Level.NONE) {
addNetworkInterceptor(HttpLoggingInterceptor().setLevel(logLevel))
}
}.build())
.build()
.create(T::class.java)

View File

@ -0,0 +1,12 @@
package com.hiczp.bilibili.api.app
import com.hiczp.bilibili.api.app.model.MyInfo
import kotlinx.coroutines.Deferred
import retrofit2.http.GET
@Suppress("DeferredIsResult")
interface AppAPI {
@Suppress("SpellCheckingInspection")
@GET("/x/v2/account/myinfo")
fun myInfo(): Deferred<MyInfo>
}

View File

@ -0,0 +1,67 @@
package com.hiczp.bilibili.api.app.model
import com.google.gson.annotations.SerializedName
data class MyInfo(
@SerializedName("code")
var code: Int, // 0
@SerializedName("data")
var `data`: Data,
@SerializedName("message")
var message: String, // 0
@SerializedName("ttl")
var ttl: Int // 1
) {
data class Data(
@SerializedName("birthday")
var birthday: String, // 1995-11-18
@SerializedName("coins")
var coins: Int, // 1025
@SerializedName("email_status")
var emailStatus: Int, // 0
@SerializedName("face")
var face: String, // http://i1.hdslb.com/bfs/face/4f65e79399ad5a1bf3f877851b2f819d5870b494.jpg
@SerializedName("identification")
var identification: Int, // 1
@SerializedName("level")
var level: Int, // 4
@SerializedName("mid")
var mid: Int, // 20293030
@SerializedName("name")
var name: String, // czp3009
@SerializedName("official")
var official: Official,
@SerializedName("rank")
var rank: Int, // 10000
@SerializedName("sex")
var sex: Int, // 0
@SerializedName("sign")
var sign: String,
@SerializedName("silence")
var silence: Int, // 0
@SerializedName("tel_status")
var telStatus: Int, // 1
@SerializedName("vip")
var vip: Vip
) {
data class Official(
@SerializedName("desc")
var desc: String,
@SerializedName("role")
var role: Int, // 0
@SerializedName("title")
var title: String
)
data class Vip(
@SerializedName("due_date")
var dueDate: Int, // 0
@SerializedName("status")
var status: Int, // 0
@SerializedName("type")
var type: Int, // 0
@SerializedName("vip_pay_type")
var vipPayType: Int // 0
)
}
}

View File

@ -26,5 +26,11 @@ data class CommonResponse(
* data 可能是各种类型, 例如 array, object, string
*/
@SerializedName("data")
var data: JsonElement?
var data: JsonElement?,
/**
* ttl, 不明确含义, 如果存在则值总为 1
*/
@SerializedName("ttl")
var ttl: Int?
)

View File

@ -1,4 +1,4 @@
package com.hiczp.bilibili.api.exception
package com.hiczp.bilibili.api.retrofit.exception
import com.hiczp.bilibili.api.retrofit.CommonResponse
import java.io.IOException

View File

@ -1,9 +1,11 @@
package com.hiczp.bilibili.api.retrofit.interceptor
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.obj
import com.google.gson.Gson
import com.google.gson.JsonParser
import com.hiczp.bilibili.api.exception.BilibiliApiException
import com.hiczp.bilibili.api.retrofit.CommonResponse
import com.hiczp.bilibili.api.retrofit.exception.BilibiliApiException
import okhttp3.Interceptor
import okhttp3.Response
@ -32,12 +34,12 @@ object FailureResponseInterceptor : Interceptor {
val jsonObject = body.source().also {
it.request(Long.MAX_VALUE)
}.buffer.clone().inputStream().reader(charset).let {
jsonParser.parse(it).asJsonObject
jsonParser.parse(it).obj
}
//判断 code 是否为 0
if (jsonObject["code"].asInt != 0) {
throw BilibiliApiException(gson.fromJson(jsonObject, CommonResponse::class.java))
if (jsonObject["code"].int != 0) {
throw BilibiliApiException(gson.fromJson(jsonObject))
}
return response

View File

@ -1,15 +1,27 @@
package com.hiczp.bilibili.api.test
import com.github.salomonbrys.kotson.byString
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.hiczp.bilibili.api.BilibiliClient
import okhttp3.logging.HttpLoggingInterceptor
object Config {
private val config = Gson().fromJson(
Config::class.java.getResourceAsStream("/config.json").reader(),
JsonObject::class.java
@Suppress("SpellCheckingInspection")
private val gson = Gson()
private val config = gson.fromJson<JsonObject>(
Config::class.java.getResourceAsStream("/config.json").reader()
)
val username = config["username"].asString!!
val username by config.byString
val password = config["password"].asString!!
val password by config.byString
val presetBilibiliClient by lazy {
BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY).apply {
loginResponse = config["loginResponse"]?.let { gson.fromJson(it) }
}
}
}

View File

@ -0,0 +1,13 @@
package com.hiczp.bilibili.api.test
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
class UserInfoTest {
@Test
fun info() {
runBlocking {
Config.presetBilibiliClient.appAPI.myInfo().await()
}
}
}

View File

@ -1,5 +1,64 @@
{
"username": "123456789",
"password": "123456",
"roomId": "3"
"roomId": "3",
"loginResponse": {
"ts": 1550629285,
"code": 0,
"data": {
"status": 0,
"token_info": {
"mid": 20293030,
"access_token": "b0b214e97b7bf388769eb727da27f951",
"refresh_token": "54c56bda9bde64cacb44a14e92007d51",
"expires_in": 2592000
},
"cookie_info": {
"cookies": [
{
"name": "bili_jct",
"value": "578b271b308c760cbd281c37afc420c4",
"http_only": 0,
"expires": 1553221285
},
{
"name": "DedeUserID",
"value": "20293035",
"http_only": 0,
"expires": 1553221285
},
{
"name": "DedeUserID__ckMd5",
"value": "cdff5c8e58b793cd",
"http_only": 0,
"expires": 1553221285
},
{
"name": "sid",
"value": "ja724hee",
"http_only": 0,
"expires": 1553221285
},
{
"name": "SESSDATA",
"value": "eaa5b420%2C1553221285%2C59590a31",
"http_only": 1,
"expires": 1553221285
}
],
"domains": [
".bilibili.com",
".biligame.com",
".im9.com",
".bigfunapp.cn"
]
},
"sso": [
"https://passport.bilibili.com/api/v2/sso",
"https://passport.biligame.com/api/v2/sso",
"https://passport.im9.com/api/v2/sso",
"https://passport.bigfunapp.cn/api/v2/sso"
]
}
}
}