You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

服务端识别唯一客户端及防恶意临时ID生成请求方案咨询

短ID生成请求的安全防护方案

1. 核心限制方案:客户端唯一标识校验

你提出的「服务器检查客户端是否已申请过未过期ID,存在则拒绝/返回旧ID」完全可行,这是解决重复请求问题的核心手段。但不推荐用IP或MAC地址作为校验标识,原因如下:

  • IP地址:多数用户处于NAT网络(家庭WiFi、公司内网),多个设备共享同一公网IP,会误拦截正常用户;且IP可通过代理轻易篡改,恶意用户能轻松绕开限制。
  • MAC地址:Android 6.0及以上系统已限制第三方APP获取真实WiFi MAC,只能返回固定占位符02:00:00:00:00:00,且MAC本身可被篡改,可靠性极低。

推荐使用以下两种客户端标识:

  • 客户端UUID:APP首次启动时生成一个UUID,存入SharedPreferences,后续所有请求均携带该UUID。优点是实现简单,缺点是用户卸载重装后标识会变更,但对你的短期ID场景影响极小。
  • Google Advertising ID:Google官方提供的设备级匿名标识,用户可主动重置,无隐私风险。需依赖Google Play服务库,适合面向全球用户的应用。

2. Spring后端实现示例

数据库表设计

CREATE TABLE device_short_ids (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    client_id VARCHAR(64) NOT NULL UNIQUE,
    short_id VARCHAR(6) NOT NULL UNIQUE,
    expire_time DATETIME NOT NULL
);

实体类与Repository

@Entity
@Table(name = "device_short_ids")
public class DeviceShortId {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String clientId;
    
    @Column(unique = true, nullable = false)
    private String shortId;
    
    @Column(nullable = false)
    private LocalDateTime expireTime;
    
    // Getter、Setter、构造方法省略
}

public interface DeviceShortIdRepository extends JpaRepository<DeviceShortId, Long> {
    Optional<DeviceShortId> findByClientIdAndExpireTimeAfter(String clientId, LocalDateTime now);
    boolean existsByShortId(String shortId);
    void deleteByExpireTimeBefore(LocalDateTime now);
}

业务逻辑层

@Service
public class ShortIdService {
    @Autowired
    private DeviceShortIdRepository repository;
    
    private static final int MAX_RETRY = 5;
    private static final int EXPIRE_HOURS = 24;

    public String getOrGenerateShortId(String clientId) {
        // 先清理过期数据
        repository.deleteByExpireTimeBefore(LocalDateTime.now());
        
        // 查询未过期的ID
        Optional<DeviceShortId> existingId = repository.findByClientIdAndExpireTimeAfter(clientId, LocalDateTime.now());
        if (existingId.isPresent()) {
            return existingId.get().getShortId();
        }
        
        // 生成唯一短ID(5-6位数字)
        String newShortId = null;
        for (int i = 0; i < MAX_RETRY; i++) {
            newShortId = generateRandomShortId();
            if (!repository.existsByShortId(newShortId)) {
                break;
            }
        }
        if (newShortId == null) {
            throw new RuntimeException("短ID生成失败,请重试");
        }
        
        // 保存至数据库
        DeviceShortId entity = new DeviceShortId();
        entity.setClientId(clientId);
        entity.setShortId(newShortId);
        entity.setExpireTime(LocalDateTime.now().plusHours(EXPIRE_HOURS));
        repository.save(entity);
        
        return newShortId;
    }

    private String generateRandomShortId() {
        Random random = new Random();
        int length = random.nextBoolean() ? 5 : 6;
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            sb.append(random.nextInt(10));
        }
        return sb.toString();
    }
}

控制器层

@RestController
@RequestMapping("/api/short-id")
public class ShortIdController {
    @Autowired
    private ShortIdService shortIdService;

    @GetMapping
    public ResponseEntity<String> getShortId(@RequestParam("clientId") String clientId) {
        if (clientId == null || clientId.trim().isEmpty()) {
            return ResponseEntity.badRequest().body("客户端标识不能为空");
        }
        try {
            String shortId = shortIdService.getOrGenerateShortId(clientId);
            return ResponseEntity.ok(shortId);
        } catch (RuntimeException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
        }
    }
}

3. Android客户端实现(Retrofit)

获取客户端UUID

public class ClientIdUtil {
    private static final String PREF_NAME = "AppPreferences";
    private static final String KEY_CLIENT_ID = "client_id";

    public static String getClientId(Context context) {
        SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
        String clientId = prefs.getString(KEY_CLIENT_ID, null);
        if (clientId == null) {
            clientId = UUID.randomUUID().toString();
            prefs.edit().putString(KEY_CLIENT_ID, clientId).apply();
        }
        return clientId;
    }
}

Retrofit请求示例

public interface ShortIdApi {
    @GET("/api/short-id")
    Call<String> getShortId(@Query("clientId") String clientId);
}

// 使用示例
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://your-server-domain.com/")
        .addConverterFactory(ScalarsConverterFactory.create())
        .build();

ShortIdApi api = retrofit.create(ShortIdApi.class);
String clientId = ClientIdUtil.getClientId(getApplicationContext());
Call<String> call = api.getShortId(clientId);

call.enqueue(new Callback<String>() {
    @Override
    public void onResponse(Call<String> call, Response<String> response) {
        if (response.isSuccessful()) {
            String shortId = response.body();
            // 处理获取到的短ID
        } else {
            // 处理错误响应
        }
    }

    @Override
    public void onFailure(Call<String> call, Throwable t) {
        // 处理请求失败
    }
});

4. 额外安全加固措施

  • 请求频率限制:用Redis或Spring Cloud Gateway实现限流,限制每个clientId每分钟最多请求5次,防止恶意客户端刷请求。
  • 定时清理过期数据:添加Spring定时任务,每天凌晨清理过期ID:
    @Scheduled(cron = "0 0 0 * * ?")
    public void cleanExpiredIds() {
        repository.deleteByExpireTimeBefore(LocalDateTime.now());
    }
    
  • 短ID生成优化:若5-6位数字的唯一性不足,可加入大小写字母扩大ID空间,生成时需加强唯一性校验。

最佳实践总结

  1. 用客户端UUID/Advertising ID作为唯一标识,替代不可靠的IP/MAC。
  2. 服务器端强制校验:优先返回客户端已有的未过期ID,避免重复生成。
  3. 配合请求频率限制,防止恶意刷量。
  4. 定期清理过期数据,减轻数据库压力。

内容的提问来源于stack exchange,提问作者Helkor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 02:30:59