diff --git a/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java b/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java index dd8e8a7..f18a52d 100644 --- a/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java +++ b/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java @@ -139,6 +139,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider ServerErrorCode.Common.UNAUTHORIZED, ServerErrorCode.Live.USER_NO_LOGIN, ServerErrorCode.Live.PLEASE_LOGIN, + ServerErrorCode.Live.PLEASE_LOGIN0, ServerErrorCode.Live.NO_LOGIN )) .addInterceptor(new AddAccessKeyInterceptor(bilibiliAccount)) diff --git a/src/main/java/com/hiczp/bilibili/api/BilibiliSecurityHelper.java b/src/main/java/com/hiczp/bilibili/api/BilibiliSecurityHelper.java index 74a15c9..d5c150c 100644 --- a/src/main/java/com/hiczp/bilibili/api/BilibiliSecurityHelper.java +++ b/src/main/java/com/hiczp/bilibili/api/BilibiliSecurityHelper.java @@ -5,24 +5,25 @@ 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 javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import java.io.IOException; -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; +import java.math.BigInteger; +import java.security.*; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.Base64; +import java.util.List; import java.util.stream.Collectors; public class BilibiliSecurityHelper { - private static String cipherPassword(BilibiliServiceProvider bilibiliServiceProvider, - String password) throws IOException { + private static String cipherPassword(@Nonnull BilibiliServiceProvider bilibiliServiceProvider, + @Nonnull String password) throws IOException { KeyEntity keyEntity = bilibiliServiceProvider.getPassportService().getKey().execute().body(); //服务器返回异常错误码 if (keyEntity.getCode() != 0) { @@ -60,17 +61,49 @@ public class BilibiliSecurityHelper { return cipheredPassword; } - public static LoginResponseEntity login(BilibiliServiceProvider bilibiliServiceProvider, - String username, - String password) throws IOException { + //计算 sign + //传入值为 name1=value1 形式 + //传入值必须已经排序 + //value 必须已经 URLEncode + public static String calculateSign(@Nonnull List nameAndValues, @Nonnull String appSecret) { + return calculateSign(nameAndValues.stream().collect(Collectors.joining("&")), appSecret); + } + + public static String calculateSign(@Nonnull String encodedQuery, @Nonnull String appSecret) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + messageDigest.update((encodedQuery + appSecret).getBytes()); + String md5 = new BigInteger(1, messageDigest.digest()).toString(16); + //md5 不满 32 位时左边加 0 + return ("00000000000000000000000000000000" + md5).substring(md5.length()); + } catch (NoSuchAlgorithmException e) { + throw new Error(e); + } + } + + //直接生成添加了 sign 的 query + //传入值为 name1=value1 形式 + //传入值必须已经排序 + //value 必须已经 URLEncode + public static String addSignToQuery(@Nonnull List nameAndValues, @Nonnull String appSecret) { + return addSignToQuery(nameAndValues.stream().collect(Collectors.joining("&")), appSecret); + } + + public static String addSignToQuery(@Nonnull String encodedQuery, @Nonnull String appSecret) { + return encodedQuery + String.format("&%s=%s", "sign", calculateSign(encodedQuery, appSecret)); + } + + public static LoginResponseEntity login(@Nonnull BilibiliServiceProvider bilibiliServiceProvider, + @Nonnull String username, + @Nonnull String password) throws IOException { return login(bilibiliServiceProvider, username, password, null, null); } - public static LoginResponseEntity login(BilibiliServiceProvider bilibiliServiceProvider, - String username, - String password, - String captcha, - String cookie) throws IOException { + public static LoginResponseEntity login(@Nonnull BilibiliServiceProvider bilibiliServiceProvider, + @Nonnull String username, + @Nonnull String password, + @Nullable String captcha, + @Nullable String cookie) throws IOException { return bilibiliServiceProvider.getPassportService() .login( username, @@ -81,16 +114,16 @@ public class BilibiliSecurityHelper { .body(); } - public static RefreshTokenResponseEntity refreshToken(BilibiliServiceProvider bilibiliServiceProvider, - String accessToken, - String refreshToken) throws IOException { + public static RefreshTokenResponseEntity refreshToken(@Nonnull BilibiliServiceProvider bilibiliServiceProvider, + @Nonnull String accessToken, + @Nonnull String refreshToken) throws IOException { return bilibiliServiceProvider.getPassportService().refreshToken(accessToken, refreshToken) .execute() .body(); } - public static LogoutResponseEntity logout(BilibiliServiceProvider bilibiliServiceProvider, - String accessToken) throws IOException { + public static LogoutResponseEntity logout(@Nonnull BilibiliServiceProvider bilibiliServiceProvider, + @Nonnull String accessToken) throws IOException { return bilibiliServiceProvider.getPassportService().logout(accessToken) .execute() .body(); diff --git a/src/main/java/com/hiczp/bilibili/api/ServerErrorCode.java b/src/main/java/com/hiczp/bilibili/api/ServerErrorCode.java index 45b42f7..b5aee4f 100644 --- a/src/main/java/com/hiczp/bilibili/api/ServerErrorCode.java +++ b/src/main/java/com/hiczp/bilibili/api/ServerErrorCode.java @@ -29,13 +29,18 @@ public class ServerErrorCode { } //一些 API 未登录时返回 3, 一些返回 -101, 还有一些返回 401, 在网关上鉴权的 API 返回 -401 + //甚至有一些 API 返回 32205 这种奇怪的错误码 public static class Live { //"user no login" public static final int USER_NO_LOGIN = 3; //"请登录" public static final int PLEASE_LOGIN = 401; + //"请登录" + public static final int PLEASE_LOGIN0 = 32205; //"请先登录" public static final int NO_LOGIN = -101; + //"关键字不能小于2个字节或大于50字节" + public static final int KEYWORD_CAN_NOT_LESS_THAN_2_BYTES_OR_GREATER_THAN_50_BYTES = -609; //已经领取过这个宝箱 public static final int THIS_SILVER_TASK_ALREADY_TOOK = -903; //今天所有的宝箱已经领完 diff --git a/src/main/java/com/hiczp/bilibili/api/interceptor/SortParamsAndSignInterceptor.java b/src/main/java/com/hiczp/bilibili/api/interceptor/SortParamsAndSignInterceptor.java index e21539f..74d57b9 100644 --- a/src/main/java/com/hiczp/bilibili/api/interceptor/SortParamsAndSignInterceptor.java +++ b/src/main/java/com/hiczp/bilibili/api/interceptor/SortParamsAndSignInterceptor.java @@ -1,6 +1,7 @@ package com.hiczp.bilibili.api.interceptor; import com.hiczp.bilibili.api.BilibiliClientProperties; +import com.hiczp.bilibili.api.BilibiliSecurityHelper; import okhttp3.HttpUrl; import okhttp3.Interceptor; import okhttp3.Request; @@ -8,21 +9,17 @@ import okhttp3.Response; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.math.BigInteger; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; public class SortParamsAndSignInterceptor implements Interceptor { - private BilibiliClientProperties bilibiliClientDefinition; + private BilibiliClientProperties bilibiliClientProperties; - public SortParamsAndSignInterceptor(BilibiliClientProperties bilibiliClientDefinition) { - this.bilibiliClientDefinition = bilibiliClientDefinition; + public SortParamsAndSignInterceptor(BilibiliClientProperties bilibiliClientProperties) { + this.bilibiliClientProperties = bilibiliClientProperties; } @Override @@ -43,31 +40,12 @@ public class SortParamsAndSignInterceptor implements Interceptor { ) ); Collections.sort(nameAndValues); - nameAndValues.add(String.format("%s=%s", "sign", calculateSign(nameAndValues))); return chain.proceed( request.newBuilder() .url(httpUrl.newBuilder() - .encodedQuery(generateQuery(nameAndValues)) + .encodedQuery(BilibiliSecurityHelper.addSignToQuery(nameAndValues, bilibiliClientProperties.getAppSecret())) .build() ).build() ); } - - private String generateQuery(List nameAndValues) { - return nameAndValues.stream().collect(Collectors.joining("&")); - } - - //排序 params 并计算 sign - //传入值为 name1=value1 形式 - private String calculateSign(List nameAndValues) { - try { - MessageDigest messageDigest = MessageDigest.getInstance("MD5"); - messageDigest.update((generateQuery(nameAndValues) + bilibiliClientDefinition.getAppSecret()).getBytes()); - String md5 = new BigInteger(1, messageDigest.digest()).toString(16); - //md5 不满 32 位时左边加 0 - return ("00000000000000000000000000000000" + md5).substring(md5.length()); - } catch (NoSuchAlgorithmException e) { - throw new Error(e); - } - } } diff --git a/src/main/java/com/hiczp/bilibili/api/live/LiveService.java b/src/main/java/com/hiczp/bilibili/api/live/LiveService.java index a3da90d..550fd30 100644 --- a/src/main/java/com/hiczp/bilibili/api/live/LiveService.java +++ b/src/main/java/com/hiczp/bilibili/api/live/LiveService.java @@ -25,6 +25,7 @@ public interface LiveService { @GET("AppRoom/index") Call getRoomInfo(@Query("room_id") long roomId); + //未登录时返回 401 @POST("feed/v1/feed/isFollowed") Call isFollowed(@Query("follow") long hostUserId); @@ -43,10 +44,11 @@ public interface LiveService { @GET("appUser/getTitle") Call getTitle(); - //这个 API 不是很明确, 所有房间都一样 + //这个 API 不是很明确, 所有房间都一样, 可能和什么活动有关 @GET("SpecialGift/room/{roomId}") Call getSpecialGift(@Path("roomId") long roomId); + //未登录时返回 3 @GET("mobile/getUser") Call getUserInfo(); @@ -59,6 +61,7 @@ public interface LiveService { return getPlayUrl(cid, "json"); } + //未登录时返回 3 @POST("mobile/userOnlineHeart") @FormUrlEncoded Call sendOnlineHeart(@Field("room_id") long roomId, @Field("scale") String scale); diff --git a/src/test/java/com/hiczp/bilibili/api/test/ManualLoginTool.java b/src/test/java/com/hiczp/bilibili/api/test/ManualLoginTool.java new file mode 100644 index 0000000..6868dc9 --- /dev/null +++ b/src/test/java/com/hiczp/bilibili/api/test/ManualLoginTool.java @@ -0,0 +1,19 @@ +package com.hiczp.bilibili.api.test; + +import com.google.gson.GsonBuilder; +import com.hiczp.bilibili.api.BilibiliAPI; +import com.hiczp.bilibili.api.passport.entity.LoginResponseEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ManualLoginTool { + private static final Logger LOGGER = LoggerFactory.getLogger(ManualLoginTool.class); + + public static void main(String[] args) throws Exception { + RuleSuite.init(); + Config config = Config.getInstance(); + LoginResponseEntity loginResponseEntity = new BilibiliAPI() + .login(config.getUsername(), config.getPassword()); + LOGGER.info(new GsonBuilder().setPrettyPrinting().create().toJson(loginResponseEntity)); + } +} diff --git a/src/test/java/com/hiczp/bilibili/api/test/ManualSignTool.java b/src/test/java/com/hiczp/bilibili/api/test/ManualSignTool.java new file mode 100644 index 0000000..fa5ab64 --- /dev/null +++ b/src/test/java/com/hiczp/bilibili/api/test/ManualSignTool.java @@ -0,0 +1,42 @@ +package com.hiczp.bilibili.api.test; + +import com.hiczp.bilibili.api.BilibiliClientProperties; +import com.hiczp.bilibili.api.BilibiliSecurityHelper; + +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +//Insomnia 手动测试时使用 +//拷贝入整个 url, 它将自动计算出 sign +public class ManualSignTool { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.println("Please input url"); + while (true) { + String input = scanner.nextLine().trim(); + if (input.equals("q")) { + break; + } + if (input.isEmpty()) { + continue; + } + + int index = input.indexOf("?"); + if (index == -1) { + continue; + } + + List nameAndValues = Arrays.stream(input.substring(index + 1) + .split("&")) + .filter(param -> !param.startsWith("sign=")) + .sorted() + .collect(Collectors.toList()); + + System.out.println( + BilibiliSecurityHelper.calculateSign(nameAndValues, BilibiliClientProperties.defaultSetting().getAppSecret()) + ); + } + } +} diff --git a/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java b/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java index f1033f4..26aab17 100644 --- a/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java +++ b/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java @@ -8,7 +8,10 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; import java.io.BufferedReader; -import java.io.InputStreamReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; @RunWith(Suite.class) @Suite.SuiteClasses({ @@ -30,20 +33,21 @@ public class RuleSuite { } }; - public static void init() { + public static void init() throws Exception { //初始化 slf4j BasicConfigurator.configure(); + //读取配置文件 - try { + try (BufferedReader bufferedReader = Files.newBufferedReader(Paths.get(Config.class.getResource("/config.json").toURI()))) { Config.setConfig( new Gson().fromJson( - new BufferedReader(new InputStreamReader(Config.class.getResourceAsStream("/config.json"))), + bufferedReader, Config.class ) ); - } catch (NullPointerException e) { + } catch (IOException e) { //抛出异常就可以取消测试 - throw new RuntimeException("Please create config file before tests"); + throw new FileNotFoundException("Please create config file before tests"); } } }