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