diff --git a/build.gradle b/build.gradle index 01980ba..19ab67e 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,6 @@ apply plugin: 'signing' sourceCompatibility = 1.8 repositories { - maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } mavenCentral() } diff --git a/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java b/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java index f18a52d..7d1e792 100644 --- a/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java +++ b/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java @@ -1,16 +1,17 @@ package com.hiczp.bilibili.api; +import com.hiczp.bilibili.api.cookie.SimpleCookieJar; import com.hiczp.bilibili.api.interceptor.*; import com.hiczp.bilibili.api.live.LiveService; import com.hiczp.bilibili.api.live.socket.LiveClient; import com.hiczp.bilibili.api.passport.PassportService; +import com.hiczp.bilibili.api.passport.SsoService; import com.hiczp.bilibili.api.passport.entity.InfoEntity; import com.hiczp.bilibili.api.passport.entity.LoginResponseEntity; import com.hiczp.bilibili.api.passport.entity.LogoutResponseEntity; import com.hiczp.bilibili.api.passport.entity.RefreshTokenResponseEntity; import com.hiczp.bilibili.api.passport.exception.CaptchaMismatchException; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; +import okhttp3.*; import okhttp3.logging.HttpLoggingInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,6 +19,7 @@ import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.security.auth.login.LoginException; import java.io.IOException; import java.text.SimpleDateFormat; @@ -25,8 +27,9 @@ import java.time.Instant; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map; -public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider { +public class BilibiliAPI implements BilibiliServiceProvider, BilibiliSsoProvider, LiveClientProvider { private static final Logger LOGGER = LoggerFactory.getLogger(BilibiliAPI.class); private final Long apiInitTime = Instant.now().getEpochSecond(); //记录当前类被实例化的时间 @@ -72,8 +75,6 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider public PassportService getPassportService(@Nonnull List interceptors, @Nonnull HttpLoggingInterceptor.Level logLevel) { OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder(); - interceptors.forEach(okHttpClientBuilder::addInterceptor); - okHttpClientBuilder .addInterceptor(new AddFixedParamsInterceptor( "build", bilibiliClientProperties.getBuild(), @@ -85,7 +86,11 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider )) .addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties)) .addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties)) - .addInterceptor(new ErrorResponseConverterInterceptor()) + .addInterceptor(new ErrorResponseConverterInterceptor()); + + interceptors.forEach(okHttpClientBuilder::addInterceptor); + + okHttpClientBuilder .addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(logLevel)); return new Retrofit.Builder() @@ -107,8 +112,6 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider public LiveService getLiveService(@Nonnull List interceptors, @Nonnull HttpLoggingInterceptor.Level logLevel) { OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder(); - interceptors.forEach(okHttpClientBuilder::addInterceptor); - okHttpClientBuilder .addInterceptor(new AddFixedHeadersInterceptor( "Buvid", bilibiliClientProperties.getBuvId(), @@ -144,7 +147,11 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider )) .addInterceptor(new AddAccessKeyInterceptor(bilibiliAccount)) .addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties)) - .addInterceptor(new ErrorResponseConverterInterceptor()) + .addInterceptor(new ErrorResponseConverterInterceptor()); + + interceptors.forEach(okHttpClientBuilder::addInterceptor); + + okHttpClientBuilder .addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(logLevel)); return new Retrofit.Builder() @@ -155,6 +162,45 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider .create(LiveService.class); } + public SsoService getSsoService() { + return getSsoService(new SimpleCookieJar()); + } + + //sso 需要保存 cookie, 不对 SsoService 进行缓存 + @Override + public SsoService getSsoService(CookieJar cookieJar) { + return getSsoService(cookieJar, Collections.emptyList(), HttpLoggingInterceptor.Level.BASIC); + } + + public SsoService getSsoService(@Nonnull CookieJar cookieJar, @Nonnull List interceptors, @Nonnull HttpLoggingInterceptor.Level logLevel) { + OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder(); + + okHttpClientBuilder + .cookieJar(cookieJar) + .addInterceptor(new AddFixedParamsInterceptor( + "build", bilibiliClientProperties.getBuild(), + "mobi_app", "android", + "platform", "android" + )) + .addInterceptor(new AddDynamicParamsInterceptor( + () -> "ts", () -> Long.toString(Instant.now().getEpochSecond()) + )) + .addInterceptor(new AddAccessKeyInterceptor(bilibiliAccount)) + .addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties)) + .addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties)); + + interceptors.forEach(okHttpClientBuilder::addInterceptor); + + okHttpClientBuilder + .addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(logLevel)); + + return new Retrofit.Builder() + .baseUrl(BaseUrlDefinition.PASSPORT) + .client(okHttpClientBuilder.build()) + .build() + .create(SsoService.class); + } + public LoginResponseEntity login(@Nonnull String username, @Nonnull String password) throws IOException, LoginException, CaptchaMismatchException { return login(username, password, null, null); } @@ -270,6 +316,30 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider return infoEntity; } + @Override + public HttpUrl getSsoUrl(@Nullable String goUrl) { + CancelRequestInterceptor cancelRequestInterceptor = new CancelRequestInterceptor(); + try { + getSsoService(new SimpleCookieJar(), Collections.singletonList(cancelRequestInterceptor), HttpLoggingInterceptor.Level.BASIC) + .sso(null) + .execute(); + } catch (IOException ignored) { + + } + return cancelRequestInterceptor.getRequest().url(); + } + + @Override + public Map> toCookies() throws IOException { + return toCookies(BaseUrlDefinition.PASSPORT + "api/oauth2/getKey"); + } + + public Map> toCookies(@Nullable String goUrl) throws IOException { + SimpleCookieJar simpleCookieJar = new SimpleCookieJar(); + getSsoService(simpleCookieJar).sso(goUrl).execute(); + return simpleCookieJar.getCookiesMap(); + } + @Override public LiveClient getLiveClient(long showRoomId) { return bilibiliAccount.getUserId() == null ? diff --git a/src/main/java/com/hiczp/bilibili/api/BilibiliSsoProvider.java b/src/main/java/com/hiczp/bilibili/api/BilibiliSsoProvider.java new file mode 100644 index 0000000..d203492 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/BilibiliSsoProvider.java @@ -0,0 +1,20 @@ +package com.hiczp.bilibili.api; + +import com.hiczp.bilibili.api.passport.SsoService; +import okhttp3.Cookie; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public interface BilibiliSsoProvider { + SsoService getSsoService(CookieJar cookieJar); + + //获取用于进行 sso 登录的初始 URL + HttpUrl getSsoUrl(String goUrl); + + //获取当前 token 对应的 cookies + Map> toCookies() throws IOException; +} diff --git a/src/main/java/com/hiczp/bilibili/api/cookie/SimpleCookieJar.java b/src/main/java/com/hiczp/bilibili/api/cookie/SimpleCookieJar.java new file mode 100644 index 0000000..24998bf --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/cookie/SimpleCookieJar.java @@ -0,0 +1,67 @@ +package com.hiczp.bilibili.api.cookie; + +import okhttp3.Cookie; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SimpleCookieJar implements CookieJar { + private Map> cookiesMap; + + public SimpleCookieJar() { + cookiesMap = new HashMap<>(); + } + + public SimpleCookieJar(Map> cookiesMap) { + this.cookiesMap = new HashMap<>(cookiesMap); + } + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + cookies.forEach(cookie -> { + String domain = cookie.domain(); + List savedCookies = cookiesMap.get(domain); + if (savedCookies == null) { + savedCookies = new ArrayList<>(); + savedCookies.add(cookie); + cookiesMap.put(domain, savedCookies); + } else { + for (Cookie savedCookie : savedCookies) { + if (savedCookie.name().equals(cookie.name())) { + savedCookies.remove(savedCookie); + break; + } + } + savedCookies.add(cookie); + } + }); + } + + @Override + public List loadForRequest(HttpUrl url) { + return getCookiesForHost(url.host()); + } + + public List getCookiesForHost(String host) { + List cookieList = new ArrayList<>(); + cookiesMap.forEach((domain, cookies) -> { + if (host.endsWith(domain)) { + for (int i = cookies.size() - 1; i >= 0; i--) { + if (cookies.get(i).expiresAt() < System.currentTimeMillis()) { + cookies.remove(i); + } + } + cookieList.addAll(cookies); + } + }); + return cookieList; + } + + public Map> getCookiesMap() { + return cookiesMap; + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/exception/UserCancelRequestException.java b/src/main/java/com/hiczp/bilibili/api/exception/UserCancelRequestException.java new file mode 100644 index 0000000..39cb0f9 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/exception/UserCancelRequestException.java @@ -0,0 +1,21 @@ +package com.hiczp.bilibili.api.exception; + +import java.io.IOException; + +public class UserCancelRequestException extends IOException { + public UserCancelRequestException() { + + } + + public UserCancelRequestException(String message) { + super(message); + } + + public UserCancelRequestException(String message, Throwable cause) { + super(message, cause); + } + + public UserCancelRequestException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/interceptor/CancelRequestInterceptor.java b/src/main/java/com/hiczp/bilibili/api/interceptor/CancelRequestInterceptor.java new file mode 100644 index 0000000..1ed1bd5 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/interceptor/CancelRequestInterceptor.java @@ -0,0 +1,22 @@ +package com.hiczp.bilibili.api.interceptor; + +import com.hiczp.bilibili.api.exception.UserCancelRequestException; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +public class CancelRequestInterceptor implements Interceptor { + private Request request; + + @Override + public Response intercept(Chain chain) throws IOException { + request = chain.request(); + throw new UserCancelRequestException(); + } + + public Request getRequest() { + return request; + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/passport/PassportService.java b/src/main/java/com/hiczp/bilibili/api/passport/PassportService.java index 1a308d8..b688930 100644 --- a/src/main/java/com/hiczp/bilibili/api/passport/PassportService.java +++ b/src/main/java/com/hiczp/bilibili/api/passport/PassportService.java @@ -12,7 +12,6 @@ import retrofit2.http.POST; import retrofit2.http.Query; import javax.annotation.Nonnull; -import javax.annotation.Nullable; public interface PassportService { //获取验证码 @@ -50,9 +49,4 @@ public interface PassportService { @POST("api/oauth2/revoke") Call logout(@Query("access_token") String accessToken); - - //TODO sso 尚不明确 - @Deprecated - @GET("api/login/sso") - Call sso(@Query("access_token") String accessToken, @Nullable @Query("gourl") String goUrl); } diff --git a/src/main/java/com/hiczp/bilibili/api/passport/SsoService.java b/src/main/java/com/hiczp/bilibili/api/passport/SsoService.java new file mode 100644 index 0000000..51990d9 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/passport/SsoService.java @@ -0,0 +1,15 @@ +package com.hiczp.bilibili.api.passport; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + +import javax.annotation.Nullable; + +//sso 很特别, 它可能返回的是一个 HTML 页面, 所以单独分出来 +//sso 会经过两次 302 跳转, 需要保存其中的 cookie, 然后才能抵达最终页面并且进入 cookie 登录状态 +public interface SsoService { + @GET("api/login/sso") + Call sso(@Nullable @Query("gourl") String goUrl); +} diff --git a/src/test/java/com/hiczp/bilibili/api/test/SsoTest.java b/src/test/java/com/hiczp/bilibili/api/test/SsoTest.java index 2c730d6..f2ce613 100644 --- a/src/test/java/com/hiczp/bilibili/api/test/SsoTest.java +++ b/src/test/java/com/hiczp/bilibili/api/test/SsoTest.java @@ -1,25 +1,28 @@ package com.hiczp.bilibili.api.test; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.hiczp.bilibili.api.BilibiliAPI; -import org.junit.Ignore; +import okhttp3.Cookie; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.Map; + public class SsoTest { - private static final Logger LOGGER = LoggerFactory.getLogger(UserInfoTest.class); - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Logger LOGGER = LoggerFactory.getLogger(SsoTest.class); private static final BilibiliAPI BILIBILI_API = Config.getBilibiliAPI(); - @Ignore @Test public void test() throws Exception { -// Object object = BILIBILI_API.getPassportService() -// .sso(BILIBILI_API.getBilibiliAccount().getAccessToken(), null) -// .execute() -// .body(); -// LOGGER.info("{}", object.toString()); + Map> cookiesMap = BILIBILI_API.toCookies(); + StringBuilder stringBuilder = new StringBuilder(); + cookiesMap.forEach((domain, cookies) -> { + stringBuilder.append("domain: ").append(domain).append("\n"); + cookies.forEach(cookie -> + stringBuilder.append("\t").append(cookie.name()).append("=").append(cookie.value()).append("\n") + ); + }); + LOGGER.info("Cookies below: \n{}", stringBuilder.toString()); } }