513 lines
18 KiB
Java
513 lines
18 KiB
Java
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;
|
||
}
|
||
|
||
}
|