├── install.sh ├── .gitignore ├── src └── main │ └── java │ └── org │ └── apache │ └── ibatis │ ├── annotations │ └── CryptField.java │ └── plugin │ └── CryptInterceptor.java ├── LICENSE ├── pom.xml └── README.md /install.sh: -------------------------------------------------------------------------------- 1 | mvn clean install source:jar -Dmaven.test.skip=true -Dpackagedir= 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse project files 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | 7 | # Intellij project files 8 | *.iml 9 | .idea/ 10 | 11 | # Others 12 | target/ 13 | logs/ 14 | tmp/ 15 | lib/ -------------------------------------------------------------------------------- /src/main/java/org/apache/ibatis/annotations/CryptField.java: -------------------------------------------------------------------------------- 1 | package org.apache.ibatis.annotations; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 项目:mybatis-crypt 7 | * 包名:org.apache.ibatis.annotations 8 | * 功能: 9 | * 时间:2017-11-22 10 | * 作者:miaoxw 11 | */ 12 | @Documented 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) 15 | public @interface CryptField { 16 | 17 | String value() default ""; 18 | 19 | boolean encrypt() default true; 20 | 21 | boolean decrypt() default true; 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 miaoxw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.lico 8 | mybatis-crypt 9 | 1.0 10 | 11 | 12 | 1.8 13 | UTF-8 14 | UTF-8 15 | 16 | 17 | 18 | 19 | org.mybatis 20 | mybatis 21 | 3.5.6 22 | provided 23 | 24 | 25 | org.apache.commons 26 | commons-lang3 27 | 3.6 28 | provided 29 | 30 | 31 | 32 | 33 | 34 | 35 | maven-compiler-plugin 36 | 37 | 1.8 38 | 1.8 39 | utf-8 40 | 41 | 3.7.0 42 | 43 | 44 | maven-resources-plugin 45 | 46 | utf-8 47 | 48 | 3.0.2 49 | 50 | 51 | maven-source-plugin 52 | 3.0.1 53 | 54 | true 55 | 56 | 57 | 58 | compile 59 | 60 | jar 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mybatis-crypt 2 | mybatis 拦截器--实现数据库数据脱敏 3 | 4 | 5 | 6 | ### 介绍 7 | 8 | 注解类 @CryptField — 可作用于类成员变量、方法、方法参数 9 | 10 | ```java 11 | @Documented 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) 14 | public @interface CryptField { 15 | 16 | String value() default ""; 17 | 18 | boolean encrypt() default true; 19 | 20 | boolean decrypt() default true; 21 | } 22 | ``` 23 | 24 | 25 | 26 | 27 | 28 | ### 使用方法 29 | 30 | 1. 引入jar包 31 | 32 | 2. 配置mybatis插件 33 | 34 | - xml 35 | 36 | ```xml 37 | 38 | 39 | 40 | 41 | 42 | 43 | classpath:mapper/*.xml 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ``` 54 | 55 | - java config 56 | 57 | ```java 58 | @Bean(name = "sqlSessionFactory") 59 | public SqlSessionFactory sqlSessionFactory() throws Exception { 60 | SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); 61 | PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); 62 | factoryBean.setMapperLocations(resolver.getResources("classpath:/mapper/*.xml")); 63 | factoryBean.setDataSource(dataSource); 64 | 65 | List interceptors = Lists.newArrayList(); 66 | 67 | CryptInterceptor cryptInterceptor = new CryptInterceptor(); 68 | interceptors.add(cryptInterceptor); 69 | 70 | factoryBean.setPlugins(interceptors.toArray(new Interceptor[interceptors.size()])); 71 | 72 | return factoryBean.getObject(); 73 | } 74 | ``` 75 | 76 | 77 | 78 | 3. 注解使用场景 79 | - 方法 80 | 81 | 表示返回值需要解密,适用对象包括String、List<String>、JavaBean、List<JavaBean> 82 | 83 | ```java 84 | @CryptField 85 | List select(String aaa); 86 | ``` 87 | 88 | - 类成员变量 89 | 90 | 表示类成员需要加解密,具体看使用场景、适用对象包括String、List<String> 91 | 92 | - 方法参数 93 | 94 | 表示方法参数需要加密,适用对象包括String、List<String>、JavaBean、List<JavaBean> 95 | -------------------------------------------------------------------------------- /src/main/java/org/apache/ibatis/plugin/CryptInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.apache.ibatis.plugin; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.apache.ibatis.annotations.CryptField; 5 | import org.apache.ibatis.binding.MapperMethod; 6 | import org.apache.ibatis.executor.Executor; 7 | import org.apache.ibatis.mapping.MappedStatement; 8 | import org.apache.ibatis.session.ResultHandler; 9 | import org.apache.ibatis.session.RowBounds; 10 | import org.apache.ibatis.session.defaults.DefaultSqlSession; 11 | 12 | import java.lang.annotation.Annotation; 13 | import java.lang.reflect.Field; 14 | import java.lang.reflect.Method; 15 | import java.util.*; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | 18 | /** 19 | * 项目:mybatis-crypt 20 | * 包名:org.apache.ibatis.plugin 21 | * 功能:数据库数据脱敏 22 | * 加解密算法推荐:aes192 + base64 23 | * 时间:2017-11-22 24 | * 作者:miaoxw 25 | */ 26 | @Intercepts({ 27 | @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), 28 | @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) 29 | }) 30 | public class CryptInterceptor implements Interceptor { 31 | 32 | private static final String PARAM = "param"; 33 | 34 | private static final String PARAM_TYPE_LIST = "list"; 35 | 36 | private static final String PARAM_TYPE_COLLECTION = "collection"; 37 | 38 | private static final String MAPPEDSTATEMENT_ID_SEPERATOR = "."; 39 | 40 | /** 41 | * 适用于加密判断 42 | */ 43 | private static final ConcurrentHashMap> METHOD_PARAM_ANNOTATIONS_MAP = new ConcurrentHashMap<>(); 44 | /** 45 | * 适用于解密判断 46 | */ 47 | private static final ConcurrentHashMap METHOD_ANNOTATIONS_MAP = new ConcurrentHashMap<>(); 48 | 49 | public CryptInterceptor() { 50 | 51 | } 52 | 53 | @Override 54 | public Object intercept(Invocation invocation) throws Throwable { 55 | Object[] args = invocation.getArgs(); 56 | // 入参 57 | Object parameter = args[1]; 58 | MappedStatement statement = (MappedStatement) args[0]; 59 | // 判断是否需要解析 60 | if (!isNotCrypt(parameter)) { 61 | // 单参数 string 62 | if (parameter instanceof String) { 63 | args[1] = stringEncrypt((String) parameter, getParameterAnnotations(statement)); 64 | // 单参数 list 65 | } else if (parameter instanceof DefaultSqlSession.StrictMap) { 66 | DefaultSqlSession.StrictMap strictMap = (DefaultSqlSession.StrictMap) parameter; 67 | for (Map.Entry entry : strictMap.entrySet()) { 68 | if (entry.getKey().contains(PARAM_TYPE_COLLECTION)) { 69 | continue; 70 | } 71 | if (entry.getKey().contains(PARAM_TYPE_LIST)) { 72 | Set set = getParameterAnnotations(statement); 73 | listEncrypt((List) entry.getValue(), !set.isEmpty()); 74 | } 75 | } 76 | // 多参数 77 | } else if (parameter instanceof MapperMethod.ParamMap) { 78 | MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameter; 79 | Set set = getParameterAnnotations(statement); 80 | boolean setEmpty = set.isEmpty(); 81 | // 解析每一个参数 82 | for (Map.Entry entry : paramMap.entrySet()) { 83 | // 判断不需要解析的类型 不解析map 84 | if (isNotCrypt(entry.getValue()) || entry.getValue() instanceof Map || entry.getKey().contains(PARAM)) { 85 | continue; 86 | } 87 | // 如果string 88 | if (entry.getValue() instanceof String) { 89 | entry.setValue(stringEncrypt(entry.getKey(), (String) entry.getValue(), set)); 90 | continue; 91 | } 92 | boolean isSetValue = !setEmpty && set.contains(entry.getKey()); 93 | // 如果 list 94 | if (entry.getValue() instanceof List) { 95 | listEncrypt((List) entry.getValue(), isSetValue); 96 | continue; 97 | } 98 | beanEncrypt(entry.getValue()); 99 | } 100 | // bean 101 | } else { 102 | beanEncrypt(parameter); 103 | } 104 | } 105 | 106 | // 获得出参 107 | Object returnValue = invocation.proceed(); 108 | 109 | // 出参解密 110 | if (isNotCrypt(returnValue)) { 111 | return returnValue; 112 | } 113 | Boolean bo = getMethodAnnotations(statement); 114 | if (returnValue instanceof String && bo) { 115 | return stringDecrypt((String) returnValue); 116 | } 117 | if (returnValue instanceof List) { 118 | listDecrypt((List) returnValue, bo); 119 | return returnValue; 120 | } 121 | 122 | return returnValue; 123 | } 124 | 125 | @Override 126 | public Object plugin(Object target) { 127 | return Plugin.wrap(target, this); 128 | } 129 | 130 | @Override 131 | public void setProperties(Properties properties) { 132 | 133 | } 134 | 135 | /** 136 | * 获取 方法上的注解 137 | * 138 | * @param statement 139 | * @return 140 | * @throws ClassNotFoundException 141 | */ 142 | private Boolean getMethodAnnotations(MappedStatement statement) throws ClassNotFoundException { 143 | final String id = statement.getId(); 144 | Boolean bo = METHOD_ANNOTATIONS_MAP.get(id); 145 | if (bo != null) { 146 | return bo; 147 | } 148 | Method m = getMethodByMappedStatementId(id); 149 | if (m == null) { 150 | return Boolean.FALSE; 151 | } 152 | final CryptField cryptField = m.getAnnotation(CryptField.class); 153 | // 如果允许解密 154 | if (cryptField != null && cryptField.decrypt()) { 155 | bo = Boolean.TRUE; 156 | } else { 157 | bo = Boolean.FALSE; 158 | } 159 | Boolean bo1 = METHOD_ANNOTATIONS_MAP.putIfAbsent(id, bo); 160 | if (bo1 != null) { 161 | bo = bo1; 162 | } 163 | 164 | return bo; 165 | } 166 | 167 | /** 168 | * 获取 方法参数上的注解 169 | * 170 | * @param statement 171 | * @return 172 | * @throws ClassNotFoundException 173 | */ 174 | private Set getParameterAnnotations(MappedStatement statement) throws ClassNotFoundException { 175 | final String id = statement.getId(); 176 | Set set = METHOD_PARAM_ANNOTATIONS_MAP.get(id); 177 | if (set != null) { 178 | return set; 179 | } 180 | set = new HashSet<>(); 181 | Method m = getMethodByMappedStatementId(id); 182 | if (m == null) { 183 | return set; 184 | } 185 | final Annotation[][] paramAnnotations = m.getParameterAnnotations(); 186 | // get names from @CryptField annotations 187 | for (Annotation[] paramAnnotation : paramAnnotations) { 188 | for (Annotation annotation : paramAnnotation) { 189 | if (annotation instanceof CryptField) { 190 | CryptField cryptField = (CryptField) annotation; 191 | // 如果允许加密 192 | if (cryptField.encrypt()) { 193 | set.add(cryptField.value()); 194 | } 195 | break; 196 | } 197 | } 198 | } 199 | 200 | Set oldSet = METHOD_PARAM_ANNOTATIONS_MAP.putIfAbsent(id, set); 201 | if (oldSet != null) { 202 | set = oldSet; 203 | } 204 | 205 | return set; 206 | } 207 | 208 | /** 209 | * 通过mappedStatementId get Method 210 | * 211 | * @param id 212 | * @return 213 | * @throws ClassNotFoundException 214 | */ 215 | private Method getMethodByMappedStatementId(String id) throws ClassNotFoundException { 216 | Method m = null; 217 | final Class clazz = Class.forName(id.substring(0, id.lastIndexOf(MAPPEDSTATEMENT_ID_SEPERATOR))); 218 | for (Method method : clazz.getMethods()) { 219 | if (method.getName().equals(id.substring(id.lastIndexOf(MAPPEDSTATEMENT_ID_SEPERATOR) + 1))) { 220 | m = method; 221 | break; 222 | } 223 | } 224 | 225 | return m; 226 | } 227 | 228 | /** 229 | * 判断是否需要加解密 230 | * 231 | * @param o 232 | * @return 233 | */ 234 | private boolean isNotCrypt(Object o) { 235 | return o == null || o instanceof Double || o instanceof Integer || o instanceof Long || o instanceof Boolean; 236 | } 237 | 238 | /** 239 | * String 加密 240 | * 241 | * @param str 242 | * @return 243 | * @throws Exception 244 | */ 245 | private String stringEncrypt(String str) throws Exception { 246 | return stringEncrypt(null, str, null, null); 247 | } 248 | 249 | /** 250 | * String 加密 251 | * 252 | * @param str 253 | * @param set 254 | * @return 255 | * @throws Exception 256 | */ 257 | private String stringEncrypt(String str, Set set) throws Exception { 258 | return stringEncrypt(null, str, set, true); 259 | } 260 | 261 | /** 262 | * String 加密 263 | * 264 | * @param name 265 | * @param str 266 | * @param set 267 | * @return 268 | * @throws Exception 269 | */ 270 | private String stringEncrypt(String name, String str, Set set) throws Exception { 271 | return stringEncrypt(name, str, set, false); 272 | } 273 | 274 | /** 275 | * String 加密 276 | * 277 | * @param name 278 | * @param str 279 | * @param set 280 | * @param isSingle 281 | * @return 282 | * @throws Exception 283 | */ 284 | private String stringEncrypt(String name, String str, Set set, Boolean isSingle) throws Exception { 285 | if (StringUtils.isBlank(str)) { 286 | return str; 287 | } 288 | if (isSingle == null) { 289 | //todo 加密实现 290 | str = ""; 291 | return str; 292 | } 293 | if (isSingle && set != null && !set.isEmpty()) { 294 | //todo 加密实现 295 | str = ""; 296 | return str; 297 | } 298 | if (!isSingle && set != null && !set.isEmpty() && set.contains(name)) { 299 | //todo 加密实现 300 | str = ""; 301 | return str; 302 | } 303 | 304 | return str; 305 | } 306 | 307 | /** 308 | * String 解密 309 | * 310 | * @param str 311 | * @return 312 | */ 313 | private String stringDecrypt(String str) { 314 | if (StringUtils.isBlank(str)) { 315 | return str; 316 | } 317 | String[] array = str.split("\\|"); 318 | if (array.length < 2) { 319 | return str; 320 | } 321 | //todo 解密实现 322 | str = ""; 323 | 324 | return str; 325 | } 326 | 327 | /** 328 | * list 加密 329 | * 330 | * @param list 331 | * @param bo 332 | * @return 333 | * @throws Exception 334 | */ 335 | private List listEncrypt(List list, Boolean bo) throws Exception { 336 | for (int i = 0; i < list.size(); i++) { 337 | Object listValue = list.get(i); 338 | // 判断不需要解析的类型 339 | if (isNotCrypt(listValue) || listValue instanceof Map) { 340 | break; 341 | } 342 | if (listValue instanceof String && bo) { 343 | list.set(i, stringEncrypt((String) listValue)); 344 | continue; 345 | } 346 | beanEncrypt(listValue); 347 | } 348 | 349 | return list; 350 | } 351 | 352 | /** 353 | * list 解密 354 | * 355 | * @param list 356 | * @param bo 357 | * @return 358 | * @throws Exception 359 | */ 360 | private List listDecrypt(List list, Boolean bo) throws Exception { 361 | for (int i = 0; i < list.size(); i++) { 362 | Object listValue = list.get(i); 363 | // 判断不需要解析的类型 获得 364 | if (isNotCrypt(listValue) || listValue instanceof Map) { 365 | break; 366 | } 367 | if (listValue instanceof String && bo) { 368 | list.set(i, stringDecrypt((String) listValue)); 369 | continue; 370 | } 371 | beanDecrypt(listValue); 372 | } 373 | 374 | return list; 375 | } 376 | 377 | /** 378 | * bean 加密 379 | * 380 | * @param val 381 | * @throws Exception 382 | */ 383 | private void beanEncrypt(Object val) throws Exception { 384 | Class objClazz = val.getClass(); 385 | Field[] objFields = objClazz.getDeclaredFields(); 386 | for (Field field : objFields) { 387 | CryptField cryptField = field.getAnnotation(CryptField.class); 388 | if (cryptField != null && cryptField.encrypt()) { 389 | field.setAccessible(true); 390 | Object fieldValue = field.get(val); 391 | if (fieldValue == null) { 392 | continue; 393 | } 394 | if (field.getType().equals(String.class)) { 395 | field.set(val, stringEncrypt((String) fieldValue)); 396 | continue; 397 | } 398 | if (field.getType().equals(List.class)) { 399 | field.set(val, listEncrypt((List) fieldValue, Boolean.TRUE)); 400 | continue; 401 | } 402 | } 403 | } 404 | } 405 | 406 | /** 407 | * bean 解密 408 | * 409 | * @param val 410 | * @throws Exception 411 | */ 412 | private void beanDecrypt(Object val) throws Exception { 413 | Class objClazz = val.getClass(); 414 | Field[] objFields = objClazz.getDeclaredFields(); 415 | for (Field field : objFields) { 416 | CryptField cryptField = field.getAnnotation(CryptField.class); 417 | if (cryptField != null && cryptField.decrypt()) { 418 | field.setAccessible(true); 419 | Object fieldValue = field.get(val); 420 | if (fieldValue == null) { 421 | continue; 422 | } 423 | if (field.getType().equals(String.class)) { 424 | field.set(val, stringDecrypt((String) fieldValue)); 425 | continue; 426 | } 427 | if (field.getType().equals(List.class)) { 428 | field.set(val, listDecrypt((List) fieldValue, Boolean.TRUE)); 429 | continue; 430 | } 431 | } 432 | } 433 | } 434 | } 435 | --------------------------------------------------------------------------------