概要
本記事は、.NET 開発者向けに、数値を文字列にエンコードする軽量なオープンソース暗号化(短いID生成)ツールクラスライブラリ Hashids.net を紹介します。
フロントエンドでもバックエンドでも、システムが自動的にコードやIDを生成するシーンがあり、生成されたコードやIDは重複しない(重複率が極めて低い)ことが求められます。
フロントエンド開発では、一般的に nanoid が使われます。一方、バックエンド開発においては、オートインクリメントID、Snowflake ID、GUID などが一般的です。オートインクリメントIDは中小規模のシステムでよく使われ、ストレージ容量が比較的小さく、検索速度も比較的速いですが、分散システムの構築には適していません。Snowflake ID や GUID はバイト数が多く、ストレージ容量も大きくなり、検索速度も相対的に遅くなりますが、後者2つは分散システムの構築に適しています。
また、バックエンドの実際のIDを隠すために、クライアントに表示する際に実際のIDを暗号化し、数値を暗号化して短い文字列を生成するシーンもあります。例えば、海外の有名な動画サイト YouTube の動画URLは https://www.yt.com/watch?v=yVd7vbeFj-g のようになっており、パラメータ v の値 yVd7vbeFj-g が暗号化された文字列です。
.NET, .NET Core, .NET 5/6/7/8 などのプログラム開発で、このような暗号化文字列を生成したい場合、本記事では .NET 開発者に Hashids.net というオープンソースの短いID生成(暗号化)クラスライブラリをお勧めします。
Hashids.net の機能と特徴
Hashids.net は数値を文字列に変換できます。例えば、347 を yr8 に、または数値配列 [27, 986] を 3kTMd に変換します。もちろん、変換後の文字列を再び数値または数値配列に戻すこともできます。これは、複数のパラメータを1つにまとめたり、実際のIDを隠したり、単に短い文字列IDとして使用する場合に非常に便利です。
Hashids.net の主な特徴は次のとおりです。
- 整数を一意の短いIDに変換する(ゼロを含む正の整数のみ対応)
- オートインクリメントIDから推測不可能な非連続IDを生成する
- 単一の数値または数値配列に対応
- カスタムアルファベットとソルトを許可
- 最小ハッシュ長の指定が可能
Hashids.net のインストール
Hashids.net は NuGet パッケージとして公開されているため、以下の方法でインストールできます。
1. NuGet コマンドライン
Install-Package Hashids.net
2. NuGet パッケージ管理ツール
プロジェクトで 依存関係 を右クリックします(図参照):

次に、開かれた NuGet パッケージ管理画面でキーワード Hashids.net を入力し、検索結果から Hashids.net クラスライブラリを選択してインストールします(図参照):

Hashids.net の使い方
Hashids.net の名前空間をインポート
using HashidsNet;
単一数値のエンコード
Hashids オブジェクトをインスタンス化する際に、一意のソルト値を渡すことで、他の人のハッシュ値と異なるものにできます。ここでは this is my salt を例とします。
var hashids = new Hashids("this is my salt");
var hash = hashids.Encode(12345);
実行結果:NkK9
Int64 型の数値を変換する場合は、EncodeLong() メソッドを呼び出します。
var hashids = new Hashids("this is my salt");
var hash = hashids.EncodeLong(666555444333222L);
実行結果:KVO9yy1oO5j
デコード
Hashids.net はエンコードされた文字列をデコードする機能を提供します。ただし、デコード時はエンコード時と同じソルトを使用する必要があります。
var hashids = new Hashids("this is my salt");
numbers = hashids.Decode("NkK9");
実行結果:[ 12345 ]
var hashids = new Hashids("this is my salt");
numbers = hashids.DecodeLong("KVO9yy1oO5j");
実行結果:[ 666555444333222L ]
異なるソルトでのデコード
デコード時のソルトがエンコード時と異なる場合、デコードは失敗します。
var hashids = new Hashids("this is my pepper");
numbers = hashids.Decode("NkK9");
実行結果:[]
複数数値のエンコード
var hashids = new Hashids("this is my salt");
var hash = hashids.Encode(683, 94108, 123, 5);
実行結果:aBMswoO2UB3Sj
複数数値エンコード後のデコード
var hashids = new Hashids("this is my salt");
var numbers = hashids.Decode("aBMswoO2UB3Sj")
実行結果:[ 683, 94108, 123, 5 ]
エンコード後の最小ハッシュ長を指定
次の例では、整数 1 をエンコードし、最小ハッシュ長を 8 に指定します(デフォルトは 0。つまりハッシュは可能な限り最短になります)。
var hashids = new Hashids("this is my salt", 8);
var hash = hashids.Encode(1);
実行結果:gB0NV05e
デコード
var hashids = new Hashids("this is my salt", 8);
var numbers = hashids.Decode("gB0NV05e");
実行結果:[ 1 ]
カスタムハッシュアルファベット
次の例では、カスタムハッシュアルファベットとして abcdefghijkABCDEFGHIJK12345 を指定しています。
var hashids = new Hashids("this is my salt", 0, "abcdefghijkABCDEFGHIJK12345")
var hash = hashids.Encode(1, 2, 3, 4, 5)
実行結果:Ec4iEHeF3
Hashids.net のランダム性
Hashids.net の主な目的は ID の難読化であり、さらに規則的な数値を推測不可能かつ予測不可能にすることができます。
繰り返し数値のエンコード
var hashids = new Hashids("this is my salt");
var hash = hashids.Encode(5, 5, 5, 5);
エンコード後、ハッシュ内に同じ数値が4つあることを示す繰り返しパターンは見られません。実行結果:1Wc8cwcE。
順序数値のエンコード
var hashids = new Hashids("this is my salt");
var hash = hashids.Encode(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
実行結果:kRHnurhptKcjIDTWC3sx
オートインクリメント数値のエンコード
var hashids = new Hashids("this is my salt");
hashids.Encode(1); // => NV
hashids.Encode(2); // => 6m
hashids.Encode(3); // => yD
hashids.Encode(4); // => 2l
hashids.Encode(5); // => rD
16進数のエンコード
var hashids = new Hashids("this is my salt");
var hash = hashids.EncodeHex("DEADBEEF");
実行結果:kRNrpKlJ
16進数のデコード
var hashids = new Hashids("this is my salt");
var hex = hashids.DecodeHex("kRNrpKlJ");
実行結果:DEADBEEF
文字列を暗号化して短い文字列を生成するには?
以下はサイト運営者による長文字列から短文字列への変換方法の拡張説明です。
実際は非常に簡単で、暗号化対象の元の文字列をソルトとして使用し、固定の数値を暗号化するだけです。
public static string GetHashids(this string sourceStr, int number = 9)
{
var hashids = new Hashids(sourceStr);
return hashids.Encode(number);
}
単体テストの例:この記事のエイリアス「Is-it-possible-to-use-it-as-a-short-link-generator-Hashidsnet」を暗号化します。
[TestClass]
public class HashHelperUnitTest
{
[TestMethod]
public void Hashids_Success()
{
var blogPostSlugStr = "Is-it-possible-to-use-it-as-a-short-link-generator-Hashidsnet";
var encodeStr1 = blogPostSlugStr.GetHashids();
var encodeStr2 = blogPostSlugStr.GetHashids();
Assert.AreEqual(encodeStr1, encodeStr2);
}
}
エイリアスを暗号化すると「6Q」になります。ブラウザを開いてこの記事の短縮リンク https://dotnet9.com/6Q にアクセスしてみてください。
注:この使い方は本ライブラリの本来の目的ではありませんが、長い文字列を短い文字列に変換するアイデアとしては悪くありません。ただし、元に戻せないことに注意してください。
ちょっとした知識
長い文字列を短い文字列に変換する方法はいくつかあり、一般的な方法としては:
- データベースを使用して短い文字列と元の文字列を保存し、短い文字列のルールを自分で定義します(例:1から始めて値を取得し、取得時にデータベースを読み取って短い文字列から長い文字列を、または長い文字列から短い文字列を取得します)。
利点:短い文字列と元の文字列が一対一対応しており、双方向変換が可能で、短い文字列の重複が発生しません。
欠点:データベースの読み書きが頻繁に発生し、データベースのパフォーマンスに影響を与える可能性があります。
- 長い文字列をアルゴリズムを使用して短い文字列に変換します。可逆なものと不可逆なものがあります。
データベースは不要ですが、長さがやや長くなることがあります。
- 可逆アルゴリズムの例(C#):
public static string ShortenString(string longString)
{
byte[] bytes = Encoding.UTF8.GetBytes(longString);
string shortString = Convert.ToBase64String(bytes);
return shortString;
}
public static string RestoreString(string shortString)
{
byte[] bytes = Convert.FromBase64String(shortString);
string longString = Encoding.UTF8.GetString(bytes);
return longString;
}
- 不可逆アルゴリズムの例(C#):
public static string ShortenString(string longString)
{
using (SHA256 sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(longString));
string shortString = Convert.ToBase64String(hashBytes);
return shortString;
}
}
- 違い:
- 可逆アルゴリズムは短い文字列から元の文字列を復元できますが、不可逆アルゴリズムは復元できません。
- 可逆アルゴリズムで生成される短い文字列の長さは長めですが、不可逆アルゴリズムで生成される短い文字列の長さは短めです。
- アルゴリズムを使用して自動的に短い文字列を生成する場合、データベースの読み書きが不要でパフォーマンスは良好ですが、異なる長い文字列が同じ短い文字列を生成する可能性(衝突)があります。
最後に、完全な単体テストを掲載しますので、参考になれば幸いです。
using HashidsNet;
using System.Security.Cryptography;
using System.Text;
namespace Dotnet9.Commons.Test;
[TestClass]
public class HashHelperUnitTest
{
public static string GetHashids(string sourceStr, int number = 9)
{
var hashids = new Hashids(sourceStr);
return hashids.Encode(number);
}
[TestMethod]
public void Hashids_Success()
{
var blogPostSlugStr = "Is-it-possible-to-use-it-as-a-short-link-generator-Hashidsnet";
var encodeStr1 = GetHashids(blogPostSlugStr);
var encodeStr2 = GetHashids(blogPostSlugStr);
Assert.AreEqual(encodeStr1, encodeStr2);
}
[TestMethod]
public void Hashids_Best_Success()
{
var blogPostSlugStr = "Is-it-possible-to-use-it-as-a-short-link-generator-Hashidsnet";
var encodeStr1 = blogPostSlugStr.GetHashids();
var encodeStr2 = ShortenString(blogPostSlugStr);
var encodeStr3 = ShortenString2(blogPostSlugStr);
Assert.IsTrue(encodeStr1.Length < encodeStr2.Length, "Hashids生成的短字符串比Base64还短");
Assert.IsTrue(encodeStr1.Length < encodeStr3.Length, "Hashids生成的短字符串还是短那么一点点");
}
public static string ShortenString(string longString)
{
byte[] bytes = Encoding.UTF8.GetBytes(longString);
string shortString = Convert.ToBase64String(bytes);
return shortString;
}
public static string RestoreString(string shortString)
{
byte[] bytes = Convert.FromBase64String(shortString);
string longString = Encoding.UTF8.GetString(bytes);
return longString;
}
public static string ShortenString2(string longString)
{
using (SHA256 sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(longString));
string shortString = Convert.ToBase64String(hashBytes);
return shortString;
}
}
}
本記事は主に知乎の元記事を引用しています:
原文タイトル:分享.NET/.NET 5 軽量オープンソースの数値を文字列にエンコードする暗号化(短いID生成)ツールクラスライブラリ -- hashids.net
原文リンク:https://codedefault.com/p/lightweight-open-source-project-hashids-net-for-generating-short-ids-from-numbers