できない.dev

ランダムな文字列を生成するには

英数字などの文字集合から指定した長さのランダムな文字列を作る方法を、7 言語で示す。
トークンに使える暗号用の乱数への切り替えと、剰余による偏りを避ける書き方までを扱う。

公開:

選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。

Python

import secrets
import string
 
alphabet = string.ascii_letters + string.digits  # 英大文字・英小文字・数字の 62 種類
token = "".join(secrets.choice(alphabet) for _ in range(16))
print(token)       # 例: kX3pQ9aVb2LmZ7Rt(16 文字。実行のたびに変わる)
print(len(token))  # 16
 
print(secrets.token_hex(8))      # 例: 9f2c41ab07d3e856(8 バイト = 16 桁の 16 進数)
print(secrets.token_urlsafe(12)) # 例: Qm1b_X9tLw3KzA7d(12 バイト = 16 文字の URL セーフな文字列)

secrets.choice で文字集合から 1 文字ずつ選んで連結すると、推測されにくい文字列になる。
random.choice でも同じ形で書けるが、random モジュールの乱数は予測できる擬似乱数なので、トークンやパスワードには使わない。
長さが決まっていてよいなら、secrets.token_hex や secrets.token_urlsafe が短く書ける。
ただし引数は文字数ではなくバイト数である。

JavaScript

const { randomInt, randomBytes } = require("node:crypto");
 
const alphabet =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // 62 種類
 
function randomString(length) {
  let result = "";
  for (let i = 0; i < length; i++) {
    result += alphabet[randomInt(alphabet.length)]; // 0 以上 62 未満の整数で 1 文字選ぶ
  }
  return result;
}
 
console.log(randomString(16)); // 例: kX3pQ9aVb2LmZ7Rt(16 文字。実行のたびに変わる)
console.log(randomBytes(8).toString("hex"));       // 例: 9f2c41ab07d3e856(16 桁の 16 進数)
console.log(randomBytes(12).toString("base64url")); // 例: Qm1b_X9tLw3KzA7d(16 文字の URL セーフな文字列)

crypto.randomInt で文字集合の添字を選ぶと、剰余による偏りが出ない。
Math.random() は暗号用途に向かないので、トークンには使わない。
短く書きたいときは randomBytes の結果を toString("hex") や toString("base64url") で文字列にする。
引数はバイト数なので、12 バイトが Base64URL で 16 文字になる。

TypeScript

const alphabet =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // 62 種類
 
// ブラウザでも Node.js でも使える。暗号用途に向く
function randomString(length: number): string {
  const limit = 256 - (256 % alphabet.length); // 248。これ以上の値は偏るので捨てる
  let result = "";
  while (result.length < length) {
    const bytes = crypto.getRandomValues(new Uint8Array(length));
    for (const b of bytes) {
      if (b < limit && result.length < length) {
        result += alphabet[b % alphabet.length];
      }
    }
  }
  return result;
}
 
console.log(randomString(16)); // 例: kX3pQ9aVb2LmZ7Rt(実行のたびに変わる)
console.log(randomString(16).length); // 16

crypto.getRandomValues は OS の乱数を返すので、ブラウザでも Node.js でも暗号用途に使える。
バイト値 (0〜255) を 62 で割った余りで文字を選ぶと、256 が 62 で割り切れないため先頭の 8 文字が出やすくなる。
そこで 248 以上の値は捨てて引き直している。

できないときReact のハイドレーションエラー(Hydration mismatch)の原因と直し方

Go

package main
 
import (
	"crypto/rand"
	"fmt"
	"math/big"
)
 
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" // 62 種類
 
func randomString(length int) (string, error) {
	max := big.NewInt(int64(len(alphabet)))
	b := make([]byte, length)
	for i := range b {
		n, err := rand.Int(rand.Reader, max) // 0 以上 62 未満
		if err != nil {
			return "", err
		}
		b[i] = alphabet[n.Int64()]
	}
	return string(b), nil
}
 
func main() {
	s, err := randomString(16)
	if err != nil {
		panic(err)
	}
	fmt.Println(s) // 例: kX3pQ9aVb2LmZ7Rt(16 文字。実行のたびに変わる)
 
	fmt.Println(rand.Text()) // 例: 4FQ2LZ7WXK3VBN5HJD6P8RTMYA(Go 1.24 以降。26 文字の Base32)
}

crypto/rand の rand.Int に文字集合の長さを渡すと、0 以上その長さ未満の整数を偏りなく得られるので、1 文字ずつ選んで並べる。
Go 1.24 以降は rand.Text() が 26 文字の Base32 文字列を返すので、文字集合を問わないトークンならこれで足りる。
math/rand は暗号用途に向かない。

Rust

// Cargo.toml: rand = "0.10"
use rand::distr::Alphanumeric;
use rand::RngExt;
 
fn main() {
    let s: String = rand::rng()
        .sample_iter(&Alphanumeric)
        .take(16)
        .map(char::from)
        .collect();
    println!("{}", s); // 例: kX3pQ9aVb2LmZ7Rt(16 文字。実行のたびに変わる)
}

標準ライブラリに乱数は無いため、rand クレートを Cargo.toml に追加する。
sample_iter(&Alphanumeric) で英数字 62 種類から 1 文字ずつ取り出し、take(16) で長さを決めて String にまとめる。
0.10 系では sample_iter を呼ぶのに RngExt トレイトの use が要る。
rand::rng() は OS から種を得る暗号論的擬似乱数として設計されている。

できないときRust「failed to resolve: use of undeclared crate or module」の原因と直し方

Java

import java.security.SecureRandom;
 
public class Main {
    private static final String ALPHABET =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // 62 種類
    private static final SecureRandom RANDOM = new SecureRandom();
 
    static String randomString(int length) {
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            sb.append(ALPHABET.charAt(RANDOM.nextInt(ALPHABET.length()))); // 0 以上 62 未満
        }
        return sb.toString();
    }
 
    public static void main(String[] args) {
        System.out.println(randomString(16)); // 例: kX3pQ9aVb2LmZ7Rt(16 文字。実行のたびに変わる)
    }
}

SecureRandom の nextInt(bound) で文字集合の添字を選ぶ。
SecureRandom は暗号用途向けの乱数で、java.util.Random はトークンに向かない。
SecureRandom はスレッドセーフなので、インスタンスを static final で 1 つ持って使い回してよい。

C#

using System;
using System.Security.Cryptography;
 
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // 62 種類
 
// .NET 8 以降。指定した文字集合から暗号用途向けの乱数で選ぶ
Console.WriteLine(RandomNumberGenerator.GetString(alphabet, 16)); // 例: kX3pQ9aVb2LmZ7Rt(16 文字。実行のたびに変わる)

RandomNumberGenerator.GetString は文字集合と長さを渡すだけで、暗号用途向けの乱数から偏りなく文字列を作る。
.NET 8 で追加されたメソッドで、それ以前の版では自分で GetInt32 を呼んで 1 文字ずつ選ぶ。
System.Random はトークンに向かない。

トークンには暗号用の乱数を使う

ランダムな文字列を作る処理は、乱数の関数を選ぶところで安全性が決まる。
ログイン用のトークン、パスワードのリセットリンク、API キーのように、推測されると困る値には OS の乱数に基づく関数を使う。

  • Python は secrets
  • JavaScript は crypto.randomInt と crypto.randomBytes
  • TypeScript は crypto.getRandomValues
  • Go は crypto/rand
  • Rust は rand::rng()
  • Java は SecureRandom
  • C# は RandomNumberGenerator

random.choice や Math.random() でも同じ長さの文字列は作れて、見た目では区別がつかない。
画面に出すだけの仮の ID なら十分だが、そのまま認証に使うと危ない。

62 種類で 16 文字なら約 95 ビット

英大文字・英小文字・数字の 62 種類から 16 文字を選ぶと、組み合わせは 62 の 16 乗で、約 95 ビットの乱数になる。
文字集合を狭めるなら、そのぶん文字数を増やして強さをそろえる。

つまずき

Rust で Cargo.toml に rand を書かずに実行すると、use rand::RngExt; の行で E0432、rand::rng() の行で E0433 になり、ビルドが止まる。
rustc 1.99.1 では次のように表示された。

  • unresolved import rand
  • cannot find module or crate rand in this scope
  • if you wanted to use a crate named rand, use cargo add rand to add it to your Cargo.toml

cargo add rand で依存を足せば直る。

乱数のバイト値 (0〜255) を 62 で割った余りだけで文字を決めると、256 が割り切れないぶん先頭の 8 文字が出やすくなる。
TypeScript の版は 248 以上を捨てて引き直しており、62 種類を 20,000 回 (16 文字ずつ、計 320,000 文字) 作って数えたところ、各文字が 4,953〜5,345 回に収まり、期待値の約 5,161 回から大きくは外れなかった。

文字数とバイト数も取り違えやすい。
secrets.token_urlsafe(12) や randomBytes(12).toString("base64url") の 12 は文字数ではなくバイト数で、結果は 16 文字になる。

React の描画中に乱数で文字列を作ると、サーバーが作った HTML と初回描画で値が食い違い、hydration mismatch の警告が出る。
作るのは useEffect の中か、イベントの中にする。