数据安全之MySQL数据加解密的实现方案
在我們日常的業務需求中,經常會遇到需要對存儲的用戶敏感數據進行加密處理的場景,如用戶的身份信息、住址、身份證號等等,本文我們就討論下,業務系統(后端)如何實現數據存儲(基于MySQL)的加解密功能。
技術棧:springboot、mybatis、mysql等
方案一:基于spring aop攔截mybatis mapper.
第一步:定義注解@Encrypt
@Target(ElementType.METHOD)//注解的范圍是類、接口、枚舉的方法上 @Retention(RetentionPolicy.RUNTIME)//被虛擬機保存,可用反射機制讀取 public @interface Encrypt{/*** 入參需要加密的字段* @return*/String[] paramFields() default {};/*** 響應參數需解密的字段* @return*/String[] respFields() default {}; }第二步:開發攔截器處理類
@Slf4j @Aspect @Component public class EncryptAspect {@Pointcut("@annotation(com.xxx.annotation.Encrypt)")public void encryPointCut() {}@Around("encryPointCut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();final Sm4Intercept annotation = method.getAnnotation(Encrypt.class);if (null == annotation) {return joinPoint.proceed();}//加密入參對象屬性值encryptRequest(joinPoint, annotation);//執行目標方法Object response = joinPoint.proceed();//解密響應結果對象的屬性值decryptResponse(response, annotation);return response;}/*** 加密請求入參對象** @param joinPoint* @param annotation*/private void encryptRequest(ProceedingJoinPoint joinPoint, Sm4Intercept annotation) {//獲取接口的入參列表final Object[] params = joinPoint.getArgs();if (!CollectionUtil.isEmpty(params)) {//接口入參Object param = params[0];//接口入參對象的屬性列表Field[] fields = param.getClass().getDeclaredFields();if (!CollectionUtil.isEmpty(annotation.paramFields())) {//遍歷加密入參的屬性值Arrays.stream(annotation.paramFields()).forEach(target -> {Field field = Arrays.stream(fields).filter(f -> f.getName().equals(target)).findFirst().orElse(null);if (null != field) {//反射獲取目標屬性值Object fieldValue = getFieldValue(param, field.getName());if (null != fieldValue) {String encryFieldValue = EncryptUtil.encryptEcb(key, fieldValue.toString());log.info("類{}的屬性{}的值{}已被加密為{}", param.getClass().getName(), target, fieldValue, encryFieldValue);setFieldValue(param, field.getName(), encryFieldValue);}}});}}}/*** 解密響應結果對象的屬性值** @param object* @param annotation*/private void decryptResponse(Object object, Sm4Intercept annotation) {//返回結果是list時if (object instanceof List) {decryptListObject((List) object, annotation);return;}//返回結果為單對象時decryptObject(object, annotation);}/*** 解密list中對象的屬性值** @param list* @param annotation*/private void decryptListObject(List list, Sm4Intercept annotation) {list.stream().forEach(record -> decryptObject(record, annotation));}/*** 解密單對象的屬性值** @param record* @param annotation*/private void decryptObject(Object record, Sm4Intercept annotation) {//接口返回對象的屬性列表Field[] fields = record.getClass().getDeclaredFields();if (!CollectionUtil.isEmpty(annotation.respFields())) {//遍歷加密入參的屬性值Arrays.stream(annotation.respFields()).forEach(target -> {Field field = Arrays.stream(fields).filter(f -> f.getName().equals(target)).findFirst().orElse(null);if (null != field) {//反射獲取目標屬性值Object fieldValue = getFieldValue(record, field.getName());if (null != fieldValue) {String decryFieldValue = EncryptUtil.decryptEcb(key, fieldValue.toString());log.info("類{}的屬性{}的值{}已被解密為{}", record.getClass().getName(), target, fieldValue, decryFieldValue);setFieldValue(record, field.getName(), decryFieldValue);}}});}}/*** 通過反射,用屬性名稱獲得屬性值** @param thisClass 需要獲取屬性值的類* @param fieldName 該類的屬性名稱* @return*/private Object getFieldValue(Object thisClass, String fieldName) {Object value = new Object();try {Method method = thisClass.getClass().getMethod(getMethodName(fieldName, "get"));value = method.invoke(thisClass);} catch (Exception e) {}return value;}/*** 通過反射,設置屬性值** @param thisClass* @param fieldName* @param fieldValue*/private void setFieldValue(Object thisClass, String fieldName, Object fieldValue) {try {Method method = thisClass.getClass().getMethod(getMethodName(fieldName, "set"), String.class);method.invoke(thisClass, fieldValue);} catch (Exception e) {}}/*** 獲取方法名稱(getXXX,setXXX)** @param fieldName* @param methodPrefix* @return*/private String getMethodName(String fieldName, String methodPrefix) {return methodPrefix.concat(fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));} }第三步:mapper中定義注解@Encrypt進行數據攔截
@Mapper public interface UserInfoMapper extends BaseMapper<UserInfo> {/*** 如果查詢條件中包含username,則在mapper執行前進行加密* 如果返回數據中包含username及address等信息,則進行解密 * @param vo* @return*/@Encrypt(paramFields = {"userName"}, respFields = {"userName", "address"})List<UserInfo> findUserInfo(UserSearchVo vo); }這樣,便實現了在數據查詢(或更新、插入等)時,完成入參及返回數據的加解密操作。不過這種處理方式僅限于數據操作是通過Dao的mapper接口調用時,如果想處理更多場景,如通過mybatis-plus的Wraper方式進行數據處理時,則考慮用后面的第二種處理方式。
方案二:基于mybatis自帶的擴展插件(plugins)實現
MyBatis 允許你在映射語句執行過程中的某一點進行攔截調用。默認情況下,MyBatis 允許使用插件來攔截的方法調用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
這些類中方法的細節可以通過查看每個方法的簽名來發現,或者直接查看 MyBatis 發行包中的源代碼。 如果你想做的不僅僅是監控方法的調用,那么你最好相當了解要重寫的方法的行為。 因為在試圖修改或重寫已有方法的行為時,很可能會破壞 MyBatis 的核心模塊。 這些都是更底層的類和方法,所以使用插件的時候要特別當心。
通過 MyBatis 提供的強大機制,使用插件是非常簡單的,只需實現 Interceptor 接口,并指定想要攔截的方法簽名即可。
?插件的使用參考實現如下(mybatis官方文檔)
@Slf4j @Component @Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class })}) public class ExamplePlugin implements Interceptor {private Properties properties = new Properties();public Object intercept(Invocation invocation) throws Throwable {// implement pre processing if needObject returnObject = invocation.proceed();// implement post processing if needreturn returnObject;}public void setProperties(Properties properties) {this.properties = properties;} }即在mybatis的Executor的query方法執行前后進行攔截,如事先定義好需要加解密的“配置規則”(如“對哪個表的哪些字段需要加解密”、“方法的出入參需加解密的字段”等等),然后攔截sql的請求參數及執行的返回結果,對其進行相應的數據加解密操作。
本文的核心實現思路都是圍繞spring aop進行實現的,足以說明aop思想的強大之處,大家平時學習工作中一定要勤學、多用、多練!希望本文可以幫助到有需要的朋友們!
總結
以上是生活随笔為你收集整理的数据安全之MySQL数据加解密的实现方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信小程序纯前端生成海报并保存本地
- 下一篇: centos7 mysql libssl