完成登录和登出

This commit is contained in:
czp3009 2019-02-19 12:11:59 +08:00
parent ca86c44995
commit b0bff448f9
6 changed files with 92 additions and 12 deletions

View File

@ -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
}
}

View File

@ -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
)

View File

@ -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<LoginResponse>
/**
* 除了 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<CommonResponse>
/**
* 将所有 cookie Map 形式传入
*/
@POST("/api/v2/oauth2/revoke")
@FormUrlEncoded
fun revoke(
@FieldMap cookieMap: Map<String, String>,
@Field("access_token") accessToken: String
): Deferred<CommonResponse>
}

View File

@ -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")

View File

@ -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())

View File

@ -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()
}
}
}
}