Files
emo-grab/src/main/java/quant/rich/emoney/entity/config/EmoneyRequestConfig.java

513 lines
18 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package quant.rich.emoney.entity.config;
import java.time.LocalDateTime;
import java.util.Objects;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.springframework.beans.factory.annotation.Autowired;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo;
import quant.rich.emoney.interfaces.ConfigInfo;
import quant.rich.emoney.interfaces.IConfig;
import quant.rich.emoney.patch.okhttp.PatchOkHttp;
import quant.rich.emoney.util.EncryptUtils;
import quant.rich.emoney.util.SpringContextHolder;
import quant.rich.emoney.util.TextUtils;
import quant.rich.emoney.validator.EmoneyRequestConfigValid;
/**
* 用于配置请求时的请求行为一般而言请求头与安卓系统的信息有关build.prop
* 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险
* @see DeviceInfoConfig
* @see AndroidSdkLevelConfig
* @see ChromeVersionsConfig
*/
@Data
@Accessors(chain = true)
@Slf4j
@EmoneyRequestConfigValid
@ConfigInfo(field = "emoneyRequest", name = "益盟请求设置", initDefault = true)
public class EmoneyRequestConfig implements IConfig<EmoneyRequestConfig> {
/**
* 是否匿名登录
*/
private Boolean isAnonymous = true;
/**
* 非匿名登录时的用户名
*/
private String username = "";
/**
* 非匿名登录时的密码
*/
private String password = "";
/**
* 鉴权信息
*/
private String authorization;
/**
* 当前 authorization 更新时间
*/
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime authorizationUpdateTime;
/**
* <b>用于:</b><ul>
* <li>益盟登录接口 <code><i>guid</i> = MD5(<b>androidId</b>)</code></li>
* <li>益盟登录接口 <code><i>exIdentify.AndroidID</i> = <b>androidId</b></code></li>
* </ul>
* <b>来源:</b><br>本例随机生成并管理,需要符合 16 位
*
*/
private String androidId = TextUtils.randomString("abcdef0123456789", 16);
/**
* <b>用于:</b><ul>
* <li>Webview <code><i>User-Agent</i></li>
* <li>Non-Webview Image <code><i>User-Agent</i></code></li>
* </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b></code>
* @see DeviceInfoConfig
*/
@Setter(AccessLevel.PRIVATE)
private String androidVersion;
/**
* <b>用于:</b><ul>
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> = EMAPP/{<b>emoneyVersion</b>}(Android;{<b>androidSdkLevel</b>})</code></li>
* <li>益盟登录接口 <code><i>osVersion</i> = <b>androidSdkLevel</b></code></li>
* </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 经由 <code><b>AndroidSdkLevelConfig</b></code> 转换,由本例代管</code>
* @see DeviceInfoConfig
* @see AndroidSdkLevelConfig
*/
@Setter(AccessLevel.PRIVATE)
private String androidSdkLevel;
/**
* <b>用于:</b><ul>
* <li>益盟登录接口 <code><i>softwareType</i> = <b>softwareType</b></code></li>
* </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b>,由本例代管</code>
* @see DeviceInfoConfig
*/
private String softwareType;
/**
* <b>用于:</b><ul>
* <li>益盟通讯接口请求头 <code><i>User-Agent</i> = <b>okHttpUserAgent</b></code></li>
* </ul>
* 一般由程序所使用的 OkHttp 版本决定<br>
* <b>来源:</b>本例管理
*/
private String okHttpUserAgent = "okhttp/3.12.2";
/**
* 对应 build.prop 中 Build.MODEL, <b>用于:</b><ul>
* <li>WebView <code><i>User-Agent</i></code></li>
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
* </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
* @see DeviceInfoConfig
*/
private String deviceName;
/**
* 对应 build.prop 中 Build.FINGERPRINT, <b>用于:</b><ul>
* <li>益盟登录接口 <code><i>hardware</i> = MD5(<b>fingerprint</b>)</code></li>
* <li>益盟登录接口 <code><i>exIdentify.OSFingerPrint</i> = <b>fingerprint</b></code></li>
* </ul>
* <font color="red">注意最终生成的 exIdentify 是 jsonString, 且有对斜杠的转义</font><br>
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
* @see DeviceInfoConfig
*
*/
private String fingerprint;
/**
* 对应 build.prop 中 Build.ID, <b>用于:</b><ul>
* <li>WebView <code><i>User-Agent</i></code></li>
* <li>非 WebView 图片<code><i>User-Agent</i></code></li>
* </ul>
* <b>来源:</b><code><b>DeviceInfoConfig</b>, 由本例代为管理
* @see DeviceInfoConfig
*
*/
private String buildId;
/**
* <b>用于:</b><ul>
* <li>WebView <code><i>User-Agent</i></code></li>
* </ul>
* <b>来源:</b><code><b>ChromeVersionsConfig</b>, 由本例代为管理
* @see ChromeVersionsConfig
*/
private String chromeVersion;
/**
* <b>用于:</b><ul>
* <li>益盟通讯接口请求头 <code><i>X-Android-Agent</i> =
* EMAPP/{<b>emoneyVersion</b>}(Android;{androidSdkLevel})</code></li>
* </ul>
* 由程序版本决定<br>
* <b>来源:</b>本例管理
* @see EmoneyRequestConfig.androidSdkLevel
*/
private String emoneyVersion = "5.8.1";
/**
* <b>用于:</b><ul>
* <li>益盟通讯接口请求头 <code><i>Emapp-ViewMode</i> = <b>emappViewMode</b></code></li>
* </ul>
* 由程序决定, 一般默认为 "1"<br>
* <b>来源:</b>本例管理
*/
private String emappViewMode = "1";
@Getter(AccessLevel.PRIVATE)
@Autowired
private AndroidSdkLevelConfig androidSdkLevelConfig;
@Getter(AccessLevel.PRIVATE)
@Autowired
private DeviceInfoConfig deviceInfoConfig;
@Getter(AccessLevel.PRIVATE)
@Autowired
private ChromeVersionsConfig chromeVersionsConfig;
public void afterBeanInit() {
try {
androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class));
deviceInfoConfig = Objects.requireNonNullElseGet(deviceInfoConfig, () -> SpringContextHolder.getBean(DeviceInfoConfig.class));
chromeVersionsConfig = Objects.requireNonNullElseGet(chromeVersionsConfig, () -> SpringContextHolder.getBean(ChromeVersionsConfig.class));
}
catch (IllegalStateException e) {
log.debug("SpringContext not ready");
}
if (ObjectUtils.anyNull(fingerprint, buildId, deviceName, androidVersion, androidSdkLevel, softwareType)) {
// 任意是 null 的都要统一由 deviceInfo 进行设置
initFromRandomDeviceInfo();
}
else {
// 都不是 null则由 fingerprint 来检查各项
// model 和 softwareType 本应交由 deviceInfoConfig 检查以
// 应对可能的通过修改本地 json 来进行攻击的方式,可是本身
// deviceInfoConfig 对 model 和 softwareType 的信息也来源
// 于本地,万一本地的 deviceInfo.(fallback.)json 也不值得信任?
// 所以只检查 fingerprint
DeviceInfo deviceInfo;
boolean valid = true;
try {
deviceInfo = DeviceInfo.from(null, fingerprint);
Validate.validState(androidVersion.equals(
deviceInfo.getVersionRelease()),
"androidVersion(versionRelease) doesn't match");
Validate.validState(androidSdkLevel.equals(
String.valueOf(androidSdkLevelConfig.getSdkLevel(deviceInfo.getVersionRelease()))),
"androidSdkLevel doesn't match");
Validate.validState(buildId.equals(deviceInfo.getBuildId()),
"buildId doesn't match");
}
catch (Exception e) {
valid = false;
}
if (!valid) {
initFromRandomDeviceInfo();
}
}
if (chromeVersion == null) {
chromeVersion = chromeVersionsConfig.getRandomChromeVersion();
}
// 注入 OkHttp
PatchOkHttp.apply(
r -> r
.hostEndsWith("emoney.cn")
.or(a -> a.hostContains("emapp"))
.or(b -> b.hasHeaderName("X-Protocol-Id"))
.overrideIf("User-Agent", getOkHttpUserAgent()));
}
/**
* 从随机 deviceInfo 填充本例相关字段
* @return
*/
private EmoneyRequestConfig initFromRandomDeviceInfo() {
DeviceInfo deviceInfo = deviceInfoConfig.getRandomDeviceInfo();
// 更新 deviceInfo 后对应 androidId 也要修改,哪怕原来非空
androidId = TextUtils.randomString("abcdef0123456789", 16);
return initFromDeviceInfo(deviceInfo);
}
/**
* 从指定 deviceInfo 填充本例相关字段
* @param deviceInfo
* @return
*/
private EmoneyRequestConfig initFromDeviceInfo(DeviceInfo deviceInfo) {
if (deviceInfo == null) {
log.error("deviceInfo is null");
RuntimeException e = new RuntimeException("deviceInfo is null");
e.printStackTrace();
throw e;
}
deviceName = deviceInfo.getModel();
androidVersion = deviceInfo.getVersionRelease();
androidSdkLevel = String.valueOf(androidSdkLevelConfig.getSdkLevel(androidVersion));
softwareType = deviceInfo.getDeviceType();
fingerprint = deviceInfo.getFingerprint();
buildId = deviceInfo.getBuildId();
return this;
}
public EmoneyRequestConfig() {}
public EmoneyRequestConfig setFingerprint(String fingerprint) {
// 进入前即便 androidSdkLevelConfig 为 null 也要尝试获取一下
// 因为为 null 时不一定是程序初始化时,也有可能是从前端 Post 而来的
try {
androidSdkLevelConfig = Objects.requireNonNullElseGet(androidSdkLevelConfig, () -> SpringContextHolder.getBean(AndroidSdkLevelConfig.class));
}
catch (IllegalStateException e) {
log.debug("SpringContext not ready");
}
if (ObjectUtils.allNotNull(deviceName, softwareType, fingerprint, androidSdkLevelConfig)) {
DeviceInfo deviceInfo = DeviceInfo.from(deviceName, fingerprint, softwareType);
initFromDeviceInfo(deviceInfo);
}
else {
this.fingerprint = fingerprint;
}
return this;
}
/**
* 根据当前配置获取 guid用于益盟登录接口
* @return
*/
@JsonIgnore
public String getGuid() {
return EncryptUtils.toMD5String(androidId);
}
/**
* 一般 Protobuf 请求 X-Android-Agent 头,由 emoneyVersion 和 androidSdkLevel 组成
* @return
*/
@JsonIgnore
public String getXAndroidAgent() {
// EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
return
new StringBuilder()
.append("EMAPP/")
.append(getEmoneyVersion())
.append("(Android;")
.append(getAndroidSdkLevel())
.append(")").toString();
}
/**
* 用于 App 内用到 Webview 的地方
* @return
*/
@JsonIgnore
public String getWebviewUserAgent() {
return new StringBuilder()
.append("Mozilla/5.0 (Linux; Android ")
.append(getAndroidVersion())
.append("; ")
.append(getDeviceName())
.append(" Build/")
.append(getBuildId())
.append("; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/")
.append(getChromeVersion())
.append(" Mobile Safari/537.36")
.toString();
}
/**
* 用于 App 内少量未用到 Webview 的地方,如首页获取图片等
* @return
*/
@JsonIgnore
public String getNonWebviewResourceUserAgent() {
// Dalvik/2.1.0 (Linux; U; Android {安卓版本};{Build.DEVICE} Build/{Build.ID})
return new StringBuilder()
.append("Dalvik/2.1.0 (Linux; U; Android ")
.append(getAndroidVersion())
.append(";")
.append(getDeviceName())
.append(" Build/")
.append(getBuildId())
.append(")")
.toString();
}
/**
* 根据当前配置获取 hardware用于益盟登录接口
* @return
*/
@JsonIgnore
public String getHardware() {
return EncryptUtils.toMD5String(getFingerprint());
}
/**
* 根据本例信息(包括保存的用户名和密码)生成一个用于登录的 ObjectNode
* @return
*/
@JsonIgnore
public ObjectNode getUsernamePasswordLoginObject() {
return getUsernamePasswordLoginObject(username, password);
}
/**
* 根据指定用户名、密码和本例信息生成一个用于登录的 ObjectNode
* @param username 用户名
* @param password 密码(可以是加密过的,也可以是明文)
* @return
*/
public ObjectNode getUsernamePasswordLoginObject(String username, String password) {
if (StringUtils.isAnyBlank(username, password)) {
throw new RuntimeException("Try to generate a emoney login object but username and/or password is blank");
}
ObjectNode node = getAnonymousLoginObject();
node.put("accId", username);
node.put("accType", 1);
// 尝试解密 password 看是否成功,如果成功说明原本就已经是加密了的
String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
node.put("pwd",
tryDecryptPassword != null ? password :
EncryptUtils.encryptAesForEmoneyPassword(password)
);
return node;
}
/**
* 根据本例信息生成一个用于匿名登录的 ObjectNode
* @return
*/
@JsonIgnore
public ObjectNode getAnonymousLoginObject() {
ObjectMapper mapper = new ObjectMapper();
ObjectNode node = mapper.createObjectNode();
ObjectNode exIdentify = mapper.createObjectNode();
exIdentify.put("IMEI", "");
exIdentify.put("AndroidID", getAndroidId());
exIdentify.put("MAC", "");
exIdentify.put("OSFingerPrint", getFingerprint());
String exIdentifyString = exIdentify.toString().replace("/", "\\/");
// 这玩意最好按照顺序来,当前这个顺序是 5.8.1 的顺序
String guid = getGuid();
node.put("appVersion", getEmoneyVersion());
node.put("productId", 4);
node.put("softwareType", getSoftwareType());
node.put("deviceName", getDeviceName());
node.put("ssid", "0");
node.put("platform", "android");
node.put("exIdentify", exIdentifyString);
node.put("osVersion", getAndroidSdkLevel());
node.put("accId", guid);
node.put("guid", guid);
node.put("accType", 4);
node.put("pwd", "");
node.put("channelId", "1711");
node.put("hardware", getHardware());
return node;
}
/**
* 设置密码:<ul>
* <li>null or empty保存空字符串</li>
* <li>尝试解密成功,说明是密文,直接保存</li>
* <li>尝试解密失败,说明是明文,加密保存</li>
* </ul>
* @param password
* @return
*/
public EmoneyRequestConfig setPassword(String password) {
if (StringUtils.isEmpty(password)) {
this.password = "";
return this;
}
String tryDecryptPassword = EncryptUtils.decryptAesForEmoneyPassword(password);
if (tryDecryptPassword != null) {
this.password = password;
}
else {
this.password = EncryptUtils.encryptAesForEmoneyPassword(password);
}
return this;
}
/**
* 确保 androidVersion/androidSdkLevel 不为 null
*/
public EmoneyRequestConfig beforeSaving() {
setFingerprint(this.fingerprint);
return this;
}
/**
* 触发更新 Authorization 的更新时间为现在
* @return
*/
public EmoneyRequestConfig updateAuthorizationTime() {
this.authorizationUpdateTime = LocalDateTime.now();
return this;
}
public EmoneyRequestConfig mergeTo(EmoneyRequestConfig other) {
boolean authorizationUpdated = !Objects.equals(other.authorization, this.authorization);
IConfig.super.mergeTo(other);
if (authorizationUpdated) {
other.updateAuthorizationTime();
}
return other;
}
public EmoneyRequestConfig mergeFrom(EmoneyRequestConfig other) {
boolean authorizationUpdated = !Objects.equals(other.authorization, this.authorization);
IConfig.super.mergeFrom(other);
if (authorizationUpdated) {
this.updateAuthorizationTime();
}
return this;
}
}