Compare commits

..

4 Commits

Author SHA1 Message Date
Doghole 2869994387 优化 RequestInfo 相关操作:
1. RequestInfoControllerV1 save 接口对 RequestInfo 参数做 @Valid 校验;
2. RequestInfoControllerV1 去除批量操作中的置匿名/去匿名,因为对于 RequestInfo
而言,当前已无字段标记匿名与否,仅凭是否配置用户名和密码来判断是否为匿名;
3. RequestInfoService 查找不到默认配置时创建 new RequestInfo(),虽数据表有触发器置 isDefault
= true,但此处仍手动 set,确保一致性;
4. RequestInfo 一些依赖 DeviceInfo 的字段,不做冗余,直接从 DeviceInfo <-
fingerprint/deviceName/softwareType 恢复。
2026-06-09 16:23:33 +08:00
Doghole 287b124f7e 修订 loginPage 失败降级,改为指定 EndPoint 2026-06-08 17:42:14 +08:00
Doghole ae4599cd9a 添加注释,添加“清除默认IpInfo缓存”的功能,并在修改默认代理后调用 2026-06-08 17:40:29 +08:00
Doghole 10598a51cf 不再需要 @Async 2026-06-08 17:36:49 +08:00
14 changed files with 133 additions and 105 deletions
@@ -8,7 +8,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* 注解在方法上以获取对 EmoneyProtocol 的额外操作
* 注解在方法上以获取对 Emoney Protocol 的额外操作
* <p>
* 目前方法必须是受 spring 管理类下的公共成员方法,且接受参数为单参数 JsonNode.
* <p>
@@ -21,18 +21,15 @@ import java.lang.annotation.Target;
@Target(METHOD)
public @interface ResponseDecodeExtension {
/**
* 指定的 protocolId
* @return
* @return 指定的 protocolId
*/
String protocolId();
/**
* inspect 的排序
* @return
* @return inspect 的排序
*/
int order() default -1;
/**
* 是否在模拟客户端请求和返回时启用, 默认 true
* @return
* @return 是否在模拟客户端请求和返回时启用, 默认 true
*/
boolean client() default true;
}
@@ -8,10 +8,13 @@ import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import lombok.RequiredArgsConstructor;
import quant.rich.emoney.service.AuthService;
@@ -27,10 +30,10 @@ public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.cacheControl(cache -> cache.disable())
.frameOptions(f -> f.sameOrigin())
.cacheControl(HeadersConfigurer.CacheControlConfig::disable)
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
)
.csrf(csrf -> csrf.disable())
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/favicon.ico").permitAll()
.requestMatchers("/admin/*/login").permitAll()
@@ -41,11 +44,8 @@ public class SecurityConfig {
.requestMatchers("/img/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form // 开启表单登录,并指定登录页
.loginPage("/admin/v1/login") // 指定登录页
.loginProcessingUrl("/admin/v1/doLogin") // 处理登录请求的 URL
.defaultSuccessUrl("/admin/v1/", false) // 登录成功后默认跳转
.permitAll())
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/admin/v1/login")))
.logout(logout -> logout
.logoutUrl("/admin/v1/logout")
.logoutSuccessUrl("/admin/v1/login")
@@ -54,7 +54,6 @@ public class SecurityConfig {
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
;
return http.build();
}
@@ -50,19 +50,27 @@ public class ProxySettingControllerV1 extends UpdateBoolServiceController<ProxyS
@PostMapping("/save")
@ResponseBody
public R<?> save(@RequestBody ProxySetting proxySetting) {
return super.save(proxySetting);
R<?> result = super.save(proxySetting);
proxySettingService.clearIpInfoCache();
return result;
}
@PostMapping("/delete")
@ResponseBody
public R<?> delete(String id) {
return super.delete(id);
R<?> result = super.delete(id);
proxySettingService.clearIpInfoCache();
return result;
}
@Override
protected
R<?> updateBool(String id, String field, Boolean value) {
return super.updateBool(id, field, value);
R<?> result = super.updateBool(id, field, value);
if ("isDefault".equals(field)) {
proxySettingService.clearIpInfoCache();
}
return result;
}
@PostMapping("/batchOp")
@@ -80,8 +88,10 @@ public class ProxySettingControllerV1 extends UpdateBoolServiceController<ProxyS
throw RException.badRequest("操作类型不能为空");
}
else if (ProxySettingBatchOp.DELETE == op) {
return R.judge(
R<?> result = R.judge(
proxySettingService.removeBatchByIds(idArray));
proxySettingService.clearIpInfoCache();
return result;
}
LambdaUpdateWrapper<ProxySetting> uw = new LambdaUpdateWrapper<>();
@@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
@@ -53,7 +52,7 @@ public class RequestInfoControllerV1 extends UpdateBoolServiceController<Request
@PostMapping("/save")
@ResponseBody
public R<?> save(@RequestBody @NonNull RequestInfo requestInfo) {
public R<?> save(@RequestBody @Valid @NonNull RequestInfo requestInfo) {
return super.save(requestInfo);
}
@@ -81,18 +80,11 @@ public class RequestInfoControllerV1 extends UpdateBoolServiceController<Request
return R.judge(getThisService().removeByIds(idArray));
}
LambdaUpdateWrapper<RequestInfo> uw = new LambdaUpdateWrapper<>();
uw.in(RequestInfo::getId, idArray);
switch (op) {
default:
throw RException.badRequest("未知操作");
}
throw RException.badRequest("未知操作");
}
private static enum RequestInfoBatchOp {
DELETE,
ENABLE_ANONYMOUS,
DISABLE_ANONYMOUS
DELETE
}
}
@@ -16,6 +16,13 @@ import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import quant.rich.emoney.validator.ProxySettingValid;
/**
* 平台代理配置。
* <p>
* SQLite 表 proxy_setting 使用触发器维护默认代理的一致性:新增或更新 is_default = 1
* 时会把其他记录置为非默认;不允许把最后一条默认代理改为非默认,也不允许删除默认代理。
* 这部分约束不在 Java 代码里重复实现,调整表结构或导入数据时需要同步检查触发器。
*/
@Data
@Accessors(chain = true)
@NoArgsConstructor
@@ -4,7 +4,6 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
@@ -12,10 +11,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import quant.rich.emoney.entity.config.AndroidSdkLevelConfig;
@@ -28,11 +25,21 @@ import quant.rich.emoney.util.TextUtils;
import quant.rich.emoney.validator.RequestInfoValid;
/**
* 用于配置请求时的请求行为,一般而言,请求头与安卓系统的信息有关(build.prop)
* 虽然部分请求对应服务器可能不进行审核,但合理的请求头能尽可能模仿真机行为,避免风险
* <b>请求信息</b>
* <p>
* 用于配置请求时的参数,包括请求头等。若使用该请求头登录成功,则将会存储鉴权信息。
*
* 一般而言,请求头与系统的信息有关(build.prop)
* 虽然服务器可能不进行请求头审核,但合理的请求头能尽可能模仿真机行为,避免风险。
* <p>
* SQLite 表 request_info 使用触发器维护默认请求配置的一致性:新增或更新 is_default = 1
* 时会把其他记录置为非默认;不允许把最后一条默认请求配置改为非默认,也不允许删除默认请求配置。
* 这部分约束不在 Java 代码里重复实现,调整表结构或导入数据时需要同步检查触发器。
*
* @see DeviceInfoConfig
* @see AndroidSdkLevelConfig
* @see ChromeVersionsConfig
* @See RequestInfoValidator
*/
@Data
@EqualsAndHashCode(callSuper=false)
@@ -109,9 +116,10 @@ public class RequestInfo extends Model<RequestInfo> {
* @see DeviceInfoConfig
* @see DeviceInfo
*/
@Setter(AccessLevel.PRIVATE)
@TableField(exist=false)
private String androidVersion;
public String getAndroidVersion() {
DeviceInfo deviceInfo = getDeviceInfo();
return deviceInfo == null ? null : deviceInfo.getVersionRelease();
}
/**
* <b>用于:</b><ul>
@@ -123,9 +131,13 @@ public class RequestInfo extends Model<RequestInfo> {
* @see DeviceInfo
* @see AndroidSdkLevelConfig
*/
@Setter(AccessLevel.PRIVATE)
@TableField(exist=false)
private String androidSdkLevel;
public String getAndroidSdkLevel() {
String androidVersion = getAndroidVersion();
if (StringUtils.isBlank(androidVersion)) {
return null;
}
return String.valueOf(androidSdkLevelConfig.getSdkLevel(androidVersion));
}
/**
* <b>用于:</b><ul>
@@ -179,8 +191,10 @@ public class RequestInfo extends Model<RequestInfo> {
* @see DeviceInfoConfig
*
*/
@TableField(exist=false)
private String buildId;
public String getBuildId() {
DeviceInfo deviceInfo = getDeviceInfo();
return deviceInfo == null ? null : deviceInfo.getBuildId();
}
/**
* <b>用于:</b><ul>
@@ -214,21 +228,29 @@ public class RequestInfo extends Model<RequestInfo> {
/**
* 从 deviceInfo 设置相关字段
* @param deviceInfo
* @return
* @return 更新过的 RequestInfo 本身
*/
public RequestInfo setRelativeFieldsFromDeviceInfo(DeviceInfo deviceInfo) {
if (deviceInfo == null) {
throw new NullPointerException("deviceInfo is null");
}
deviceName = deviceInfo.getModel();
androidVersion = deviceInfo.getVersionRelease();
androidSdkLevel = String.valueOf(androidSdkLevelConfig.getSdkLevel(androidVersion));
softwareType = deviceInfo.getDeviceType();
fingerprint = deviceInfo.getFingerprint();
buildId = deviceInfo.getBuildId();
return this;
}
/**
* @return 获取由相应字段指定的 DeviceInfo
*/
@JsonIgnore
private DeviceInfo getDeviceInfo() {
if (ObjectUtils.anyNull(deviceName, fingerprint, softwareType)) {
return null;
}
return DeviceInfo.from(deviceName, fingerprint, softwareType);
}
/**
* 设置密码:<ul>
* <li>null or empty,保存空字符串</li>
@@ -260,13 +282,7 @@ public class RequestInfo extends Model<RequestInfo> {
* @see RequestInfo#setRelativeFieldsFromDeviceInfo
*/
public RequestInfo setFingerprint(String fingerprint) {
if (ObjectUtils.allNotNull(deviceName, softwareType, fingerprint, androidSdkLevelConfig)) {
DeviceInfo deviceInfo = DeviceInfo.from(deviceName, fingerprint, softwareType);
setRelativeFieldsFromDeviceInfo(deviceInfo);
}
else {
this.fingerprint = fingerprint;
}
this.fingerprint = fingerprint;
return this;
}
@@ -11,6 +11,9 @@ import lombok.experimental.Accessors;
/**
* 益盟策略和策略池
* <p>
* SQLite 表 strategy_and_pool 对 pool_id 设置了 UNIQUE_POOL_ID 唯一约束,并使用
* ON CONFLICT REPLACE。重复写入同一 pool_id 时会由 SQLite 替换旧记录。
*/
@Data
@Accessors(chain=true)
@@ -41,4 +41,8 @@ public class ProxySettingService extends SqliteServiceImpl<ProxySettingMapper, P
return ipInfo;
}
public void clearIpInfoCache() {
ipInfo = null;
}
}
@@ -44,6 +44,7 @@ public class RequestInfoService extends SqliteServiceImpl<RequestInfoMapper, Req
);
if (requestInfo == null) {
requestInfo = new RequestInfo();
requestInfo.setIsDefault(true);
save(requestInfo);
}
return requestInfo;
@@ -18,7 +18,6 @@ import java.net.InetAddress;
import java.net.Proxy;
import java.util.concurrent.TimeUnit;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.Async;
@Slf4j
public class GeoIPUtil {
@@ -35,7 +34,6 @@ public class GeoIPUtil {
}
}
@Async
public static IpInfo getIpInfoThroughProxy(ProxySetting proxySetting) {
if (proxySetting == null) {
throw new RuntimeException("代理为空");
@@ -66,7 +64,7 @@ public class GeoIPUtil {
String responseBody = response.body().string();
log.debug("Response ipv4 from proxy: {}", responseBody.trim());
ipInfo.setIp(responseBody);
} catch (IOException e) {
} catch (Exception e) {
log.warn("Proxy ipv4 error: {}", e.getMessage());
return IpInfo.EMPTY;
}
@@ -81,7 +79,7 @@ public class GeoIPUtil {
String responseBody = response.body().string();
log.debug("Response ipv6 from proxy: {}", responseBody.trim());
ipInfo.setIpv6(responseBody);
} catch (IOException e) {
} catch (Exception e) {
log.warn("Proxy ipv6 error {}", e.getMessage());
}
return queryIpInfoGeoLite(ipInfo);
@@ -104,7 +102,7 @@ public class GeoIPUtil {
if (response.getSubdivisions() != null && !response.getSubdivisions().isEmpty()) {
// 省
Subdivision subdivision = response.getSubdivisions().get(0);
Subdivision subdivision = response.getSubdivisions().getFirst();
if (subdivision != null) {
String subdivisionStr = subdivision.getNames().get(LOCALE);
ipInfo.setSubdivision(subdivisionStr);
@@ -14,7 +14,7 @@ import jakarta.validation.Payload;
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestInfoValid {
String message() default "非法的 EmoneyRequestConfig";
String message() default "非法的 RequestInfo";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@@ -10,6 +10,9 @@ import quant.rich.emoney.entity.config.DeviceInfoConfig;
import quant.rich.emoney.entity.sqlite.RequestInfo;
import quant.rich.emoney.interfaces.IValidator;
/**
* RequestInfo 校验器
*/
public class RequestInfoValidator implements IValidator, ConstraintValidator<RequestInfoValid, RequestInfo> {
static final Pattern androidIdPattern = Pattern.compile("^[0-9a-f]{16}$");
@@ -158,7 +158,7 @@
}
else {
$(non).children('label').children('span').remove();
$(non).find('lay-verify').removeAttr('lay-verify');
$(non).find('[lay-verify]').removeAttr('lay-verify');
}
});
});
@@ -58,9 +58,7 @@
Helper.renderDropdown({
elem: '.operdown',
data: [
{title: '删除', op: 'DELETE'},
{title: '开启匿名', op: 'ENABLE_ANONYMOUS'},
{title: '关闭匿名', op: 'DISABLE_ANONYMOUS'}],
{title: '删除', op: 'DELETE'}],
tableFilter: 'requestInfos',
url: '/admin/v1/manage/requestInfo/batchOp',
idName: 'id'