Skip to main content

alun_utils/
mask.rs

1//! 敏感信息脱敏工具
2
3use regex::Regex;
4use serde_json::{Map, Value};
5use std::sync::LazyLock;
6
7static MOBILE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^1[3-9]\d{9}$").unwrap());
8static ID_CARD_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\d{17}[\dXx]$").unwrap());
9static EMAIL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[\w.\-]+@[\w.\-]+\.\w+$").unwrap());
10
11/// 递归对 JSON 进行脱敏
12///
13/// 遍历 JSON 对象的所有字段,对匹配 `sensitive_fields` 的字段名、
14/// 或字段值内容匹配手机号/身份证/邮箱格式的值进行脱敏。
15///
16/// # 参数
17///
18/// * `value` - 待脱敏的 JSON
19/// * `sensitive_fields` - 敏感字段名列表
20pub fn mask_json_value(value: Value, sensitive_fields: &[&str]) -> Value {
21    match value {
22        Value::Object(map) => mask_object(map, sensitive_fields),
23        Value::Array(arr) => Value::Array(arr.into_iter().map(|v| mask_json_value(v, sensitive_fields)).collect()),
24        other => mask_scalar_if_needed(other),
25    }
26}
27
28fn mask_object(map: Map<String, Value>, sensitive_fields: &[&str]) -> Value {
29    let mut masked = Map::new();
30    for (key, val) in map {
31        let is_sensitive = sensitive_fields.iter().any(|f| {
32            f.eq_ignore_ascii_case(&key)
33        });
34        if is_sensitive {
35            masked.insert(key, Value::String("****".into()));
36        } else {
37            masked.insert(key, mask_json_value(val, sensitive_fields));
38        }
39    }
40    Value::Object(masked)
41}
42
43fn mask_scalar_if_needed(val: Value) -> Value {
44    match &val {
45        Value::String(s) => {
46            let s = s.trim();
47            if s.is_empty() { return val; }
48            if s.len() >= 11 && (MOBILE_RE.is_match(s) || s.len() == 11 && s.starts_with('1')) {
49                return Value::String(mask_mobile(s));
50            }
51            if s.len() >= 15 && s.len() <= 20 && contains_alpha_numeric(s) {
52                if ID_CARD_RE.is_match(s) {
53                    return Value::String(mask_id_card(s));
54                }
55            }
56            if EMAIL_RE.is_match(s) {
57                return Value::String(mask_email(s));
58            }
59            val
60        }
61        _ => val,
62    }
63}
64
65fn contains_alpha_numeric(s: &str) -> bool {
66    s.chars().any(|c| c.is_ascii_digit())
67}
68
69fn mask_mobile(s: &str) -> String {
70    if s.len() < 7 { return s.to_string(); }
71    format!("{}****{}", &s[..3], &s[s.len()-4..])
72}
73
74fn mask_id_card(s: &str) -> String {
75    if s.len() < 8 { return s.to_string(); }
76    format!("{}****{}", &s[..4], &s[s.len()-4..])
77}
78
79fn mask_email(s: &str) -> String {
80    if let Some(at) = s.find('@') {
81        let prefix = &s[..at];
82        if prefix.len() <= 2 { format!("*{}", &s[at..]) }
83        else { format!("{}***{}", &prefix[..1], &s[at..]) }
84    } else { s.to_string() }
85}
86
87/// 敏感信息脱敏工具 —— 对手机号、邮箱、身份证、银行卡、人名等进行部分遮盖
88///
89/// # 示例
90///
91/// ```ignore
92/// use alun_utils::Mask;
93/// assert_eq!(Mask::mobile("13812345678"), "138****5678");
94/// assert_eq!(Mask::email("alice@mail.com"), "a***@mail.com");
95/// ```
96pub struct Mask;
97
98impl Mask {
99    /// 手机号脱敏:保留前3后4位
100    pub fn mobile(phone: &str) -> String { mask_mobile(phone) }
101    /// 邮箱脱敏:保留首字符和域名部分
102    pub fn email(email: &str) -> String { mask_email(email) }
103    /// 身份证脱敏:保留前4后4位
104    pub fn id_card(id: &str) -> String { mask_id_card(id) }
105    /// 银行卡脱敏:保留前4后4位,中间用 ` **** ` 分隔
106    pub fn bank_card(card: &str) -> String {
107        if card.len() < 8 { return card.to_string(); }
108        format!("{} **** {}", &card[..4], &card[card.len()-4..])
109    }
110    /// 姓名脱敏:保留首字符,其余用 `*` 代替
111    pub fn name(name: &str) -> String {
112        let chars: Vec<char> = name.chars().collect();
113        if chars.len() <= 1 { return name.to_string(); }
114        let mut result = String::new();
115        result.push(chars[0]);
116        for _ in 1..chars.len() { result.push('*'); }
117        result
118    }
119    /// 用户ID脱敏:保留前2后2字符
120    pub fn user_id(id: &str) -> String {
121        if id.len() <= 4 { return "****".to_string(); }
122        format!("{}****{}", &id[..2], &id[id.len()-2..])
123    }
124    /// 密码脱敏:固定返回 `******`
125    pub fn password(_password: &str) -> String {
126        "******".to_string()
127    }
128    /// 地址脱敏:保留前6个字符(省市区),其余用 `****` 代替
129    pub fn address(addr: &str) -> String {
130        if addr.len() <= 6 { return "****".to_string(); }
131        format!("{}****", &addr[..addr.len().min(6)])
132    }
133    /// 车牌号脱敏:保留首字符(省份简称)和末位,中间用 `****` 代替
134    pub fn license_plate(plate: &str) -> String {
135        let chars: Vec<char> = plate.chars().collect();
136        if chars.len() < 3 { return "****".to_string(); }
137        format!("{}****{}", chars[0], chars[chars.len()-1])
138    }
139    /// 根据数据类型自动选择脱敏方式
140    ///
141    /// # 参数
142    /// - `data_type`: 数据类型(user_id/mobile/password/address/bank_card/license_plate/email/id_card/name)
143    /// - `value`: 待脱敏的值
144    pub fn mask_by_type(data_type: &str, value: &str) -> String {
145        match data_type {
146            "user_id" => Self::user_id(value),
147            "mobile" => Self::mobile(value),
148            "password" => Self::password(value),
149            "address" => Self::address(value),
150            "bank_card" => Self::bank_card(value),
151            "license_plate" => Self::license_plate(value),
152            "email" => Self::email(value),
153            "id_card" => Self::id_card(value),
154            "name" => Self::name(value),
155            _ => format!("{}***{}", &value[..value.len().min(2)], &value[value.len()-value.len().min(2)..]),
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    #[test]
164    fn test_mobile() { assert_eq!(Mask::mobile("13812345678"), "138****5678"); }
165    #[test]
166    fn test_email() { assert_eq!(Mask::email("alice@mail.com"), "a***@mail.com"); }
167}