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 { /** * 是否匿名登录 */ 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; /** * 用于: * 来源:
本例随机生成并管理,需要符合 16 位 * */ private String androidId = TextUtils.randomString("abcdef0123456789", 16); /** * 用于: * 来源:DeviceInfoConfig * @see DeviceInfoConfig */ @Setter(AccessLevel.PRIVATE) private String androidVersion; /** * 用于: * 来源:DeviceInfoConfig, 经由 AndroidSdkLevelConfig 转换,由本例代管 * @see DeviceInfoConfig * @see AndroidSdkLevelConfig */ @Setter(AccessLevel.PRIVATE) private String androidSdkLevel; /** * 用于: * 来源:DeviceInfoConfig,由本例代管 * @see DeviceInfoConfig */ private String softwareType; /** * 用于: * 一般由程序所使用的 OkHttp 版本决定
* 来源:本例管理 */ private String okHttpUserAgent = "okhttp/3.12.2"; /** * 对应 build.prop 中 Build.MODEL, 用于: * 来源:DeviceInfoConfig, 由本例代为管理 * @see DeviceInfoConfig */ private String deviceName; /** * 对应 build.prop 中 Build.FINGERPRINT, 用于:
    *
  • 益盟登录接口 hardware = MD5(fingerprint)
  • *
  • 益盟登录接口 exIdentify.OSFingerPrint = fingerprint
  • *
* 注意最终生成的 exIdentify 是 jsonString, 且有对斜杠的转义
* 来源:DeviceInfoConfig, 由本例代为管理 * @see DeviceInfoConfig * */ private String fingerprint; /** * 对应 build.prop 中 Build.ID, 用于:
    *
  • WebView User-Agent
  • *
  • 非 WebView 图片User-Agent
  • *
* 来源:DeviceInfoConfig, 由本例代为管理 * @see DeviceInfoConfig * */ private String buildId; /** * 用于:
    *
  • WebView User-Agent
  • *
* 来源:ChromeVersionsConfig, 由本例代为管理 * @see ChromeVersionsConfig */ private String chromeVersion; /** * 用于:
    *
  • 益盟通讯接口请求头 X-Android-Agent = * EMAPP/{emoneyVersion}(Android;{androidSdkLevel})
  • *
* 由程序版本决定
* 来源:本例管理 * @see EmoneyRequestConfig.androidSdkLevel */ private String emoneyVersion = "5.8.1"; /** * 用于:
    *
  • 益盟通讯接口请求头 Emapp-ViewMode = emappViewMode
  • *
* 由程序决定, 一般默认为 "1"
* 来源:本例管理 */ 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; } /** * 设置密码:
    *
  • null or empty,保存空字符串
  • *
  • 尝试解密成功,说明是密文,直接保存
  • *
  • 尝试解密失败,说明是明文,加密保存
  • *
* @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; } }