引言

数据脱敏是一种通过对敏感信息进行处理,以保护数据隐私和安全的技术手段。它通过对数据进行伪装、加密、匿名化或模糊处理,使得未经授权的人员无法识别或还原原始数据,从而防止数据泄露和滥用。常见的脱敏技术包括替换、掩码、随机化和数据混淆等。数据脱敏广泛应用于金融、医疗、互联网等行业,在数据测试、开发和共享过程中,确保了数据的安全性和隐私性,同时又不影响数据的整体分析和使用价值

二、哪些数据需要脱敏

个人身份信息(PII):

  • 姓名:包括用户的真实姓名、昵称等。如:赵侠客 脱敏为 赵*客
  • 身份证号:如社会保障号码、身份证号码等。如:342822199202230227 脱敏为 ***************0227
  • 出生日期:完整的生日信息。如:将"1990-05-15"脱敏为"1990-0*-1*"
  • 地址:家庭住址、邮寄地址等。如:“北京市海淀区中关村大街27号”脱敏为“北京市海淀区
  • 联系方式:电话号码、电子邮件地址等。如:18283741313 脱敏为:182****1313

金融信息:

  • 信用卡信息:信用卡号、有效期、安全码等。
  • 银行账户信息:银行账户号码、路由号码等。
  • 交易记录:包括交易时间、交易金额、交易对象等。

医疗信息:

  • 医疗记录:包括病史、诊断信息、治疗记录等。
  • 保险信息:健康保险号码、保险公司信息等。

登录凭证和认证信息:

  • 用户名和密码:账户登录名、密码、加密后的密码等。
  • 多因素认证信息:包括安全问题答案、二次认证代码等。

设备和网络信息:

  • IP地址:用户的公网IP地址和内网IP地址。
  • MAC地址:设备的物理地址。
  • 设备标识符:如设备ID、序列号等。

行为和偏好数据:

  • 浏览历史:用户访问的网站、点击记录等。
  • 搜索记录:用户的搜索查询记录。
  • 购买历史:用户在电商平台上的购买记录、购物车内容等。

通信内容:

  • 电子邮件:邮件内容、附件等。
  • 聊天记录:即时通讯工具中的聊天记录、消息内容等。
  • 社交媒体信息:用户在社交媒体上的发言、评论、私信等。

地理位置数据:

  • GPS数据:设备的精确地理位置。
  • 位置历史:用户过去的位置信息记录。

工作和教育信息:

  • 工作记录:工作单位、职位、薪资等。
  • 教育记录:学校、学历、学位等。

客户服务数据:

  • 客服记录:用户与客服的交流记录、服务请求内容等。

三、传统脱敏代码

3.1 自己写代码

我想大部公司都有自己的一套工具类,自己写代码脱敏可能就是自己写一个工具类,然后放到公共包中,其它项目需要使用引用这个公共包就可以调用,如以下:

public class StrUtil {
/**
 * 脱敏手机号码
 * 将手机号码的中间4位替换为星号
 *
 * @param phoneNumber 原始手机号码
 * @return 脱敏后的手机号码
 */
public static String maskPhoneNumber(String phoneNumber) {
    if (phoneNumber == null || phoneNumber.length() != 11) {
        throw new IllegalArgumentException("手机号码格式不正确");
    }
    return phoneNumber.substring(0, 3) + "****" + phoneNumber.substring(7);
}

public static void main(String[] args) {
    String phoneNumber = "13812345678";
    String maskedPhoneNumber = maskPhoneNumber(phoneNumber);
    System.out.println("原始手机号码: " + phoneNumber);
    System.out.println("脱敏手机号码: " + maskedPhoneNumber);
}
}

3.2 使用第三方工具如:Hutool

很多第三方工具类都提供了字段脱敏的工具类,如Hutool,我们可以很方便的调用DesensitizedUtil.desensitized来完成数据脱敏,如以下代码

//D.d为DesensitizedUtil.desensitized简写方便阅读
//DT为DesensitizedType的缩写
D.d("admin", DT.USER_ID);
D.d("赵侠客", DT.CHINESE_NAME);
D.d("323455100993934554", DT.ID_CARD);
D.d("0571-8331223", DT.FIXED_PHONE);
D.d("18034232232",DT.MOBILE_PHON);
D.d("浙江省杭州市西湖区西湖大道1号", DT.ADDRESS);
D.d("zhaoxiake@163.com", DT.EMAIL);
D.d("123456789", DT.PASSWORD);
D.d("浙A888888", DT.CAR_LICENSE);
D.d("62122612028837228932", DT.BANK_CARD);
D.d("10.100.12.12", DesensitDTizedType.IPV4);
D.d("2001:0db8:85a3:0000:0000:8a2e:0370:7334", DT.IPV6);
D.d("1231231", DT.FIRST_MASK);

输出:

0
    赵**
    3***************54
    3234************54
    057******2234
    浙江省杭州市西********
    z********@163.com
    *********
    浙A8****8
    6212 **** **** **** 8932
    10.*.*.*
    2001:*:*:*:*:*:*:*
    1******

3.3 这些方法的缺点

我觉得这些方法有以下缺点:

  • 不够建优雅,每次使用都需要手动调用
  • 无法通用,如果产品提出个性手机号脱敏显示规则,这些方法都不能通用了
  • 没有面向对象,这些方法没有融入到项目框架中,还是面向过程化编写

四、基于Jackson优雅脱敏

4.1 使用方法

首先我们看一下如何使用,看看是不是很优雅,添加@FieldAnonymize注解,指定脱敏策略,如果想自定义增加自己的脱敏方法如:testStrategy,就不需要写其它代码了

@Data
public class SentiveUser {

private Long id;
@FieldAnonymize("testStrategy")
private String username;
@FieldAnonymize(AnonymizeType.mobile)
private String mobile;
@FieldAnonymize(AnonymizeType.email)
private String email;
@FieldAnonymize(AnonymizeType.chineseName)
private String chineseName;
@FieldAnonymize(AnonymizeType.idCard)
private String idCard;
@FieldAnonymize(AnonymizeType.phone)
private String phone;
@FieldAnonymize(AnonymizeType.address)
private String address;
@FieldAnonymize(AnonymizeType.bankCard)
private String bankCard;
@FieldAnonymize(AnonymizeType.password)
private String password;
@FieldAnonymize(AnonymizeType.carNumber)
private String carNumber;

}

使用方法:

@Test
public void testAnonymize()  {
    
//自定义脱敏策略
AnonymizeImpl strategy= new AnonymizeImpl().addStrategy("testStrategy", t -> t + "***test***");
AnonymizeSerializer.setAnonymizeStrategy(strategy);
SentiveUser sentiveUser=new SentiveUser();
sentiveUser.setMobile("0571-85312234");
sentiveUser.setUsername("admin");
sentiveUser.setEmail("zhaoxiake@163.com");
sentiveUser.setChineseName("赵侠客");
sentiveUser.setAddress("浙江省杭州市西湖区西湖大道1号");
sentiveUser.setPhone("180723432123");
sentiveUser.setIdCard("323455100993934554");
sentiveUser.setBankCard("62122612028837228932");
sentiveUser.setCarNumber("浙AA1126");
sentiveUser.setPassword("Aa123456");
String json=JsonUtils.toJson(sentiveUser);
System.out.println(json);
        
}

这样我们所有JSON序列化输出结果都脱敏了,包括使用SpringBoot开发接口返回的JSON数据 ,前提是使用了Jackson做JSON数据序列化:

{
    "username": "admin***test***",
    "mobile": "057********34",
    "email": "zha******@163.com",
    "chineseName": "赵**",
    "idCard": "**************4554",
    "phone": "********2123",
    "address": "浙江省杭州市西********",
    "bankCard": "621226**********8932",
    "password": "********",
    "carNumber": "浙A****6"
}

4.2 实现步骤

定义注解:

@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = AnonymizeSerializer.class)
public @interface FieldAnonymize {
  String value();
}

定义接口:

public interface AnonymizeType {
    String chineseName = "chineseName";
    String idCard = "idCard";
    String phone = "phone";
    String mobile = "mobile";
    String address = "address";
    String email = "email";
    String bankCard = "bankCard";
    String password = "password";
    String carNumber = "carNumber";
}

public interface IAnonymize
 {
   default String handle(String type, String value) {
     return ((Function<String, String>)getStrategyFunctionMap().get(type)).apply(value);
   }

   Map<String, Function<String, String>> getStrategyFunctionMap();
 }
 
 public interface IJacksonSerializer extends ContextualSerializer {
    default JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property) throws JsonMappingException {
        if (null != property) {
            return getJsonSerializer(provider, property);
        }
        return provider.findNullValueSerializer(null);
    }

    default <A extends Annotation> A getAnnotation(BeanProperty property, Class<A> clazz) {
        Annotation annotation = property.getAnnotation(clazz);
        if (null == annotation) {
            annotation = property.getContextAnnotation(clazz);
        }
        return (A) annotation;
    }

    JsonSerializer<?> getJsonSerializer(SerializerProvider paramSerializerProvider, BeanProperty paramBeanProperty) throws JsonMappingException;
}

默认实现:

public class AnonymizeImpl implements IAnonymize {

    private static Map<String, Function<String, String>> STRATEGY_FUNCTION_MAP;

    public AnonymizeImpl() {
         STRATEGY_FUNCTION_MAP = new HashMap<>();
         STRATEGY_FUNCTION_MAP.put(AnonymizeType.chineseName, DefaultAnonymize::chineseName);
         STRATEGY_FUNCTION_MAP.put(AnonymizeType.idCard, DefaultAnonymize::idCard);
         STRATEGY_FUNCTION_MAP.put(AnonymizeType.phone, DefaultAnonymize::phone);
         STRATEGY_FUNCTION_MAP.put(AnonymizeType.mobile, DefaultAnonymize::mobile);
         STRATEGY_FUNCTION_MAP.put(AnonymizeType.address, DefaultAnonymize::address);
         STRATEGY_FUNCTION_MAP.put(AnonymizeType.email, DefaultAnonymize::email);
         STRATEGY_FUNCTION_MAP.put(AnonymizeType.bankCard, DefaultAnonymize::bankCard);
         STRATEGY_FUNCTION_MAP.put(AnonymizeType.password, DefaultAnonymize::password);
         STRATEGY_FUNCTION_MAP.put(AnonymizeType.carNumber, DefaultAnonymize::carNumber);
    }

    public Map<String, Function<String, String>> getStrategyFunctionMap() {
        return STRATEGY_FUNCTION_MAP;
    }

    public AnonymizeImpl addStrategy(String paramString, Function<String, String> paramFunction) {
        STRATEGY_FUNCTION_MAP.put(paramString, paramFunction);
        return this;
    }


}

定义序列化器:

public class AnonymizeSerializer extends JsonSerializer<String> implements IJacksonSerializer {
    private static IAnonymize ANONYMIZE_STRATEGY;
    private String type;

    public AnonymizeSerializer() {
    }

    public AnonymizeSerializer(String type) {
        this.type = type;
    }

    public static void setAnonymizeStrategy(IAnonymize anonymizeStrategy) {
        ANONYMIZE_STRATEGY = anonymizeStrategy;
    }


    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (null == ANONYMIZE_STRATEGY) {
            throw new RuntimeException("You used the annotation `@FieldAnonymize` but did not inject `AnonymizeStrategy`");
        }
        Object fieldValue = ANONYMIZE_STRATEGY.handle(this.type, value);
        gen.writeObject(fieldValue);
    }


    public JsonSerializer<?> getJsonSerializer(SerializerProvider provider, BeanProperty property) throws JsonMappingException {
        if (Objects.equals(property.getType().getRawClass(), String.class)) {
            FieldAnonymize anonymizeInfo = getAnnotation(property, FieldAnonymize.class);
            if (null != anonymizeInfo) {
                return new AnonymizeSerializer(anonymizeInfo.value());
            }
        }
        return provider.findValueSerializer(property.getType(), property);
    }
}

定义默认脱敏方法:

public class DefaultAnonymize {
    /**
     * 对中文姓名进行脱敏处理,保留首字,其余用*替代
     * 例如:脱敏前:张三  脱敏后:张*
     *
     * @param originalChineseName 原始的中文姓名字符串
     * @return 脱敏处理后的中文姓名字符串
     */
    public static String chineseName(String originalChineseName) {
        return processString(originalChineseName, x -> StrUtil.concatStr(StrUtil.subStrFromHead(originalChineseName, 1), StrUtil.nullSafeLength(originalChineseName), "*"));
    }

    /**
     * 对身份证号进行脱敏处理,保留后 4 位,其余用*替代
     * 例如:脱敏前:123456789012345678  脱敏后:**********1234
     *
     * @param originalIdCard 原始的身份证号字符串
     * @return 脱敏处理后的身份证号字符串
     */
    public static String idCard(String originalIdCard) {
        return processString(originalIdCard, x -> StrUtil.maskStart(StrUtil.subStrFromLast(originalIdCard, 4), StrUtil.nullSafeLength(originalIdCard), "*"));
    }


    /**
     * 对电话号码进行脱敏处理,保留后 4 位,其余用*替代
     * 例如:脱敏前:13812345678  脱敏后:****5678
     *
     * @param originalPhoneNumber 原始的电话号码字符串
     * @return 脱敏处理后的电话号码字符串
     */
    public static String phone(String originalPhoneNumber) {
        return processString(originalPhoneNumber, x -> StrUtil.maskStart(StrUtil.subStrFromLast(originalPhoneNumber, 4), StrUtil.nullSafeLength(originalPhoneNumber), "*"));
    }


    /**
     * 对手机号码进行脱敏处理,保留前 3 位和后 2 位,中间用*替代
     * 例如:脱敏前:13812345678  脱敏后:138****5678
     *
     * @param originalMobileNumber 原始的手机号码字符串
     * @return 脱敏处理后的手机号码字符串
     */
    public static String mobile(String originalMobileNumber) {
        return processString(originalMobileNumber, x -> StrUtil.subStrFromHead(originalMobileNumber, 3).concat(StrUtil.removeFromHeader(StrUtil.maskStart(StrUtil.subStrFromLast(originalMobileNumber, 2), StrUtil.nullSafeLength(originalMobileNumber), "*"), "***")));
    }


    /**
     * 对地址进行脱敏处理,如果长度小于等于 8 则保持不变,否则保留前 i - 8 位,其余用*替代
     * 例如:脱敏前:北京市海淀区  脱敏后:北京市海淀区
     * 脱敏前:北京市海淀区中关村大街  脱敏后:北京市****
     *
     * @param originalAddress 原始的地址字符串
     * @return 脱敏处理后的地址字符串
     */
    public static String address(String originalAddress) {
        return processString(originalAddress, paramString1 -> {
            int i = StrUtil.nullSafeLength(originalAddress);
            return (i <= 8) ? originalAddress : StrUtil.concatStr(StrUtil.subStrFromHead(originalAddress, i - 8), i, "*");
        });
    }

    /**
     * 对电子邮箱进行脱敏处理,根据 "@" 的位置决定保留的前缀长度,其余用*替代,然后拼接后缀
     * 例如:脱敏前:example@example.com  脱敏后:ex****@example.com
     *
     * @param originalEmail 原始的电子邮箱字符串
     * @return 脱敏处理后的电子邮箱字符串
     */
    public static String email(String originalEmail) {
        return processString(originalEmail, x -> {
            int i = StrUtil.findIndex(originalEmail, "@");
            byte b = 1;
            if (i > 5)
                b = 3;
            return (1 == i) ? originalEmail : StrUtil.concatStr(StrUtil.subStrFromHead(originalEmail, b), i, "*").concat(StrUtil.subStrFromIndex(originalEmail, i, StrUtil.nullSafeLength(originalEmail)));
        });
    }


    /**
     * 对银行卡号进行脱敏处理,保留前 6 位和后 4 位,中间用*替代
     * 例如:脱敏前:1234567890123456  脱敏后:123456******3456
     *
     * @param originalBankCardNumber 原始的银行卡号字符串
     * @return 脱敏处理后的银行卡号字符串
     */
    public static String bankCard(String originalBankCardNumber) {
        return processString(originalBankCardNumber, x -> StrUtil.subStrFromHead(originalBankCardNumber, 6).concat(StrUtil.removeFromHeader(StrUtil.maskStart(StrUtil.subStrFromLast(originalBankCardNumber, 4), StrUtil.nullSafeLength(originalBankCardNumber), "*"), "******")));
    }

    /**
     * 对密码进行脱敏处理,全部用*替代
     * 例如:脱敏前:123456  脱敏后:******
     *
     * @param originalPassword 原始的密码字符串
     * @return 脱敏处理后的密码字符串
     */
    public static String password(String originalPassword) {
        return processString(originalPassword, x -> StrUtil.concatStr(StrUtil.subStrFromHead(originalPassword, 0), StrUtil.nullSafeLength(originalPassword), "*"));
    }

    /**
     * 对车牌号进行脱敏处理,保留前 2 位和后 1 位,中间用*替代
     * 例如:脱敏前:浙AA1156  脱敏后:浙A****6
     *
     * @param originalCarNumber 原始的车牌号字符串
     * @return 脱敏处理后的车牌号字符串
     */
    public static String carNumber(String originalCarNumber) {
        return processString(originalCarNumber, x -> StrUtil.subStrFromHead(originalCarNumber, 2).concat(StrUtil.removeFromHeader(StrUtil.maskStart(StrUtil.subStrFromLast(originalCarNumber, 1), StrUtil.nullSafeLength(originalCarNumber), "*"), "**")));
    }

    /**
     * 通用的脱敏处理方法
     *
     * @param originalString  原始字符串
     * @param anonymizeFunction 具体的脱敏处理函数
     * @return 脱敏处理后的字符串,如果原始字符串全为空则返回 null
     */
    private static String processString(String originalString, Function<String, String> anonymizeFunction) {
        return StrUtil.isBlank(originalString)? null : anonymizeFunction.apply(originalString);
    }
}

总结

本文针对互联网项目常见的数据脱敏需求,提供了一种基于Jackson优雅、通用、灵活的的数据脱敏方法,主要有以下优点:

  • 使用优雅,只需要添加一个注解
  • 和SpringBoot框架无缝对接,实体增加注解后所有接口JSON数据自动脱敏
  • 代码通用,所有人都可以使用该方法
  • 策略灵活,可以自定义策略

符StrUtil源码:

public class StrUtil {
    /**
     * 根据指定参数在字符串起始位置进行填充
     *
     * @param originalString 原始字符串
     * @param targetLength   填充后的总长度
     * @param fillElement    用于填充的元素,可以是字符串或单个字符
     * @return 填充后的字符串
     * 示例:
     * 原始字符串:"hello",填充总长度 10,填充字符串 "**"  处理后:"******hello"
     * 原始字符串:"world",填充总长度 8,填充字符 '*'  处理后:"****world"
     */
    public static String maskStart(String originalString, int targetLength, Object fillElement) {
        if (originalString == null) {
            return null;
        }
        int originalStringLength = originalString.length();
        int paddingLength = targetLength - originalStringLength;

        if (paddingLength <= 0) {
            return originalString;
        }

        if (fillElement instanceof String) {
            String fillString = (String) fillElement;
            if (isBlank(fillString)) {
                fillString = " ";
            }
            int fillStringLength = fillString.length();
            if (fillStringLength == 1 && paddingLength <= 8192) {
                return maskStart(originalString, targetLength, fillString.charAt(0));
            }
            if (paddingLength == fillStringLength) {
                return fillString.concat(originalString);
            }
            if (paddingLength < fillStringLength) {
                return fillString.substring(0, paddingLength).concat(originalString);
            }
            char[] paddingChars = new char[paddingLength];
            char[] fillStringChars = fillString.toCharArray();
            for (byte b = 0; b < paddingLength; b++) {
                paddingChars[b] = fillStringChars[b % fillStringLength];
            }
            return new String(paddingChars).concat(originalString);
        } else if (fillElement instanceof Character) {
            char fillChar = (char) fillElement;
            return paddingLength <= 0 ? originalString : (paddingLength > 8192 ? maskStart(originalString, targetLength, String.valueOf(fillChar)) : repeatChar(fillChar, paddingLength).concat(originalString));
        }
        return originalString;
    }

    /**
     * 连接两个字符串,根据指定的总长度和第二个字符串/字符进行处理
     *
     * @param firstString   第一个字符串
     * @param totalLength   连接后的总长度
     * @param secondElement 第二个元素,可以是字符串或单个字符
     * @return 连接后的字符串
     * 示例:
     * 第一个字符串:"hello",连接后总长度 10,第二个字符串:"world"  处理后:"hello world  "
     * 第一个字符串:"hello",连接后总长度 8,第二个字符:'*'  处理后:"hello***"
     */
    public static String concatStr(String firstString, int totalLength, Object secondElement) {
        if (firstString == null) {
            return null;
        }
        int firstStringLength = firstString.length();
        int paddingLength = totalLength - firstStringLength;

        if (paddingLength <= 0) {
            return firstString;
        }

        if (secondElement instanceof String) {
            String secondString = (String) secondElement;
            if (isBlank(secondString)) {
                secondString = " ";
            }
            int secondStringLength = secondString.length();
            if (secondStringLength == 1 && paddingLength <= 8192) {
                return concatStr(firstString, totalLength, secondString.charAt(0));
            }
            if (paddingLength == secondStringLength) {
                return firstString.concat(secondString);
            }
            if (paddingLength < secondStringLength) {
                return firstString.concat(secondString.substring(0, paddingLength));
            }
            char[] paddingChars = new char[paddingLength];
            char[] secondStringChars = secondString.toCharArray();
            for (byte b = 0; b < paddingLength; b++) {
                paddingChars[b] = secondStringChars[b % secondStringLength];
            }
            return firstString.concat(new String(paddingChars));
        } else if (secondElement instanceof Character) {
            char secondChar = (char) secondElement;
            return paddingLength <= 0 ? firstString : (paddingLength > 8192 ? concatStr(firstString, totalLength, String.valueOf(secondChar)) : firstString.concat(repeatChar(secondChar, paddingLength)));
        }
        return firstString;
    }


    private static String repeatChar(char charToRepeat, int repeatCount) {
        if (repeatCount <= 0) {
            return "";
        }
        char[] repeatedChars = new char[repeatCount];
        for (int i = repeatCount - 1; i >= 0; i--) {
            repeatedChars[i] = charToRepeat;
        }
        return new String(repeatedChars);
    }


    /**
     * 查找字符串中的子字符串索引
     *
     * @param sourceString 原始字符串
     * @param subString    要查找的子字符串
     * @return 子字符串在原始字符串中的索引,如果原始字符串为空则返回 -1
     * 示例:
     * 原始字符串:"hello world",子字符串:"world"
     * 返回:6
     */
    public static int findIndex(String sourceString, String subString) {
        if (isBlank(sourceString)) {
            return -1;
        }
        return sourceString.indexOf(subString);
    }

    /**
     * 判断字符序列是否为空
     *
     * @param charSequence 要判断的字符序列
     * @return 如果字符序列为 null 或长度为 0 则返回 true
     * 示例:
     * 输入:null
     * 返回:true
     * 输入:""
     * 返回:true
     */
    public static boolean isBlank(CharSequence charSequence) {
        return charSequence == null || charSequence.length() == 0;
    }

    /**
     * 获取字符序列的安全长度(如果为 null 则返回 0)
     *
     * @param charSequence 要获取长度的字符序列
     * @return 字符序列的长度,如果为 null 则返回 0
     * 示例:
     * 输入:null
     * 返回:0
     * 输入:"hello"
     * 返回:5
     */
    public static int nullSafeLength(CharSequence charSequence) {
        return charSequence == null ? 0 : charSequence.length();
    }

    /**
     * 对两个字符串进行条件判断和相应处理
     *
     * @param firstStr  第一个字符串
     * @param secondStr 第二个字符串
     * @return 根据条件返回相应的字符串
     * 示例:
     * 第一个字符串:"hello",第二个字符串:"he"
     * 处理后:"llo"
     */
    public static String removeFromHeader(String firstStr, String secondStr) {
        return isBlank(firstStr) || isBlank(secondStr) ? firstStr : (firstStr.startsWith(secondStr) ? firstStr.substring(secondStr.length()) : firstStr);
    }

    /**
     * 从字符串头部截取指定长度的子串
     *
     * @param sourceString 原始字符串
     * @param subLength    要截取的长度
     * @return 截取后的子串,如果参数不合法则返回相应的默认值
     * 示例:
     * 原始字符串:"hello world",截取长度 5
     * 处理后:"hello"
     */
    public static String subStrFromHead(String sourceString, int subLength) {
        if (sourceString == null) {
            return null;
        }
        return subLength < 0 ? "" : sourceString.length() <= subLength ? sourceString : sourceString.substring(0, subLength);
    }


    /**
     * 从字符串尾部截取指定长度的子串
     *
     * @param sourceString 原始字符串
     * @param subLength    要截取的长度
     * @return 截取后的子串,如果参数不合法则返回相应的默认值
     * 示例:
     * 原始字符串:"hello world",截取长度 5
     * 处理后:"world"
     */
    public static String subStrFromLast(String sourceString, int subLength) {
        if (sourceString == null) {
            return null;
        }
        return subLength < 0 ? "" : sourceString.length() <= subLength ? sourceString : sourceString.substring(sourceString.length() - subLength);
    }


    /**
     * 从字符串指定索引开始截取指定长度的子串
     *
     * @param sourceString 原始字符串
     * @param startIndex   起始索引
     * @param subLength    要截取的长度
     * @return 截取后的子串,如果参数不合法则返回空字符串
     * 示例:
     * 原始字符串:"hello world",起始索引 6,截取长度 5
     * 处理后:"world"
     */
    public static String subStrFromIndex(String sourceString, int startIndex, int subLength) {
        if (sourceString == null) {
            return null;
        }
        if (subLength < 0 || startIndex > sourceString.length()) {
            return "";
        }
        if (startIndex < 0) {
            startIndex = 0;
        }
        return sourceString.length() <= startIndex + subLength ? sourceString.substring(startIndex) : sourceString.substring(startIndex, startIndex + subLength);
    }

}