Hashids 原理及實現

Hashids是一個將數字轉化爲長度較短、唯一且不連續的值的庫。特點是:

  • 對非負整數都可以生成唯一短id
  • 可以設置不同的鹽,具有保密性
  • 遞增的輸入產生的輸出無法預測
  • 代碼較短,且不依賴於第三方庫

原理

進制轉換:將10進制的整數轉化爲 62 進制(26個字母大小寫+10個數字),可擴展爲任意進制。

  private static String hash(long input, String alphabet) {
    String hash = "";
    final int alphabetLen = alphabet.length();

    do {
      final int index = (int) (input % alphabetLen);
      if (index >= 0 && index < alphabet.length()) {
        hash = alphabet.charAt(index) + hash;
      }
      input /= alphabetLen;
    } while (input > 0);

    return hash;
  }

Fisher-Yates Shuffle算法

其基本思想就是從原始數組中隨機取一個之前沒取過的數字到新的數組中,具體如下:

  1. 初始化原始數組和新數組,原始數組長度爲n(已知);
  2. 從還沒處理的數組(假如還剩k個)中,隨機產生一個[0, k)之間的數字p(假設數組從0開始);
  3. 從剩下的k個數中把第p個數取出;
  4. 重複步驟2和3直到數字全部取完;
  5. 從步驟3取出的數字序列便是一個打亂了的數列。

Hashids 使用了 Fisher–Yates 洗牌算法的變種。從原始數組中隨機取一個之前沒取過的數字與當前數組最後一個元素交換位置。

private static Random random = new Random();
private static String shuffle(String alphabet) {
    char[] shuffle = alphabet.toCharArray();
    for (int i = shuffle.length - 1; i >= 1; i--) {
        int j = random.nextInt(i+1);
        char tmp = shuffle[i];
        shuffle[i] = shuffle[j];
        shuffle[j] = tmp;
    }
    return new String(shuffle);
}
<dependency>
    <groupId>org.hashids</groupId>
    <artifactId>hashids</artifactId>
    <version>1.0.3</version>
</dependency>

代碼解析

import java.util.ArrayList;

public class Hashids {
    // Hashids 能夠加密的最大值
    public static final long MAX_NUMBER = 9007199254740992L;
    // 默認編解碼字符串
    private static final String DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    // 默認分隔符
    private static final String DEFAULT_SEPS = "cfhistuCFHISTU";
    // 默認鹽值
    private static final String DEFAULT_SALT = "";
    // 默認最小加密後字符串的長度
    private static final int DEFAULT_MIN_HASH_LENGTH = 0;
    // 最小編解碼字符串
    private static final int MIN_ALPHABET_LENGTH = 16;
    //
    private static final double SEP_DIV = 3.5;
    //
    private static final int GUARD_DIV = 12;
    // 編解碼鹽值
    private final String salt;
    // 編碼後最小的字符長度
    private final int minHashLength;
    // 編解碼字符串
    private final String alphabet;
    // 多個數字編解碼的分界符
    private final String seps;
    // 補齊至 minHashLength 長度添加的字符列表
    private final String guards;
    // 以上 3 個字符串中的字符是互不包含關係

    public Hashids() {
        this(DEFAULT_SALT);
    }

    public Hashids(String salt) {
        this(salt, 0);
    }

    public Hashids(String salt, int minHashLength) {
        this(salt, minHashLength, DEFAULT_ALPHABET);
    }

    public Hashids(String salt, int minHashLength, String alphabet) {
        this.salt = salt != null ? salt : DEFAULT_SALT;
        this.minHashLength = minHashLength > 0 ? minHashLength : DEFAULT_MIN_HASH_LENGTH;

        // alphabet 中的字符需要唯一
        final StringBuilder uniqueAlphabet = new StringBuilder();
        for (int i = 0; i < alphabet.length(); i++) {
            if (uniqueAlphabet.indexOf(String.valueOf(alphabet.charAt(i))) == -1) {
                uniqueAlphabet.append(alphabet.charAt(i));
            }
        }
        alphabet = uniqueAlphabet.toString();

        // 最小長度檢查
        if (alphabet.length() < MIN_ALPHABET_LENGTH) {
            throw new IllegalArgumentException(
                    "alphabet must contain at least " + MIN_ALPHABET_LENGTH + " unique characters");
        }

        // 編解碼錶不能含有空格
        if (alphabet.contains(" ")) {
            throw new IllegalArgumentException("alphabet cannot contains spaces");
        }

        // seps 只能包含 alphabet 中的字符
        // alphabet 不能包含 seps 中的字符
        String seps = DEFAULT_SEPS;
        for (int i = 0; i < seps.length(); i++) {
            final int j = alphabet.indexOf(seps.charAt(i));
            if (j == -1) {
                seps = seps.substring(0, i) + " " + seps.substring(i + 1);
            } else {
                alphabet = alphabet.substring(0, j) + " " + alphabet.substring(j + 1);
            }
        }
        alphabet = alphabet.replaceAll("\\s+", "");
        seps = seps.replaceAll("\\s+", "");

        // 根據 salt 打亂默認 seps 的順序
        seps = Hashids.consistentShuffle(seps, this.salt);

        if ((seps.isEmpty()) || (((float) alphabet.length() / seps.length()) > SEP_DIV)) {
            int seps_len = (int) Math.ceil(alphabet.length() / SEP_DIV);

            if (seps_len == 1) {
                seps_len++;
            }

            if (seps_len > seps.length()) {
                final int diff = seps_len - seps.length();
                seps += alphabet.substring(0, diff);
                alphabet = alphabet.substring(diff);
            } else {
                seps = seps.substring(0, seps_len);
            }
        }

        // 根據 salt 打亂編解碼錶 alphabet 的順序
        alphabet = Hashids.consistentShuffle(alphabet, this.salt);
        // use double to round up
        final int guardCount = (int) Math.ceil((double) alphabet.length() / GUARD_DIV);

        String guards;
        if (alphabet.length() < 3) {
            guards = seps.substring(0, guardCount);
            seps = seps.substring(guardCount);
        } else {
            guards = alphabet.substring(0, guardCount);
            alphabet = alphabet.substring(guardCount);
        }
        this.guards = guards;
        this.alphabet = alphabet;
        this.seps = seps;
    }

    // 將數字加密爲字符串
    public String encode(long... numbers) {
        if (numbers.length == 0) {
            return "";
        }

        for (final long number : numbers) {
            // 只能加解密非負整數
            if (number < 0) {
                return "";
            }
            if (number > MAX_NUMBER) {
                throw new IllegalArgumentException("number can not be greater than " + MAX_NUMBER + "L");
            }
        }
        return this._encode(numbers);
    }

    // 將字符串解密爲數字
    public long[] decode(String hash) {
        if (hash.isEmpty()) {
            return new long[0];
        }

        String validChars = this.alphabet + this.guards + this.seps;
        for (int i = 0; i < hash.length(); i++) {
            if (validChars.indexOf(hash.charAt(i)) == -1) {
                return new long[0];
            }
        }

        return this._decode(hash, this.alphabet);
    }

    private String _encode(long... numbers) {
        long numberHashInt = 0;
        for (int i = 0; i < numbers.length; i++) {
            numberHashInt += (numbers[i] % (i + 100));
        }
        String alphabet = this.alphabet;
        // 第一位不參與編解碼
        final char ret = alphabet.charAt((int) (numberHashInt % alphabet.length()));

        long num;
        long sepsIndex, guardIndex;
        String buffer;
        final StringBuilder ret_strB = new StringBuilder(this.minHashLength);
        ret_strB.append(ret);
        char guard;

        for (int i = 0; i < numbers.length; i++) {
            num = numbers[i];
            buffer = ret + this.salt + alphabet;

            alphabet = Hashids.consistentShuffle(alphabet, buffer.substring(0, alphabet.length()));
            final String last = Hashids.hash(num, alphabet);

            ret_strB.append(last);

            // 不是最後一個待加密數字,增加分隔符
            // 按照一定規則在 seps 選擇一位分隔符,如何選擇無所謂,因爲 seps 不存在於 alphabet
            if (i + 1 < numbers.length) {
                if (last.length() > 0) {
                    num %= (last.charAt(0) + i);
                    sepsIndex = (int) (num % this.seps.length());
                } else {
                    sepsIndex = 0;
                }
                ret_strB.append(this.seps.charAt((int) sepsIndex));
            }
        }

        // 小於最小長度 minHashLength 則進行擴充
        String ret_str = ret_strB.toString();
        // 擴充兩位
        if (ret_str.length() < this.minHashLength) {
            guardIndex = (numberHashInt + (ret_str.charAt(0))) % this.guards.length();
            guard = this.guards.charAt((int) guardIndex);

            ret_str = guard + ret_str;

            if (ret_str.length() < this.minHashLength) {
                guardIndex = (numberHashInt + (ret_str.charAt(2))) % this.guards.length();
                guard = this.guards.charAt((int) guardIndex);

                ret_str += guard;
            }
        }

        final int halfLen = alphabet.length() / 2;
        while (ret_str.length() < this.minHashLength) {
            alphabet = Hashids.consistentShuffle(alphabet, alphabet);
            // 使用編解碼字符串填充,可見上述填充的兩位前後都是冗餘字段
            ret_str = alphabet.substring(halfLen) + ret_str + alphabet.substring(0, halfLen);
            final int excess = ret_str.length() - this.minHashLength;
            // 長度長於 minHashLength 限制
            if (excess > 0) {
                final int start_pos = excess / 2;
                ret_str = ret_str.substring(start_pos, start_pos + this.minHashLength);
            }
        }

        return ret_str;
    }

    private long[] _decode(String hash, String alphabet) {
        final ArrayList<Long> ret = new ArrayList<Long>();

        int i = 0;
        final String regexp = "[" + this.guards + "]";
        String hashBreakdown = hash.replaceAll(regexp, " ");
        String[] hashArray = hashBreakdown.split(" ");

        // 見下面代碼說明
        if (hashArray.length == 3 || hashArray.length == 2) {
            i = 1;
        }

        if (hashArray.length > 0) {
            hashBreakdown = hashArray[i];
            if (!hashBreakdown.isEmpty()) {
                // 首位無用字段
                final char lottery = hashBreakdown.charAt(0);

                hashBreakdown = hashBreakdown.substring(1);
                hashBreakdown = hashBreakdown.replaceAll("[" + this.seps + "]", " ");
                hashArray = hashBreakdown.split(" ");

                String subHash, buffer;
                for (final String aHashArray : hashArray) {
                    subHash = aHashArray;
                    buffer = lottery + this.salt + alphabet;
                    alphabet = Hashids.consistentShuffle(alphabet, buffer.substring(0, alphabet.length()));
                    ret.add(Hashids.unhash(subHash, alphabet));
                }
            }
        }

        // transform from List<Long> to long[]
        long[] arr = new long[ret.size()];
        for (int k = 0; k < arr.length; k++) {
            arr[k] = ret.get(k);
        }

        // 加密校驗
        if (!this.encode(arr).equals(hash)) {
            arr = new long[0];
        }

        return arr;
    }

    private static String consistentShuffle(String alphabet, String salt) {
        if (salt.length() <= 0) {
            return alphabet;
        }

        int asc_val, j;
        final char[] tmpArr = alphabet.toCharArray();
        for (int i = tmpArr.length - 1, v = 0, p = 0; i > 0; i--, v++) {
            v %= salt.length();
            asc_val = salt.charAt(v);
            p += asc_val;
            j = (asc_val + v + p) % i;
            final char tmp = tmpArr[j];
            tmpArr[j] = tmpArr[i];
            tmpArr[i] = tmp;
        }

        return new String(tmpArr);
    }

    /**
     * 將整數轉換爲 alphabet.length() 進制
     *
     * @param input
     * @param alphabet
     * @return
     */
    private static String hash(long input, String alphabet) {
        String hash = "";
        final int alphabetLen = alphabet.length();

        do {
            final int index = (int) (input % alphabetLen);
            if (index >= 0 && index < alphabet.length()) {
                hash = alphabet.charAt(index) + hash;
            }
            input /= alphabetLen;
        } while (input > 0);

        return hash;
    }

    /**
     * 將 alphabet.length() 進制轉換爲整數
     *
     * @param input
     * @param alphabet
     * @return
     */
    private static Long unhash(String input, String alphabet) {
        long number = 0, pos;

        for (int i = 0; i < input.length(); i++) {
            pos = alphabet.indexOf(input.charAt(i));
            number = number * alphabet.length() + pos;
        }

        return number;
    }
}

幾種編碼狀態
在這裏插入圖片描述

    @Test
    public void test5() {
        String str = "456a123b456";
        String s = str.replaceAll("[abc]", " ");
        System.out.println(s);
        System.out.println(Arrays.toString(s.split(" ")));
    }
// 456 123 456
// [456, 123, 456]

https://hashids.org/java/
https://blog.csdn.net/qq_26399665/article/details/79831490

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章