Java Tips | 文字列処理:マスク処理

Java Java
スポンサーリンク

マスク処理は「見せていいところだけ見せる」ための技

業務システムでは、「全部は見せちゃダメだけど、ある程度は見せたい情報」がたくさんあります。

クレジットカード番号
電話番号
メールアドレス
会員ID、社員番号、住所の一部…

これらをそのままログや画面に出すと、情報漏えいのリスクが一気に上がります。
そこで使うのが マスク処理――
「文字列の一部を * などで隠し、必要な範囲だけ見せる」テクニックです。

マスク処理は、セキュリティと利便性のバランスを取るための“現場の必須スキル” だと思ってください。


マスク処理の基本発想:「どこを残して、どこを隠すか」

3つの観点でルールを決める

マスク処理を設計するときは、だいたい次の3つを決めます。

どの文字を残すか(先頭何文字/末尾何文字など)
どの文字をマスクするか(残す以外全部/一部だけ)
何でマスクするか(*x など)

例えば、クレジットカード番号なら、よくあるルールはこうです。

先頭6桁と末尾4桁だけ残す
間の桁は全部 * にする

1234567890123456123456******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

に書き換えてみてください。

それだけで、あなたのシステムは「ちょっと怖いシステム」から
「情報の扱い方を分かっているシステム」に、一段階レベルアップします。

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