高安全券码、注册码生成
下面给你提供一个生产级 Java 工具类,可以生成 C6Y2-6CK8-VF7J 这种格式的券码:
- 去掉易混淆字符(I、1、L、O、0)
- 可自定义:长度、分组大小、分隔符
- 高安全性:基于 SecureRandom
- 支持批量生成 + 去重校验
你给的示例
C6Y2-6CK8-VF7J实际上只有 13 位有效字符(不含横杠),下面代码按 12 位 + 3 组 实现,你也可以灵活调整。
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
/**
* 生产级券码生成器 - JDK 8 兼容版本
*/
public class CouponCodeGenerator {
private static final Logger logger = Logger.getLogger(CouponCodeGenerator.class.getName());
private static final String ALPHABET = "ABCDEFGHJKMNOPQRSTUVWXYZ23456789";
private static final int BASE = ALPHABET.length(); // 32, 2^5
private static final SecureRandom RANDOM = new SecureRandom();
// JDK 8 使用 ThreadLocal 避免 StringBuilder 重复创建
private static final ThreadLocal<StringBuilder> THREAD_BUFFER =
ThreadLocal.withInitial(() -> new StringBuilder(64));
/**
* 生成单个券码
*/
public static String generate(int totalLen, int groupSize, String separator) {
validateParams(totalLen, groupSize);
char[] chars = new char[totalLen];
for (int i = 0; i < totalLen; i++) {
chars[i] = ALPHABET.charAt(RANDOM.nextInt(BASE));
}
String actualSeparator = separator == null ? "" : separator;
return formatGroups(new String(chars), groupSize, actualSeparator);
}
/**
* 格式化分组(JDK 8 优化版)
*/
private static String formatGroups(String str, int groupSize, String separator) {
if (separator.isEmpty()) {
return str;
}
int len = str.length();
StringBuilder sb = THREAD_BUFFER.get();
sb.setLength(0); // 清空重用
for (int i = 0; i < len; i++) {
if (i > 0 && i % groupSize == 0) {
sb.append(separator);
}
sb.append(str.charAt(i));
}
return sb.toString();
}
/**
* 批量生成(自动选择串行/并行)
*/
public static Set<String> generateBatch(int count, int totalLen, int groupSize,
String separator, boolean parallel) {
// 容量检查
checkCapacity(count, totalLen);
if (parallel && count > 5000) { // JDK 8 并行阈值调低
return generateBatchParallel(count, totalLen, groupSize, separator);
} else {
return generateBatchSequential(count, totalLen, groupSize, separator);
}
}
/**
* 串行生成(修正计数逻辑)
*/
private static Set<String> generateBatchSequential(int count, int totalLen,
int groupSize, String separator) {
Set<String> codes = new LinkedHashSet<>(count * 2);
int collisions = 0;
int maxCollisions = count * 10;
String actualSeparator = separator == null ? "" : separator;
while (codes.size() < count && collisions < maxCollisions) {
String code = generate(totalLen, groupSize, actualSeparator);
if (!codes.add(code)) {
collisions++;
}
}
if (codes.size() < count) {
double rate = collisions * 100.0 / (collisions + codes.size());
throw new CouponExhaustedException(
String.format(Locale.US,
"仅生成 %d/%d 个券码,碰撞次数: %d,碰撞率: %.2f%%",
codes.size(), count, collisions, rate));
}
logger.info(String.format("串行生成 %d 个券码完成,碰撞次数: %d", count, collisions));
return codes;
}
/**
* 并行生成(JDK 8 兼容版本)
*/
private static Set<String> generateBatchParallel(int count, int totalLen,
int groupSize, String separator) {
// JDK 8 使用 ConcurrentHashMap.newKeySet() 需要 Java 8+
Set<String> result = ConcurrentHashMap.newKeySet();
AtomicInteger collisions = new AtomicInteger(0);
AtomicInteger completed = new AtomicInteger(0);
int maxCollisions = count * 10;
String actualSeparator = separator == null ? "" : separator;
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(cores);
// 每个线程的目标生成数量
int perThreadCount = (count + cores - 1) / cores;
List<Future<Void>> futures = new ArrayList<>();
for (int t = 0; t < cores; t++) {
final int targetPerThread = perThreadCount;
futures.add(executor.submit(() -> {
Set<String> localSet = new HashSet<>(targetPerThread);
int localCollisions = 0;
int maxLocalCollisions = targetPerThread * 10;
while (localSet.size() < targetPerThread &&
result.size() < count &&
collisions.get() < maxCollisions) {
String code = generate(totalLen, groupSize, actualSeparator);
// 先尝试加本地集,再加全局集(减少全局锁竞争)
if (localSet.add(code)) {
if (result.add(code)) {
completed.incrementAndGet();
} else {
// 全局已存在,从本地移除并计数碰撞
localSet.remove(code);
localCollisions++;
collisions.incrementAndGet();
}
} else {
// 本地重复
localCollisions++;
collisions.incrementAndGet();
}
}
// 将本地剩余数据合并到全局
for (String code : localSet) {
result.add(code);
}
return null;
}));
}
// 等待所有线程完成
for (Future<Void> future : futures) {
try {
future.get(30, TimeUnit.SECONDS);
} catch (Exception e) {
executor.shutdownNow();
throw new CouponExhaustedException("并行生成失败: " + e.getMessage());
}
}
executor.shutdown();
if (result.size() < count) {
throw new CouponExhaustedException(
String.format(Locale.US,
"并行生成仅得到 %d/%d 个券码,碰撞次数: %d",
result.size(), count, collisions.get()));
}
logger.info(() -> String.format("并行生成 %d 个券码完成,碰撞次数: %d", count, collisions.get()));
return new LinkedHashSet<>(result);
}
/**
* 流式生成到文件(避免 OOM,JDK 8 兼容)
*/
public static void generateBatchToFile(int count, int totalLen, int groupSize,
String separator, String filePath) throws IOException {
try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filePath)))) {
Set<String> batchCache = new HashSet<>();
int batchSize = 10000; // 每批1万条
int generated = 0;
int collisions = 0;
int maxCollisions = count * 10;
String actualSeparator = separator == null ? "" : separator;
while (generated < count && collisions < maxCollisions) {
String code = generate(totalLen, groupSize, actualSeparator);
if (batchCache.add(code)) {
writer.println(code);
generated++;
if (generated % batchSize == 0) {
writer.flush();
batchCache.clear(); // 释放内存
logger.info(String.format("已生成 %d/%d 个券码", generated, count));
}
} else {
collisions++;
}
}
if (generated < count) {
throw new CouponExhaustedException(
String.format("仅生成 %d/%d 个券码,碰撞次数: %d", generated, count, collisions));
}
writer.flush();
logger.info(() -> String.format("券码已保存到文件: %s", filePath));
}
}
/**
* 容量检查(使用 BigInteger 避免溢出)
*/
private static void checkCapacity(int count, int totalLen) {
BigInteger capacity = BigInteger.valueOf(BASE).pow(totalLen);
BigInteger needed = BigInteger.valueOf(count);
BigInteger threshold = capacity.multiply(BigInteger.valueOf(70))
.divide(BigInteger.valueOf(100));
if (needed.compareTo(threshold) > 0) {
double ratio = needed.doubleValue() / capacity.doubleValue() * 100;
logger.warning(String.format(Locale.US,
"警告:生成数量 %d 接近理论容量 %.0e,碰撞概率较高 (%.1f%%)",
count, Math.pow(BASE, totalLen), ratio));
}
}
/**
* 验证券码格式
*/
public static boolean validate(String code, int totalLen, int groupSize, String separator) {
if (code == null || code.isEmpty()) return false;
String actualSeparator = separator == null ? "" : separator;
String clean = code.replace(actualSeparator, "");
if (clean.length() != totalLen) return false;
for (char c : clean.toCharArray()) {
if (ALPHABET.indexOf(c) == -1) return false;
}
// 验证分隔符位置
if (!actualSeparator.isEmpty()) {
String reformatted = formatGroups(clean, groupSize, actualSeparator);
if (!reformatted.equals(code)) return false;
}
return true;
}
private static void validateParams(int totalLen, int groupSize) {
if (totalLen < 4 || totalLen > 32) {
throw new IllegalArgumentException("totalLen 必须介于 4-32 之间");
}
if (groupSize < 1 || groupSize > totalLen) {
throw new IllegalArgumentException("groupSize 必须介于 1-" + totalLen);
}
}
/**
* 自定义异常
*/
public static class CouponExhaustedException extends RuntimeException {
public CouponExhaustedException(String message) {
super(message);
}
}
/**
* 测试示例
*/
public static void main(String[] args) {
// 1. 基本生成测试
String code = generate(16, 4, "-");
System.out.println("生成单个券码: " + code);
System.out.println("验证结果: " + validate(code, 16, 4, "-") + "\n");
// 2. 串行批量生成
long start = System.nanoTime();
Set<String> batch1 = generateBatch(1000, 12, 4, "-", false);
long time = (System.nanoTime() - start) / 1_000_000;
System.out.printf("串行生成 1000 个券码: %d ms%n", time);
System.out.println("示例: " + batch1.iterator().next() + "\n");
// 3. 并行批量生成(JDK 8 ForkJoinPool)
start = System.nanoTime();
Set<String> batch2 = generateBatch(5000, 12, 4, "-", true);
time = (System.nanoTime() - start) / 1_000_000;
System.out.printf("并行生成 5000 个券码: %d ms%n", time);
System.out.println("示例: " + batch2.iterator().next() + "\n");
// 4. 理论容量测试
BigInteger cap = BigInteger.valueOf(BASE).pow(12);
System.out.printf("12位券码理论容量: %s (%.2e)%n", cap, Math.pow(BASE, 12));
// 5. 异常测试
try {
generateBatch(1000000, 8, 4, "-", false);
} catch (CouponExhaustedException e) {
System.out.println("\n预期异常: " + e.getMessage());
}
}
}
五、生产环境还需要考虑什么(可选)
- 不要用毫秒时间戳参与生成:会降低熵,且分布式下可能重复。
- 如果需要更短(8~10位):可以将
ALPHABET改为"ABCDEFGHJKLMNPQRSTUVWXYZ123456789"(进一步增加位数密度),但不建议低于 10 位。 - 如果券码需要反解出业务信息:可以在前面加固定业务前缀(如
JD),但会降低安全性,需要配合其他风控。
一句话总结:上面的代码就是你要的 C6Y2-6CK8-VF7J 这种风格的高安全、可读性好的 Java 实现,拿来即用。
高熵随机是一个信息论/密码学的概念,简单理解就是:结果非常难以预测,且每个可能的值出现的概率几乎相等。
把它拆成两个词来解释,你就明白了。
1. 先看“熵”
熵在这里代表不确定性或混乱程度。
- 低熵:非常确定,可预测。比如流水号
000001, 000002...。你看到000001,就能猜出下一个大概率是000002。 - 高熵:非常不确定,不可预测。比如一个真正的随机数。你看到
A3F9,完全无法猜出下一个是K2M7还是9D4F。
熵越高,意味着你掌握的信息越少,猜测的难度越大。
2. 再来看“高熵随机”
它不是一个具体算法,而是对随机性质量的形容,通常具备三个特点:
- 真正的随机源:不是用
new Random()这种基于时间戳的“伪随机”(可预测),而是用SecureRandom这类基于硬件噪音、鼠标移动等物理现象的“密码学安全随机”。 - 充足的位数:比如12位,每位有32种可能(去掉混淆字符后),总组合空间巨大(32^12),无法暴力枚举。
- 均匀分布:每个字符(如A、B、C...9)出现的概率是相等的,不会出现某个字母明显偏多。
举个直观的例子
| 类型 | 例子 | 熵的高低 | 原因 |
|---|---|---|---|
| 流水号 | MP240615000001 |
极低熵 | 看到前面,就能推算后面;攻击者可遍历。 |
| 伪随机 | new Random().nextInt() 生成的 845321 |
中等熵 | 看似随机,但如果你知道种子(时间戳),就能推算出整个序列。 |
| 高熵随机 | SecureRandom 生成的 C6Y2-6CK8-VF7J |
极高熵 | 无法预测下一张券码;哪怕你拿到1000万个历史码,也无法推算第1001个。 |
对你之前的券码问题来说
-
你问
C6YV2-6CKK8-VF7JB怎么样
→ 这就是高熵随机的典型例子。它无法被暴力遍历,因为攻击者面对的是天文数字级别的可能性,只能一个个瞎猜。 -
为什么不用
MP240615000123
→ 因为它是低熵的,攻击者可以系统性地枚举(从1试到100万),几分钟就能扫光。
一句话总结
高熵随机 = 用真正的随机源生成的、组合空间巨大、无法预测的随机数。它是防暴力遍历、防猜测的核心武器。
在实际开发中,用 java.security.SecureRandom 生成的券码,就可以称为“高熵随机券码”。
本文来自博客园,作者:VipSoft 转载请注明原文链接:https://www.cnblogs.com/vipsoft/p/20195199
浙公网安备 33010602011771号