C# Tips | 文字列処理:ランダム文字列生成

C# C#
スポンサーリンク
スポンサーリンク

はじめに 「ランダム文字列生成」は“それっぽいIDやトークンを安全に作る”技

業務システムで「ランダム文字列」が欲しくなる場面はたくさんあります。

一時パスワード
メール確認用トークン
招待コード
ファイル名の衝突回避用サフィックス

ここで大事なのは、

「ただの“なんとなくランダム”」ではなく
「用途に応じて“十分に安全で、扱いやすいランダム文字列”」

を作ることです。

ここでは、初心者向けに、

C# でランダム文字列を作る基本パターン
Random を使う場合と、セキュアな乱数(RandomNumberGenerator)を使う場合の違い
文字種(英大文字・小文字・数字など)の設計
業務ユーティリティとしてのまとめ方

を、例題付きでかみ砕いて説明していきます。


ランダム文字列生成の基本の考え方

「やりたいこと」を分解するとこうなる

ランダム文字列を作る処理は、分解するととてもシンプルです。

1文字ずつ、「使ってよい文字の集合」からランダムに1文字選ぶ
それを指定された長さ分だけ繰り返す
最後に文字列として返す

つまり、

「どの文字を使ってよいか(文字セット)」
「どうやってランダムなインデックスを選ぶか(乱数生成)」

この2つを決めれば、あとはループで組み立てるだけです。

まずは「英数字だけ」のランダム文字列を目標にする

最初のゴールとして、

「英大文字・英小文字・数字からなるランダム文字列」

を作れるようになると、かなり多くの用途をカバーできます。

例:"aZ3kP9"、"X7f2Q0bC" など。


基本版:Random を使ったランダム文字列生成

文字セットを決める

まず、「使ってよい文字」を1つの文字列として定義します。

using System;
using System.Text;

public static class RandomStringUtil
{
    private const string DefaultChars =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
        "abcdefghijklmnopqrstuvwxyz" +
        "0123456789";

    public static string Generate(int length)
    {
        if (length <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(length), "length は 1 以上にしてください。");
        }

        var random = new Random();
        var sb = new StringBuilder(length);

        for (int i = 0; i < length; i++)
        {
            int index = random.Next(DefaultChars.Length);
            char c = DefaultChars[index];
            sb.Append(c);
        }

        return sb.ToString();
    }
}
C#

ここでやっていることを言葉で追うと、

DefaultChars に「使ってよい文字」を全部並べておく
Random.Next(DefaultChars.Length) で 0〜文字数-1 のランダムなインデックスを得る
そのインデックスの文字を取り出して StringBuilder に追加
これを length 回繰り返す

という流れです。

動作例

Console.WriteLine(RandomStringUtil.Generate(8));  // 例: aZ3kP9qR
Console.WriteLine(RandomStringUtil.Generate(16)); // 例: X7f2Q0bC9mLp3HsT
C#

毎回違う文字列が生成されます(もちろん、たまたま同じになる可能性はゼロではありませんが、実務的には十分ランダムです)。


Random の“落とし穴”と、実務での注意点

new Random() を毎回 new する問題

上のコードは「サンプルとして」は分かりやすいのですが、
実務でそのまま使うと、1つ問題があります。

new Random() は、デフォルトでは「現在時刻」を種にします。
短時間に何度も new Random() すると、同じ種から同じ乱数列が出てしまうことがあります。

例えば、ループの中で毎回 new Random() していると、
「全部同じランダム文字列ができた」なんてことが起こりえます。

対策:Random を使い回す

簡単な対策は、「Random を static フィールドとして1つだけ持ち回す」ことです。

public static class RandomStringUtil
{
    private static readonly Random _random = new Random();

    private const string DefaultChars =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
        "abcdefghijklmnopqrstuvwxyz" +
        "0123456789";

    public static string Generate(int length)
    {
        if (length <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(length));
        }

        var sb = new StringBuilder(length);

        lock (_random) // マルチスレッド環境を考えるならロックしておく
        {
            for (int i = 0; i < length; i++)
            {
                int index = _random.Next(DefaultChars.Length);
                sb.Append(DefaultChars[index]);
            }
        }

        return sb.ToString();
    }
}
C#

これで、「短時間に大量に呼ばれても、同じ種から同じ列が繰り返される」問題を避けられます。

ただし、セキュリティ用途(パスワード・トークンなど)には Random は不十分です。
そこは次のセクションで扱います。


セキュア版:RandomNumberGenerator を使ったランダム文字列生成

なぜセキュアな乱数が必要か

一時パスワード
認証トークン
パスワードリセット用URLのキー

のような「攻撃者に推測されたくない」用途では、
Random ではなく 暗号論的擬似乱数 を使う必要があります。

C# では、System.Security.Cryptography.RandomNumberGenerator(.NET 6 以降なら RandomNumberGenerator.GetInt32)がそれにあたります。

RandomNumberGenerator.GetInt32 を使う実装

using System;
using System.Security.Cryptography;
using System.Text;

public static class SecureRandomStringUtil
{
    private const string DefaultChars =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
        "abcdefghijklmnopqrstuvwxyz" +
        "0123456789";

    public static string Generate(int length)
    {
        if (length <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(length));
        }

        var sb = new StringBuilder(length);

        for (int i = 0; i < length; i++)
        {
            int index = RandomNumberGenerator.GetInt32(DefaultChars.Length);
            sb.Append(DefaultChars[index]);
        }

        return sb.ToString();
    }
}
C#

RandomNumberGenerator.GetInt32(maxValue) は、
0 以上 maxValue 未満の整数を、暗号論的に安全な乱数として返してくれます。

動作例

Console.WriteLine(SecureRandomStringUtil.Generate(32));
// 例: aZ3kP9qRX7f2Q0bC9mLp3HsT1uVwYz0K
C#

見た目は普通のランダム文字列ですが、
内部では「セキュアな乱数」を使っているので、
攻撃者がパターンを推測するのは非常に困難です。


文字セットを切り替えられるようにする

英数字だけ/数字だけ/大文字だけ…を選べるように

用途によっては、

数字だけのワンタイムコード(例:6桁の数字)
英大文字だけの招待コード
記号も含めた強めのパスワード

など、使いたい文字種が変わります。

そこで、「文字セットを引数で渡せる」ようにしておくと便利です。

public static class SecureRandomStringUtil
{
    public const string CharsUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    public const string CharsLower = "abcdefghijklmnopqrstuvwxyz";
    public const string CharsDigit = "0123456789";

    public static string Generate(int length, string allowedChars)
    {
        if (length <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(length));
        }

        if (string.IsNullOrEmpty(allowedChars))
        {
            throw new ArgumentException("allowedChars は 1 文字以上必要です。", nameof(allowedChars));
        }

        var sb = new StringBuilder(length);

        for (int i = 0; i < length; i++)
        {
            int index = RandomNumberGenerator.GetInt32(allowedChars.Length);
            sb.Append(allowedChars[index]);
        }

        return sb.ToString();
    }

    public static string GenerateAlphaNumeric(int length)
        => Generate(length, CharsUpper + CharsLower + CharsDigit);

    public static string GenerateDigits(int length)
        => Generate(length, CharsDigit);
}
C#

動作例

Console.WriteLine(SecureRandomStringUtil.GenerateAlphaNumeric(12));
// 例: aZ3kP9qRX7f2

Console.WriteLine(SecureRandomStringUtil.GenerateDigits(6));
// 例: 493027

Console.WriteLine(SecureRandomStringUtil.Generate(8, SecureRandomStringUtil.CharsUpper));
// 例: QZMTLPRK
C#

こうしておくと、「どんな文字を使ってよいか」を呼び出し側で柔軟に決められます。


実務での設計ポイント

用途ごとに「どのユーティリティを使うか」を決める

ざっくり分けると、こんな感じの使い分けが現実的です。

ログの相関ID、テスト用のダミーIDなど
→ Random ベースでも十分(ただし Random の使い回しは意識する)

一時パスワード、認証トークン、URLキーなど
→ RandomNumberGenerator ベースのセキュア版を必ず使う

そして、それをコード上で明確にするために、

RandomStringUtil(非セキュア・軽量)
SecureRandomStringUtil(セキュア用途)

のようにクラスを分けておくと、
「間違ってセキュア用途に Random を使ってしまう」事故を防ぎやすくなります。

長さと文字種は“人間の使い勝手”も考える

例えば、一時パスワードをユーザーにメールで送り、
それを手入力してもらう場合、

紛らわしい文字(0 と O、1 と l など)を避ける
長さは 6〜8 文字程度にして、入力負荷を下げる

といった工夫も大事です。

文字セットを自分で定義できるようにしておけば、
こうした「人間側の事情」も簡単に反映できます。


まとめ 「ランダム文字列生成ユーティリティ」は“用途に合わせて乱数の強さと文字種を選ぶ道具”

ランダム文字列生成は、一見シンプルですが、

どの乱数(Random / RandomNumberGenerator)を使うか
どの文字を使ってよいか(文字セット)
どのくらいの長さにするか

といった設計次第で、「安全性」と「使いやすさ」が大きく変わります。

押さえておきたいポイントは、

基本の形は「文字セットからランダムに1文字ずつ選んで連結」
Random はテストや軽い用途向け、セキュリティ用途には不十分
セキュアな用途では RandomNumberGenerator.GetInt32 を使う
文字セットを引数にして、英数字・数字だけ・大文字だけなどを切り替えられるようにする
用途ごとにユーティリティを分けておくと、後から読んだときに意図が分かりやすい

ここまで理解できれば、「なんとなくランダムっぽい文字列を作っている」段階から一歩進んで、
“業務要件とセキュリティを両立したランダム文字列ユーティリティ”を、自分の手で設計・実装できるようになっていきます。

タイトルとURLをコピーしました