| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package com.ruoyi.common.utils; |
| | | |
| | | import javax.crypto.Mac; |
| | | import javax.crypto.spec.SecretKeySpec; |
| | | import java.nio.charset.StandardCharsets; |
| | | |
| | | /** |
| | | * å¤é¾çç ï¼æ {@code service_out_path.id} æ å°æåºå® 6 ä½ base62 å符串ã |
| | | * |
| | | * <p><b>为ä»ä¹éè¦å®</b>ï¼åæ¥ççç æ¯ {@code String.format("%03X", id)}ï¼çäºæèªå¢ä¸»é®ç´æ¥åå¨ |
| | | * çä¿¡é¾æ¥éï¼{@code /wt?p=1F4}ï¼ã3~4 ä½åå
è¿å¶ç空é´åªæ 4096 / 65536ï¼èè¿å¼ 表已æä¸ä¸æ¡è®°å½ï¼ |
| | | * 空é´å ä¹è¢«å 满 ââ æ»å»è
é个è¯å°±è½æææéè®¿è®°å½æä¸éã2026-11-05 æµæ±çç«åå¾·å»é¢ç |
| | | * ç½ç»å®å
¨éæ¥ï¼S20262767ï¼å°±æ¯æçè¿ä¸ªç¹ã |
| | | * |
| | | * <p><b>åäºä»ä¹</b>ï¼å¯¹ä¸»é®å䏿¬¡<b>带å¯é¥çä¼ªéæºåå°</b>ï¼6 è½® Feistel ç½ç»ï¼è½®å½æ°ç¨ |
| | | * HmacSHA256ï¼ï¼è¾åºä»æ¯ 6 ä½ã仿¯"主é®ççº¯å½æ°"ï¼å æ¤ï¼ |
| | | * <ul> |
| | | * <li>åä¸ä¸ª id å¨ä»»ä½æºå¨ã任使¶åé½ç®åºåä¸ä¸ªç ï¼ä¸ {@code %03X} 䏿 ·æ¯ç¡®å®æ§çï¼ï¼ |
| | | * æä»¥æ£è½å¨ RyTask / RedisMqReceiver / ServiceSubtaskServiceImpl ç 14 å¤ç |
| | | * "ä» id éç®çç "é»è¾ä¸éè¦æ¹æ°æ®æµï¼</li> |
| | | * <li>ä¸éè¦æ°å¢æ°æ®åºåï¼ä¹ä¸éè¦ä¸ºåéæ°æ®åè¿ç§»ï¼</li> |
| | | * <li>ç ç©ºé´ 62^6 = 56,800,235,584ï¼çº¦ 568 亿ï¼ï¼ä¸ä¸æ¡è®°å½ä¸éæºå½ä¸ç约 1/710 ä¸ï¼ |
| | | * é个éåå¨éæµä¸ä¸å¯è¡ã</li> |
| | | * </ul> |
| | | * |
| | | * <p><b>为ä»ä¹ä¸ç´æ¥ç¨éæºç + è½åº</b>ï¼éæºç è¦æ±"çæä¸æ¬¡ãä¹åå¤å¤è¯»åº"ï¼èç°å¨æ 14 å¤ |
| | | * æ¯ä» id ç°ç®çç çï¼å¦ {@code RyTask} è¡¥å¿åéã{@code RedisMqReceiver} åç±»åéééï¼ï¼ |
| | | * æ¹æè¯»åºè¦å¨æ°æ®æµï¼ä¸ä»»ä½ä¸å¤æ¼æ¹é½ä¼å¯¼è´"龿¥æä¸å¼"ãåå°æ¹æ¡æ¯çº¯å½æ°ï¼å¤©ç¶ä¸ä¼æè¿ä¸ªé®é¢ã |
| | | * |
| | | * <p><b>为ä»ä¹ä¸ç¨ AES</b>ï¼6 ä½ base62 çç©ºé´æ¯ 238328Â²ï¼æ£å¥½å¯ä»¥åæå¹³è¡¡ Feistelï¼ |
| | | * èä¸å AES 飿 ·è¦æ± 2 ç广¬¡åç»ãç¨ Feistel å¯ä»¥åå°"è¾åºé¿åº¦å®å
¨å¯æ§"ã |
| | | * |
| | | * <p><b>å¯é¥</b>ï¼ç± {@code shortcode.secret} é
置注å
¥ï¼è§ {@link ShortCodeConfig}ï¼ã |
| | | * <b>ä¸çº¿åä¸è¦åæ¹</b> ââ æ¹äºä¹åå·²ååºç 6 ä½ç龿 æ³åè§£ï¼åªè½é {@code radix} åå
åºã |
| | | * |
| | | * @author smartor |
| | | */ |
| | | public final class ShortCode { |
| | | |
| | | /** çç é¿åº¦ï¼6 ä½ã */ |
| | | public static final int LENGTH = 6; |
| | | |
| | | /** base62 å符表ï¼é¡ºåºå³ç¼ç 顺åºã */ |
| | | private static final char[] ALPHABET = |
| | | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray(); |
| | | |
| | | /** è¿å¶ï¼62ã */ |
| | | private static final int BASE = ALPHABET.length; |
| | | |
| | | /** ååºå¤§å°ï¼62^3 = 238328ã */ |
| | | private static final int HALF = 238328; |
| | | |
| | | /** æ´ä¸ªç 空é´ï¼62^6 = 238328 * 238328 = 56800235584ã */ |
| | | private static final long DOMAIN = 56800235584L; |
| | | |
| | | /** Feistel è½®æ°ã6 è½®é
å HMAC è½®å½æ°ï¼å¯¹æ¬åºæ¯ï¼åªè½åæå¡ç«¯"ç䏿¬¡ yes/no"ï¼è¶³å¤ã */ |
| | | private static final int ROUNDS = 6; |
| | | |
| | | private static final String ALGORITHM = "HmacSHA256"; |
| | | |
| | | /** 缺çå¯é¥ï¼ä¸ application.yml éç {@code shortcode.secret} ä¿æä¸è´ã */ |
| | | private static final String DEFAULT_SECRET = "smartor-outpath-shortcode-2026"; |
| | | |
| | | private static volatile byte[] secret = DEFAULT_SECRET.getBytes(StandardCharsets.UTF_8); |
| | | |
| | | private ShortCode() { |
| | | } |
| | | |
| | | /** |
| | | * 注å
¥å¯é¥ï¼ç± {@link ShortCodeConfig} å¨å¯å¨æ¶è°ç¨ï¼ãä¼ ç©ºå¼åä¿æç¼ºçå¯é¥ä¸åã |
| | | */ |
| | | public static void setSecret(String value) { |
| | | if (value != null && value.trim().length() > 0) { |
| | | secret = value.trim().getBytes(StandardCharsets.UTF_8); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * ä¸»é® â 6 ä½çç ã |
| | | * |
| | | * @param id {@code service_out_path.id}ï¼åå¼èå´ [0, 56800235584) |
| | | * @return å®é¿ 6 ä½ base62 å符串 |
| | | */ |
| | | public static String encode(long id) { |
| | | if (id < 0 || id >= DOMAIN) { |
| | | throw new IllegalArgumentException("主é®è¶
åºçç å¯è¡¨ç¤ºèå´ [0, " + DOMAIN + ")ï¼" + id); |
| | | } |
| | | long left = id / HALF; |
| | | long right = id % HALF; |
| | | Mac mac = newMac(); |
| | | for (int round = 0; round < ROUNDS; round++) { |
| | | long f = round(mac, round, right); |
| | | long next = (left + f) % HALF; |
| | | left = right; |
| | | right = next; |
| | | } |
| | | return toBase62(left * HALF + right); |
| | | } |
| | | |
| | | /** |
| | | * 6 ä½çç â 主é®ã䏿¯åæ³çç ï¼é¿åº¦ä¸ä¸º 6ãå«é base62 åç¬¦ï¼æ¶è¿å {@code null}ï¼ |
| | | * è°ç¨æ¹æ®æ¤åè½å°åå²ç {@code radix} æ¥æ³ã |
| | | * |
| | | * @param code çç |
| | | * @return 主é®ï¼ä¸åæ³æ¶ä¸º {@code null} |
| | | */ |
| | | public static Long decode(String code) { |
| | | if (code == null || code.length() != LENGTH) { |
| | | return null; |
| | | } |
| | | long value = 0L; |
| | | for (int i = 0; i < LENGTH; i++) { |
| | | int index = indexOf(code.charAt(i)); |
| | | if (index < 0) { |
| | | return null; |
| | | } |
| | | value = value * BASE + index; |
| | | } |
| | | // Feistel åæ¼ï¼æ£åæ¯ L'=R, R'=(L+F(R)) mod nï¼éåå R=L'ãL=(R'-F(L')) mod n |
| | | long left = value / HALF; |
| | | long right = value % HALF; |
| | | Mac mac = newMac(); |
| | | for (int round = ROUNDS - 1; round >= 0; round--) { |
| | | long f = round(mac, round, left); |
| | | long previousLeft = (right - f + HALF) % HALF; |
| | | right = left; |
| | | left = previousLeft; |
| | | } |
| | | return left * HALF + right; |
| | | } |
| | | |
| | | private static Mac newMac() { |
| | | try { |
| | | Mac mac = Mac.getInstance(ALGORITHM); |
| | | mac.init(new SecretKeySpec(secret, ALGORITHM)); |
| | | return mac; |
| | | } catch (Exception e) { |
| | | // HmacSHA256 æ¯ JDK æ åç®æ³ï¼èµ°å°è¿é说æè¿è¡ç¯å¢å¼å¸¸ï¼ç´æ¥æ´é²åºæ¥ |
| | | throw new IllegalStateException("çç 计ç®å¤±è´¥ï¼æ æ³åå§å " + ALGORITHM, e); |
| | | } |
| | | } |
| | | |
| | | /** è½®å½æ° F(round, block) = HmacSHA256(secret, round||block) åå 4 åèï¼æ¨¡ HALFã */ |
| | | private static long round(Mac mac, int round, long block) { |
| | | byte[] data = new byte[9]; |
| | | data[0] = (byte) round; |
| | | data[1] = (byte) (block >>> 56); |
| | | data[2] = (byte) (block >>> 48); |
| | | data[3] = (byte) (block >>> 40); |
| | | data[4] = (byte) (block >>> 32); |
| | | data[5] = (byte) (block >>> 24); |
| | | data[6] = (byte) (block >>> 16); |
| | | data[7] = (byte) (block >>> 8); |
| | | data[8] = (byte) block; |
| | | byte[] digest = mac.doFinal(data); |
| | | int value = ((digest[0] & 0xFF) << 24) |
| | | | ((digest[1] & 0xFF) << 16) |
| | | | ((digest[2] & 0xFF) << 8) |
| | | | (digest[3] & 0xFF); |
| | | return (value & 0x7FFFFFFFL) % HALF; |
| | | } |
| | | |
| | | /** å®é¿ 6 ä½ base62ï¼å·¦ä¾§è¡¥ '0'ï¼ã */ |
| | | private static String toBase62(long value) { |
| | | char[] out = new char[LENGTH]; |
| | | for (int i = LENGTH - 1; i >= 0; i--) { |
| | | out[i] = ALPHABET[(int) (value % BASE)]; |
| | | value /= BASE; |
| | | } |
| | | return new String(out); |
| | | } |
| | | |
| | | private static int indexOf(char c) { |
| | | for (int i = 0; i < ALPHABET.length; i++) { |
| | | if (ALPHABET[i] == c) { |
| | | return i; |
| | | } |
| | | } |
| | | return -1; |
| | | } |
| | | } |