From 7d9056bef6ab032d0f1ea198570bbe38c162dee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=80=E6=BA=90=E6=B5=B7=E5=93=A5?= Date: Thu, 23 Mar 2023 12:33:39 +0800 Subject: [PATCH] add @ColumnMask() annotation support --- .../mybatisflex/annotation/ColumnMask.java | 31 +++ .../mybatisflex/core/mask/MaskFactory.java | 59 +++++ .../mybatisflex/core/mask/MaskProcesser.java | 24 ++ .../core/mask/MaskTypeHandler.java | 57 +++++ .../java/com/mybatisflex/core/mask/Masks.java | 213 ++++++++++++++++++ .../mybatisflex/core/table/ColumnInfo.java | 25 +- .../com/mybatisflex/core/table/TableInfo.java | 6 +- .../mybatisflex/core/table/TableInfos.java | 9 + 8 files changed, 420 insertions(+), 4 deletions(-) create mode 100644 mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/ColumnMask.java create mode 100644 mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskFactory.java create mode 100644 mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskProcesser.java create mode 100644 mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskTypeHandler.java create mode 100644 mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/Masks.java diff --git a/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/ColumnMask.java b/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/ColumnMask.java new file mode 100644 index 00000000..6de5081b --- /dev/null +++ b/mybatis-flex-annotation/src/main/java/com/mybatisflex/annotation/ColumnMask.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface ColumnMask { + + /** + * 脱敏方式 + */ + String value(); + + +} \ No newline at end of file diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskFactory.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskFactory.java new file mode 100644 index 00000000..af0a527b --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskFactory.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.mask; + +import java.util.HashMap; +import java.util.Map; + +/** + * 数据脱敏工厂类 + */ +public class MaskFactory { + + + /** + * 脱敏处理器 + */ + private static Map processerMap = new HashMap<>(); + + + static { + registerMaskProcesser(Masks.MOBILE, Masks.MOBILE_PROCESSER); + registerMaskProcesser(Masks.FIXED_PHONE, Masks.FIXED_PHONE_PROCESSER); + registerMaskProcesser(Masks.ID_CARD_NUMBER, Masks.ID_CARD_NUMBER_PROCESSER); + registerMaskProcesser(Masks.CHINESE_NAME, Masks.CHINESE_NAME_PROCESSER); + registerMaskProcesser(Masks.ADDRESS, Masks.ADDRESS_PROCESSER); + registerMaskProcesser(Masks.EMAIL, Masks.EMAIL_PROCESSER); + registerMaskProcesser(Masks.PASSWORD, Masks.PASSWORD_PROCESSER); + registerMaskProcesser(Masks.CAR_LICENSE, Masks.CAR_LICENSE_PROCESSER); + registerMaskProcesser(Masks.BANK_CARD_NUMBER, Masks.BANK_CARD_PROCESSER); + } + + + public static void registerMaskProcesser(String type, MaskProcesser processer) { + processerMap.put(type, processer); + } + + + public static Object mask(String type, Object data) { + MaskProcesser maskProcesser = processerMap.get(type); + if (maskProcesser == null) { + throw new IllegalStateException("Can not get mask processer for by type: " + type); + } + return maskProcesser.mask(data); + } + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskProcesser.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskProcesser.java new file mode 100644 index 00000000..2ab96a06 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskProcesser.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.mask; + +/** + * 数据脱敏处理器 + */ +public interface MaskProcesser { + + Object mask(Object data); +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskTypeHandler.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskTypeHandler.java new file mode 100644 index 00000000..972c5786 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/MaskTypeHandler.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.mask; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class MaskTypeHandler extends BaseTypeHandler { + + private String maskType; + + public MaskTypeHandler(String maskType) { + this.maskType = maskType; + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i,parameter.toString()); + } + + + @Override + public Object getNullableResult(ResultSet rs, String columnName) throws SQLException { + String data = rs.getString(columnName); + return MaskFactory.mask(maskType,data); + } + + @Override + public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String data = rs.getString(columnIndex); + return MaskFactory.mask(maskType,data); + } + + @Override + public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String data = cs.getString(columnIndex); + return MaskFactory.mask(maskType,data); + } +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/Masks.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/Masks.java new file mode 100644 index 00000000..5962d5b9 --- /dev/null +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mask/Masks.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mybatisflex.core.mask; + +/** + * 内置的数据脱敏方式 + */ +public class Masks { + + /** + * 手机号脱敏 + */ + public static final String MOBILE = "mobile"; + + /** + * 固定电话脱敏 + */ + public static final String FIXED_PHONE = "fixed_phone"; + + /** + * 身份证号脱敏 + */ + public static final String ID_CARD_NUMBER = "id_card_number"; + + /** + * 中文名脱敏 + */ + public static final String CHINESE_NAME = "chinese_name"; + + /** + * 地址脱敏 + */ + public static final String ADDRESS = "address"; + + /** + * 邮件脱敏 + */ + public static final String EMAIL = "email"; + + /** + * 密码脱敏 + */ + public static final String PASSWORD = "password"; + + /** + * 车牌号脱敏 + */ + public static final String CAR_LICENSE = "car_license"; + + /** + * 银行卡号脱敏 + */ + public static final String BANK_CARD_NUMBER = "bank_card_number"; + + + private static String createMask(int count) { + StringBuilder mask = new StringBuilder(); + for (int i = 0; i < count; i++) { + mask.append("*"); + } + return mask.toString(); + } + + + private static String mask(String needToMaskString, int keepFirstCount, int keepLastCount, int maskCount) { + return needToMaskString.substring(0, keepFirstCount) + + createMask(maskCount) + + needToMaskString.substring(needToMaskString.length() - keepLastCount); + } + + + /** + * 手机号脱敏处理器 + * 保留前三后四,中间的为星号 "*" + */ + static MaskProcesser MOBILE_PROCESSER = data -> { + if (data instanceof String && ((String) data).startsWith("1") && ((String) data).length() == 11) { + return mask((String) data, 3, 4, 4); + } + return data; + }; + + + /** + * 固定电话脱敏 + * 保留前三后四,中间的为星号 "*" + */ + static MaskProcesser FIXED_PHONE_PROCESSER = data -> { + if (data instanceof String && ((String) data).length() > 5) { + return mask((String) data, 3, 2, ((String) data).length() - 5); + } + return data; + }; + + + /** + * 身份证号脱敏处理器 + * 身份证号的保留前三后四,中间的数为星号 "*" + */ + static MaskProcesser ID_CARD_NUMBER_PROCESSER = data -> { + if (data instanceof String && ((String) data).length() >= 15) { + return mask((String) data, 3, 4, ((String) data).length() - 7); + } + return data; + }; + + + /** + * 姓名脱敏 + */ + static MaskProcesser CHINESE_NAME_PROCESSER = data -> { + if (data instanceof String) { + String name = (String) data; + if (name.length() == 2) { + return name.charAt(0) + "*"; + } else if (name.length() == 3) { + return name.charAt(0) + "*" + name.charAt(2); + } else if (name.length() == 4) { + return "**" + name.substring(2, 4); + } else if (name.length() > 4) { + return mask(name, 2, 1, name.length() - 3); + } + } + return data; + }; + + + /** + * 地址脱敏 + */ + static MaskProcesser ADDRESS_PROCESSER = data -> { + if (data instanceof String) { + String address = (String) data; + if (address.length() > 6) { + return mask(address, 6, 0, 3); + } else if (address.length() > 3) { + return mask(address, 3, 0, 3); + } + } + return data; + }; + + + /** + * email 脱敏 + */ + static MaskProcesser EMAIL_PROCESSER = data -> { + if (data instanceof String && ((String) data).contains("@")) { + String fullEmail = (String) data; + int indexOf = fullEmail.lastIndexOf("@"); + String email = fullEmail.substring(0, indexOf); + + if (email.length() == 1) { + return "*" + fullEmail.substring(indexOf); + } else if (email.length() == 2) { + return "**" + fullEmail.substring(indexOf); + } else if (email.length() < 5) { + return mask(email, 2, 0, email.length() - 2) + fullEmail.substring(indexOf); + } else { + return mask(email, 3, 0, email.length() - 3) + fullEmail.substring(indexOf); + } + } + return data; + }; + + + /** + * 密码 脱敏 + */ + static MaskProcesser PASSWORD_PROCESSER = data -> { + if (data instanceof String ) { + return mask((String) data, 0, 0, ((String) data).length()) ; + } + return data; + }; + + + /** + * 车牌号 脱敏 + */ + static MaskProcesser CAR_LICENSE_PROCESSER = data -> { + if (data instanceof String) { + return mask((String) data, 3, 1, ((String) data).length() - 4); + } + return data; + }; + + + /** + * 银行卡号 脱敏 + */ + static MaskProcesser BANK_CARD_PROCESSER = data -> { + if (data instanceof String && ((String) data).length() >= 8) { + return mask((String) data, 4, 4, 4); + } + return data; + }; + + +} diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/ColumnInfo.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/ColumnInfo.java index 3c8055fd..f039172d 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/ColumnInfo.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/ColumnInfo.java @@ -15,6 +15,8 @@ */ package com.mybatisflex.core.table; +import com.mybatisflex.core.mask.MaskTypeHandler; +import com.mybatisflex.core.util.StringUtil; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.TypeHandler; @@ -46,6 +48,9 @@ public class ColumnInfo { protected TypeHandler typeHandler; + protected String maskType; + + public String getColumn() { return column; @@ -79,11 +84,29 @@ public class ColumnInfo { this.jdbcType = jdbcType; } - public TypeHandler getTypeHandler() { + public TypeHandler buildTypeHandler() { + + //优先使用自定义的 typeHandler + if (typeHandler != null){ + return typeHandler; + } + //若用户未定义 typeHandler,而配置了数据脱敏,则使用脱敏的 handler 处理 + else if (StringUtil.isNotBlank(maskType)){ + typeHandler = new MaskTypeHandler(maskType); + } + return typeHandler; } public void setTypeHandler(TypeHandler typeHandler) { this.typeHandler = typeHandler; } + + public String getMaskType() { + return maskType; + } + + public void setMaskType(String maskType) { + this.maskType = maskType; + } } diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java index 0e09d66c..2a6b1e9b 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java @@ -433,7 +433,7 @@ public class TableInfo { ResultMapping mapping = new ResultMapping.Builder(configuration, columnInfo.getProperty(), columnInfo.getColumn(), columnInfo.getPropertyType()) .jdbcType(columnInfo.getJdbcType()) - .typeHandler(columnInfo.getTypeHandler()) + .typeHandler(columnInfo.buildTypeHandler()) .build(); resultMappings.add(mapping); } @@ -443,7 +443,7 @@ public class TableInfo { idInfo.getColumn(), idInfo.getPropertyType()) .flags(CollectionUtil.newArrayList(ResultFlag.ID)) .jdbcType(idInfo.getJdbcType()) - .typeHandler(idInfo.getTypeHandler()) + .typeHandler(idInfo.buildTypeHandler()) .build(); resultMappings.add(mapping); } @@ -456,7 +456,7 @@ public class TableInfo { ColumnInfo columnInfo = columnInfoMapping.get(column); Object value = getPropertyValue(metaObject, columnInfo.property); - TypeHandler typeHandler = columnInfo.getTypeHandler(); + TypeHandler typeHandler = columnInfo.buildTypeHandler(); if (value != null && typeHandler != null) { return new TypeHandlerObject(typeHandler, value, columnInfo.getJdbcType()); } diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfos.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfos.java index d465ce06..3209b4be 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfos.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfos.java @@ -16,6 +16,7 @@ package com.mybatisflex.core.table; import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.ColumnMask; import com.mybatisflex.annotation.Id; import com.mybatisflex.annotation.Table; import com.mybatisflex.core.FlexConsts; @@ -206,6 +207,14 @@ public class TableInfos { columnInfo.setTypeHandler(typeHandler); } + ColumnMask columnMask = field.getAnnotation(ColumnMask.class); + if (columnMask != null && StringUtil.isNotBlank(columnMask.value())) { + if (String.class != field.getType()) { + throw new IllegalStateException("@ColumnMask() only support for string type field. error: " + entityClass.getName() + "." + field.getName()); + } + columnInfo.setMaskType(columnMask.value().trim()); + } + if (column != null && column.jdbcType() != JdbcType.UNDEFINED) { columnInfo.setJdbcType(column.jdbcType()); }