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