マスク処理は「見せていいところだけ見せる」ための技
業務システムでは、「全部は見せちゃダメだけど、ある程度は見せたい情報」がたくさんあります。
クレジットカード番号
電話番号
メールアドレス
会員ID、社員番号、住所の一部…
これらをそのままログや画面に出すと、情報漏えいのリスクが一気に上がります。
そこで使うのが マスク処理――
「文字列の一部を * や ● などで隠し、必要な範囲だけ見せる」テクニックです。
マスク処理は、セキュリティと利便性のバランスを取るための“現場の必須スキル” だと思ってください。
マスク処理の基本発想:「どこを残して、どこを隠すか」
3つの観点でルールを決める
マスク処理を設計するときは、だいたい次の3つを決めます。
どの文字を残すか(先頭何文字/末尾何文字など)
どの文字をマスクするか(残す以外全部/一部だけ)
何でマスクするか(*、●、x など)
例えば、クレジットカード番号なら、よくあるルールはこうです。
先頭6桁と末尾4桁だけ残す
間の桁は全部 * にする
1234567890123456 → 123456******3456
電話番号なら、こんなルールもあります。
末尾4桁だけ残す
それ以外は * にする
09012345678 → *******5678
ここで大事なのは、「ルールを“文字数ベース”で決めて、それをユーティリティに閉じ込める」ことです。
毎回手書きで substring していると、必ずどこかでミスります。
汎用マスクユーティリティの設計
「先頭を残す」「末尾を残す」をパラメータ化する
まずは、「先頭N文字と末尾M文字だけ残して、間をマスク文字で埋める」という汎用ユーティリティを作ります。
public final class Mask {
private Mask() {}
public static String maskMiddle(String text, int head, int tail, char maskChar) {
if (text == null) {
return null;
}
int len = text.length();
if (len == 0) {
return text;
}
// 残すべき文字数が全体以上なら、そのまま返す
if (head + tail >= len) {
return text;
}
StringBuilder sb = new StringBuilder(len);
// 先頭部分
sb.append(text, 0, head);
// 中央のマスク部分
int maskCount = len - head - tail;
for (int i = 0; i < maskCount; i++) {
sb.append(maskChar);
}
// 末尾部分
sb.append(text, len - tail, len);
return sb.toString();
}
}
Java使い方はこうなります。
System.out.println(Mask.maskMiddle("1234567890123456", 6, 4, '*'));
// 123456******3456
System.out.println(Mask.maskMiddle("09012345678", 0, 4, '*'));
// *******5678
System.out.println(Mask.maskMiddle("abcdef", 2, 2, 'x'));
// abxxef
System.out.println(Mask.maskMiddle("abc", 2, 2, '*')); // 残す数が多すぎるのでそのまま
// abc
Javaここで深掘りしたい重要ポイントは三つです。
一つ目は、「“残すべき文字数が全体以上なら、そのまま返す”という安全側の挙動」です。head + tail >= len のときに無理にマスクしようとすると、
インデックス計算がややこしくなりますし、「全部見えてもいい」と判断しているケースも多いです。
そこで、「残す数が多すぎるならマスクしない」というシンプルなルールにしています。
二つ目は、「マスク部分の長さを len - head - tail で計算している」ことです。
これにより、文字列の長さが変わらず、
「元の桁数は分かるけど、中身は見えない」という状態を保てます。
カード番号や電話番号など、「桁数自体に意味がある」データと相性がいいです。
三つ目は、「マスク文字を引数で渡せるようにしている」ことです。* にするか ● にするかは、システムや画面のデザインポリシー次第です。
ユーティリティ側で固定せず、呼び出し側で選べるようにしておくと、再利用性が高まります。
例題:クレジットカード番号のマスク
先頭6桁+末尾4桁を残す、よくある実務ルール
クレジットカード番号のマスクは、実務でほぼ定番のパターンです。
public final class CardMask {
private CardMask() {}
public static String maskCardNumber(String cardNumber) {
if (cardNumber == null) {
return null;
}
// 数字以外(スペースやハイフン)を一旦取り除くかどうかは要件次第
return Mask.maskMiddle(cardNumber, 6, 4, '*');
}
}
Java使い方はこうです。
System.out.println(CardMask.maskCardNumber("1234567890123456"));
// 123456******3456
Javaここでのポイントは、「“カード番号用”という意味を持つ薄いラッパーを作っている」ことです。Mask.maskMiddle(cardNumber, 6, 4, '*') だけでも動きますが、CardMask.maskCardNumber(cardNumber) という名前があることで、
「これはカード番号のマスクなんだ」
「先頭6桁+末尾4桁というルールなんだ」
と、コードを読む人に意図が伝わりやすくなります。
例題:メールアドレスのマスク
「ローカル部だけ一部隠す」というよくあるパターン
メールアドレスは、@ の前(ローカル部)だけを部分的にマスクすることが多いです。
public final class EmailMask {
private EmailMask() {}
public static String maskEmail(String email) {
if (email == null) {
return null;
}
int at = email.indexOf('@');
if (at <= 0) {
// メールアドレスっぽくない場合は、そのまま返すか、全体マスクするか要件次第
return email;
}
String local = email.substring(0, at);
String domain = email.substring(at); // '@' 含む
// ローカル部の先頭1文字だけ残して、残りをマスク
String maskedLocal;
if (local.length() <= 1) {
maskedLocal = "*";
} else {
maskedLocal = local.charAt(0) + repeat('*', local.length() - 1);
}
return maskedLocal + domain;
}
private static String repeat(char ch, int count) {
StringBuilder sb = new StringBuilder(count);
for (int i = 0; i < count; i++) {
sb.append(ch);
}
return sb.toString();
}
}
Java使い方はこうです。
System.out.println(EmailMask.maskEmail("taro@example.com"));
// t********@example.com
System.out.println(EmailMask.maskEmail("a@ex.com"));
// *@ex.com
Javaここでの重要ポイントは、「“どこまで見せるか”をデータの構造に合わせて決めている」ことです。
メールアドレスは @ でローカル部とドメイン部に分かれるので、
「ドメインはそのまま」「ローカル部だけマスク」というルールが自然です。
汎用マスクユーティリティ(Mask.maskMiddle)だけでは表現しづらい、
「構造を意識したマスク」は、こうして専用メソッドとして切り出すとスッキリします。
例題:ログ用の「ざっくりマスク」
「完全に隠す」か「一部だけ見せる」かを使い分ける
ログに個人情報を出すときは、
「そもそも出さない」が最優先ですが、
どうしてもトラブル調査のために一部を残したいこともあります。
例えば、会員IDは末尾3桁だけ残す、など。
public final class LogMask {
private LogMask() {}
public static String maskForLog(String value) {
if (value == null) {
return null;
}
// 長さが短いものは全部マスク
if (value.length() <= 3) {
return repeat('*', value.length());
}
// 末尾3文字だけ残す
int tail = 3;
int maskCount = value.length() - tail;
return repeat('*', maskCount) + value.substring(value.length() - tail);
}
private static String repeat(char ch, int count) {
StringBuilder sb = new StringBuilder(count);
for (int i = 0; i < count; i++) {
sb.append(ch);
}
return sb.toString();
}
}
Java使い方はこうです。
System.out.println(LogMask.maskForLog("1234567890"));
// *******890
System.out.println(LogMask.maskForLog("abc"));
// ***
Javaここでのポイントは、「ログ用のマスクは“調査に必要な最小限だけ残す”という発想で設計する」ことです。
全部隠すと調査ができない。
全部見せると危険。
その間の「ギリギリのライン」を、項目ごとに決めていくイメージです。
マスク処理で絶対に意識してほしいこと
「どこでマスクするか」を設計として決める
マスク処理は、「どこでやるか」 がとても重要です。
DBに保存する前にマスクするのか
画面に出す直前でマスクするのか
ログに書く直前でマスクするのか
おすすめは、
DBには“生の値”を保存する(暗号化は別の話)
画面やログなど「人の目に触れるところ」に出す直前でマスクする
という設計です。
理由はシンプルで、
DBにマスク済みの値しか残っていないと、
後から正しい値を使った集計や連携ができなくなるからです。
一方で、画面やログは「見せ方の問題」なので、
「出力層でマスクする」 という責務分担にしておくと、
どこで何が見えているのかを追いやすくなります。
まとめ:マスク処理ユーティリティで身につけたい感覚
マスク処理は、「情報を完全に隠す」のではなく、
「見せていい範囲だけを慎重に残す」 ためのテクニックです。
押さえておきたい感覚は、まず「先頭何文字/末尾何文字を残すか」をルールとして決め、
それを Mask.maskMiddle のような汎用ユーティリティに落とし込むこと。
次に、カード番号・メールアドレス・ログ用IDなど、
用途ごとの“意味のあるマスク”を薄いラッパーとして用意すること。
そして何より、「マスクは“出力の責務”であり、DBや内部処理では生の値を扱う」という線引きを、
コードと設計でハッキリさせておくことです。
もしあなたのコードのどこかに、
logger.info("card={}", cardNumber);
Javaのような行があったら、
そこを題材にして、
logger.info("card={}", CardMask.maskCardNumber(cardNumber));
Javaに書き換えてみてください。
それだけで、あなたのシステムは「ちょっと怖いシステム」から
「情報の扱い方を分かっているシステム」に、一段階レベルアップします。

