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 を使ってしまう」事故を防ぎやすくなります。

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

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

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

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

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


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

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

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

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

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

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

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

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