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}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    #[test]
125    fn test_mobile() { assert_eq!(Mask::mobile("13812345678"), "138****5678"); }
126    #[test]
127    fn test_email() { assert_eq!(Mask::email("alice@mail.com"), "a***@mail.com"); }
128}