优化计算 sign 的过程

This commit is contained in:
czp 2018-02-19 01:32:10 +08:00
parent 7b99019b6e
commit 6575e8d736
8 changed files with 138 additions and 53 deletions

View File

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

View File

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

View File

@ -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;
//今天所有的宝箱已经领完

View File

@ -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<String> nameAndValues) {
return nameAndValues.stream().collect(Collectors.joining("&"));
}
//排序 params 并计算 sign
//传入值为 name1=value1 形式
private String calculateSign(List<String> 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);
}
}
}

View File

@ -25,6 +25,7 @@ public interface LiveService {
@GET("AppRoom/index")
Call<LiveRoomInfoEntity> getRoomInfo(@Query("room_id") long roomId);
//未登录时返回 401
@POST("feed/v1/feed/isFollowed")
Call<IsFollowedResponseEntity> isFollowed(@Query("follow") long hostUserId);
@ -43,10 +44,11 @@ public interface LiveService {
@GET("appUser/getTitle")
Call<TitlesEntity> getTitle();
//这个 API 不是很明确, 所有房间都一样
//这个 API 不是很明确, 所有房间都一样, 可能和什么活动有关
@GET("SpecialGift/room/{roomId}")
Call<SpecialGiftEntity> getSpecialGift(@Path("roomId") long roomId);
//未登录时返回 3
@GET("mobile/getUser")
Call<UserInfoEntity> getUserInfo();
@ -59,6 +61,7 @@ public interface LiveService {
return getPlayUrl(cid, "json");
}
//未登录时返回 3
@POST("mobile/userOnlineHeart")
@FormUrlEncoded
Call<SendOnlineHeartResponseEntity> sendOnlineHeart(@Field("room_id") long roomId, @Field("scale") String scale);

View File

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

View File

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

View File

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