diff --git a/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt b/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt index 5d773c5..e575ac5 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt @@ -1,6 +1,7 @@ package com.hiczp.bilibili.api 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.interceptor.CommonHeaderInterceptor import com.hiczp.bilibili.api.retrofit.interceptor.CommonParamInterceptor @@ -11,7 +12,11 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.security.KeyFactory +import java.security.spec.X509EncodedKeySpec import java.time.Instant +import java.util.* +import javax.crypto.Cipher private val logger = KotlinLogging.logger {} @@ -35,11 +40,17 @@ class BilibiliClient( */ private val initTime = Instant.now().epochSecond + /** + * 登陆操作得到的 Response + */ + var loginResponse: LoginResponse? = null + /** * 是否已登录 */ @Suppress("MemberVisibilityCanBePrivate") - var isLogin = false + val isLogin + get() = loginResponse != null @Suppress("SpellCheckingInspection") val passportAPI: PassportAPI by lazy { @@ -84,17 +95,42 @@ class BilibiliClient( /** * 登陆 */ - suspend fun login(username: String, password: String) { - val (hash, key) = passportAPI.getKey().await().data.let { - it.hash to it.key + suspend fun login(username: String, password: String): LoginResponse { + //取得 hash 和 RSA 公钥 + val (hash, key) = passportAPI.getKey().await().data.let { data -> + data.hash to data.key.split('\n').filterNot { it.startsWith('-') }.joinToString(separator = "") } + //解析 RSA 公钥 + val publicKey = X509EncodedKeySpec(Base64.getDecoder().decode(key)).let { + KeyFactory.getInstance("RSA").generatePublic(it) + } + //加密密码 + //兼容 Android + val cipheredPassword = Cipher.getInstance("RSA/ECB/PKCS1Padding").apply { + init(Cipher.ENCRYPT_MODE, publicKey) + }.doFinal((hash + password).toByteArray()).let { + Base64.getEncoder().encode(it) + }.let { + String(it) + } + + return passportAPI.login(username, cipheredPassword).await().also { + this.loginResponse = it + } } /** * 登出 + * 这个方法不一定是线程安全的, 登出的同时如果进行登陆操作可能引发错误 */ - fun logout() { - TODO() + suspend fun logout() { + val data = loginResponse?.data ?: return + val cookieMap = data.cookieInfo.cookies + .associate { + it.name to it.value + } + passportAPI.revoke(cookieMap, data.tokenInfo.accessToken).await() + loginResponse = null } } diff --git a/src/main/kotlin/com/hiczp/bilibili/api/CommonResponse.kt b/src/main/kotlin/com/hiczp/bilibili/api/CommonResponse.kt new file mode 100644 index 0000000..72b7d1f --- /dev/null +++ b/src/main/kotlin/com/hiczp/bilibili/api/CommonResponse.kt @@ -0,0 +1,12 @@ +package com.hiczp.bilibili.api + +import com.google.gson.annotations.SerializedName + +data class CommonResponse( + @SerializedName("code") + var code: Int, // 0 + @SerializedName("message") + var message: String?, + @SerializedName("ts") + var ts: Int // 1550546539 +) diff --git a/src/main/kotlin/com/hiczp/bilibili/api/passport/PassportAPI.kt b/src/main/kotlin/com/hiczp/bilibili/api/passport/PassportAPI.kt index d2716af..6f82e32 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/passport/PassportAPI.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/passport/PassportAPI.kt @@ -1,9 +1,11 @@ package com.hiczp.bilibili.api.passport +import com.hiczp.bilibili.api.CommonResponse 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.FieldMap import retrofit2.http.FormUrlEncoded import retrofit2.http.POST @@ -15,4 +17,28 @@ interface PassportAPI { @POST("/api/v3/oauth2/login") @FormUrlEncoded fun login(@Field("username") username: String, @Field("password") password: String): Deferred + + /** + * 除了 accessToken, 其他全部都是 cookie 的值 + */ + @POST("/api/v2/oauth2/revoke") + @FormUrlEncoded + fun revoke( + @Field("DedeUserID") dedeUserId: String, + @Field("DedeUserID__ckMd5") ckMd5: String, + @Suppress("SpellCheckingInspection") @Field("SESSDATA") sessData: String, + @Field("access_token") accessToken: String, + @Field("bili_jct") biliJct: String, + @Field("sid") sid: String + ): Deferred + + /** + * 将所有 cookie 以 Map 形式传入 + */ + @POST("/api/v2/oauth2/revoke") + @FormUrlEncoded + fun revoke( + @FieldMap cookieMap: Map, + @Field("access_token") accessToken: String + ): Deferred } diff --git a/src/main/kotlin/com/hiczp/bilibili/api/passport/model/LoginResponse.kt b/src/main/kotlin/com/hiczp/bilibili/api/passport/model/LoginResponse.kt index 4158c72..3769eec 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/passport/model/LoginResponse.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/passport/model/LoginResponse.kt @@ -32,7 +32,7 @@ data class LoginResponse( @SerializedName("expires") var expires: Int, // 1552811689 @SerializedName("http_only") - var httpOnly: Boolean, // 1 + var httpOnly: Int, // 1 @SerializedName("name") var name: String, // SESSDATA @SerializedName("value") 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 c976d71..eaac47a 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 @@ -2,7 +2,6 @@ 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.addAll import com.hiczp.bilibili.api.retrofit.sortedRaw import mu.KotlinLogging import okhttp3.FormBody @@ -39,9 +38,13 @@ class SortAndSignInterceptor(private val paramType: ParamType, private val appSe val body = request.body() if (request.method() == Method.POST && body is FormBody) { - val sign = calculateSign(body.sortedRaw(), appSecret) + val sortedRaw = body.sortedRaw() + val sign = calculateSign(sortedRaw, appSecret) val newFormBody = FormBody.Builder().apply { - addAll(body) + sortedRaw.split('&').forEach { + val (name, value) = it.split('=') + addEncoded(name, value) + } addEncoded("sign", sign) }.build() return chain.proceed(request.newBuilder().post(newFormBody).build()) diff --git a/src/test/kotlin/com/hiczp/bilibili/api/test/LoginTest.kt b/src/test/kotlin/com/hiczp/bilibili/api/test/LoginTest.kt index 7cd313f..1ae1038 100644 --- a/src/test/kotlin/com/hiczp/bilibili/api/test/LoginTest.kt +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/LoginTest.kt @@ -7,10 +7,13 @@ import org.junit.jupiter.api.Test class LoginTest { @Test - fun login() { + fun loginAndLogout() { runBlocking { BilibiliClient(logLevel = HttpLoggingInterceptor.Level.BODY) - .login(Config.username, Config.password) + .run { + login(Config.username, Config.password) + logout() + } } } }