mybatis中數(shù)據(jù)加密與解密的實現(xiàn)
數(shù)據(jù)加解密的實現(xiàn)方式多種多樣,在
mybatis
環(huán)境中數(shù)據(jù)加解密變得非常簡單易用,本文旨在提供參考,在生產中應盡可能完成單元測試,開展足夠的覆蓋測試,以驗證可靠性、可用性、安全性。
1、需求
**原始需求:**數(shù)據(jù)在保存時進行加密,取出時解密,避免被拖庫時泄露敏感信息。
**初始分析:**數(shù)據(jù)從前端過來,到達后端,經過業(yè)務邏輯后存入數(shù)據(jù)庫,其中經歷三大環(huán)節(jié):
1、前端與后端之間傳輸,是否加密,如果需要加密則前端傳輸前就需要加密,暫時可以用HTTPS代替;
2、到達后端,此時數(shù)據(jù)通常需要經過一些邏輯判斷,所以加密沒有意義,反而會帶來不必要的麻煩;
3、入庫,這個是最后環(huán)節(jié),數(shù)據(jù)經過insert的sql或者update語句入庫,在此前需要加密;
**核心需求:**入庫前最后一步完成數(shù)據(jù)加密,達成的目的是如果數(shù)據(jù)庫被暴露,一定程度上保障數(shù)據(jù)的安全,也可以防止有數(shù)據(jù)操作權限的人將數(shù)據(jù)泄露。
**加密算法:**對稱和非對稱算法均可,考慮加密和解密的效率以及場景,考慮選用對稱算法AES加密。
**ORM環(huán)境:**mybatis
**加密字段:**加密字段不確定,應該在數(shù)據(jù)庫表設計的時候確定敏感字段,即加密字段可定制。
應注意的細節(jié):
1、某個字段被加密后,其字段的存取性能下降,加密字段越多性能下降就越多,無具體指標;
2、字段被加密后,該字段的索引沒有太大意義,比如對手機號碼字段mobile加密,原先可能設計為唯一索引以防止號碼重復,加密后密文性能下降,比對結果不直觀,沒有大量數(shù)據(jù)驗證,理論上密文也不會相同;
3、一些SQL的比對也無法直接實現(xiàn),比如手機號碼匹配查詢,在開發(fā)和運維中,就需要考慮后續(xù)工作中敏感字段的可操作性;
4、原字段的長度需要擴充,密文肯定比原文長;
5、不要對主鍵加密(真的,有人會這么做的);
6、有時,為了減少關聯(lián)查詢,我們會對表做冗余字段,比如將name字段放入業(yè)務表,如果對name字段加密,則需同步對冗余表做加密處理,所以在進行數(shù)據(jù)加密需求時,應進行全局考慮。
最后:數(shù)據(jù)加密用來提高安全性的同時,必然會犧牲整個程序性能和易用性。
2、解決方案
在mybatis的依賴環(huán)境下,至少有兩種自動加密的方式:
1、使用攔截器,對insert和update語句攔截,獲取需加密字段,加密后存入數(shù)據(jù)庫。讀取時攔截query,解密后存入result對象;
2、使用類型轉換器TypeHandler來實現(xiàn)。
3、使用攔截器方式
3.1 定義加密接口
因為mybatis攔截器會攔截所有符合簽名的請求,為了提高效率定義一個標記接口非常重要,既然有接口不如就在接口里加入需要加密的字段信息,當然也可以不加,根據(jù)實際場景來設計。
/** * @author: xu.dm * @since: 2022/3/8 16:30 * 該接口用于標記實體類需要加密,具體的加密內容字段通過getEncryptFields返回. * 注意:getEncryptFields與@Encrypt注解可配合使用也可以互斥使用,根據(jù)具體的需求實現(xiàn)。 **/ public interface Encrypted { /** * 實現(xiàn)該接口,返回需要加密的字段名數(shù)組,需與類中字段完全一致,區(qū)分大小寫 * @return 返回需要加密的字段 */ default String[] getEncryptFields() { return new String[0]; } }
3.2 定義加密注解
主要為了某些場景,直接在實體類的字段打標記,直觀的說明該字段是加密字段,某些業(yè)務邏輯也可以依賴此標記做進一步操作,一句話,根據(jù)場景來適配和設計。
/** * @author : xu.dm * @since : 2022/3/8 * 標識加密的注解,value值暫時沒用,根據(jù)需要可以考慮采用的加密方式與算法等 * 注意:Encrypted接口的getEncryptFields與@Encrypt注解可配合使用也可以互斥使用,根據(jù)具體的需求實現(xiàn)。 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Encrypt { String value() default ""; }
3.3 攔截器加密數(shù)據(jù)
初始攔截器定義是相對單一的場景,利用反射遍歷需加密的字段,對字段的字符加密,也就是待加密字段最好是字符串類型,并且,沒有對父類反射遍歷,如果有繼承情況,并且父類也有需要加密的字段,需根據(jù)場景調整代碼,對父類遞歸,直到根父類。在當前設計中Encrypted接口和@Encrypt只會生效一種,并且以接口優(yōu)先。
/** * @author: xu.dm * @since: 2022/3/8 * 攔截所有實現(xiàn)Encrypted接口的實體類insert和update操作 * 如果接口的getEncryptFields返回數(shù)組長度大于0,則使用該參數(shù)進行加密, * 否則檢查實體類中帶@Encrypt注解,對該標識字段加密, * 注意:待加密的字段最好是字符串,加密調用的是標識對象的ToString()結果進行加密, * **/ @Component @Slf4j @Intercepts({ @Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class}) }) public class EncryptionInterceptor implements Interceptor { public EncryptionInterceptor() { } @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); SqlCommandType sqlCommandType = null; for (Object object : args) { // 從MappedStatement參數(shù)中獲取到操作類型 if (object instanceof MappedStatement) { MappedStatement ms = (MappedStatement) object; sqlCommandType = ms.getSqlCommandType(); log.debug("Encryption interceptor 操作類型: {}", sqlCommandType); continue; } log.debug("Encryption interceptor 操作參數(shù):{}",object); // 判斷參數(shù) if (object instanceof Encrypted) { if (SqlCommandType.INSERT == sqlCommandType) { encryptField((Encrypted)object); continue; } if (SqlCommandType.UPDATE == sqlCommandType) { encryptField((Encrypted)object); log.debug("Encryption interceptor update operation,encrypt field: {}",object.toString()); } } } return invocation.proceed(); } /** * @param object 待檢查的對象 * @throws IllegalAccessException * 通過查詢注解@Encrypt或者Encrypted返回的字段,進行動態(tài)加密 * 兩種方式互斥 */ private void encryptField(Encrypted object) throws IllegalAccessException, NoSuchFieldException { String[] encryptFields = object.getEncryptFields(); String factor = "xu.dm118dAADF!@$"; Class<?> clazz = object.getClass(); if(encryptFields.length==0){ Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); Encrypt encrypt = field.getAnnotation(Encrypt.class); if(encrypt!=null) { String encryptString = AesUtils.encrypt(field.get(object).toString(), factor); field.set(object,encryptString); log.debug("Encryption interceptor,encrypt field: {}",field.getName()); } } }else { for (String fieldName : encryptFields) { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); String encryptString = AesUtils.encrypt(field.get(object).toString(), factor); field.set(object,encryptString); log.debug("Encryption interceptor,encrypt field: {}",field.getName()); } } } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
3.4 攔截器解密數(shù)據(jù)
解密時攔截query方法,只對結果集判斷,結果屬于Encrypted接口或者結果結果集第一條數(shù)據(jù)屬于Encrypted接口則進入解密流程。
解密失敗或者解密方法返回空串后,不會修改原本字段數(shù)據(jù)。
/** * @author: xu.dm * @since: 2022/3/9 11:39 * 解密數(shù)據(jù),返回結果為list集合時,應保證集合里都是同一類型的元素。 * 解密失敗時返回為null,或者返回為空串時,不對原數(shù)據(jù)操作。 **/ @Component @Slf4j @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), }) public class DecryptionInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); if(result instanceof ArrayList) { @SuppressWarnings("rawtypes") ArrayList list = (ArrayList) result; if(list.size() == 0) { return result; } if(list.get(0) instanceof Encrypted) { for (Object item : list) { decryptField((Encrypted) item); } } return result; } if(result instanceof Encrypted) { decryptField((Encrypted) result); } return result; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } /** * @param object 待檢查的對象 * @throws IllegalAccessException * 通過查詢注解@Encrypt或者Encrypted返回的字段,進行解密 * 兩種方式互斥 */ private void decryptField(Encrypted object) throws IllegalAccessException, NoSuchFieldException { String[] encryptFields = object.getEncryptFields(); String factor = "xu.dm118dAADF!@$"; Class<?> clazz = object.getClass(); if(encryptFields.length==0){ Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); Encrypt encrypt = field.getAnnotation(Encrypt.class); if(encrypt!=null) { String encryptString = AesUtils.decrypt(field.get(object).toString(), factor); if(encryptString!=null){ field.set(object,encryptString); log.debug("Encryption interceptor,encrypt field: {}",field.getName()); } } } }else { for (String fieldName : encryptFields) { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); String encryptString = AesUtils.decrypt(field.get(object).toString(), factor); if(encryptString!=null && encryptString.length() > 0){ field.set(object,encryptString); log.debug("Encryption interceptor,encrypt field: {}",field.getName()); } } } } }
3.5 解密工具類
解密工具類可根據(jù)場景進一步優(yōu)化,例如:可考慮解密類實例化后常駐內存,以減少CPU負載。
/** * @author: xu.dm * @since: 2018/11/24 22:26 * */ public class AesUtils { private static final String ALGORITHM = "AES/ECB/PKCS5Padding"; public static String encrypt(String content, String key) { try { //獲得密碼的字節(jié)數(shù)組 byte[] raw = key.getBytes(); //根據(jù)密碼生成AES密鑰 SecretKeySpec keySpec = new SecretKeySpec(raw, "AES"); //根據(jù)指定算法ALGORITHM自成密碼器 Cipher cipher = Cipher.getInstance(ALGORITHM); //初始化密碼器,第一個參數(shù)為加密(ENCRYPT_MODE)或者解密(DECRYPT_MODE)操作,第二個參數(shù)為生成的AES密鑰 cipher.init(Cipher.ENCRYPT_MODE, keySpec); //獲取加密內容的字節(jié)數(shù)組(設置為utf-8)不然內容中如果有中文和英文混合中文就會解密為亂碼 byte [] contentBytes = content.getBytes(StandardCharsets.UTF_8); //密碼器加密數(shù)據(jù) byte [] encodeContent = cipher.doFinal(contentBytes); //將加密后的數(shù)據(jù)轉換為字符串返回 return Base64.encodeBase64String(encodeContent); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("AesUtils加密失敗"); } } public static String decrypt(String encryptStr, String decryptKey) { try { //獲得密碼的字節(jié)數(shù)組 byte[] raw = decryptKey.getBytes(); //根據(jù)密碼生成AES密鑰 SecretKeySpec keySpec = new SecretKeySpec(raw, "AES"); //根據(jù)指定算法ALGORITHM自成密碼器 Cipher cipher = Cipher.getInstance(ALGORITHM); //初始化密碼器,第一個參數(shù)為加密(ENCRYPT_MODE)或者解密(DECRYPT_MODE)操作,第二個參數(shù)為生成的AES密鑰 cipher.init(Cipher.DECRYPT_MODE, keySpec); //把密文字符串轉回密文字節(jié)數(shù)組 byte [] encodeContent = Base64.decodeBase64(encryptStr); //密碼器解密數(shù)據(jù) byte [] byteContent = cipher.doFinal(encodeContent); //將解密后的數(shù)據(jù)轉換為字符串返回 return new String(byteContent, StandardCharsets.UTF_8); } catch (Exception e) { // e.printStackTrace(); // 解密失敗暫時返回null,可以拋出runtime異常 return null; } } }
3.6 實體類樣例
/** * (SysUser)實體類 * * @author xu.dm * @since 2020-05-02 09:34:53 */ @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor @ToString(exclude = {"password","username"},callSuper = true) public class SysUser extends BaseDO implements Serializable, Encrypted { private static final long serialVersionUID = 100317866935565576L; /** * ID 轉換成字符串給前端,否則js會出現(xiàn)精度問題 * 對于前后臺傳參Long類型64位而言,當前端超過53位后會丟失精度,超過的部分會以00的形式展示. * 可以使用 @JsonSerialize(using = ToStringSerializer.class) */ @JsonSerialize(using = ToStringSerializer.class) private Long id; /** * 手機號碼 */ @Encrypt private String mobile; /** * 用戶登錄名稱 */ private String username; private String name; /** * 密碼 */ @JsonIgnore private String password; /** * email */ private String email; @Override public String[] getEncryptFields() { return new String[]{"mobile","name"}; } }
4、使用類型轉換器
在mybatis中使用類型轉換器,本質上就是就自定義一個類型(本質就是一個類),通過mybatis提供的TypeHandler接口擴展,對數(shù)據(jù)類型轉換,在這個過程中加入加密和解密業(yè)務邏輯實現(xiàn)數(shù)據(jù)存儲和查詢的加解密功能。
4.1 定義加密類型
這個類型就直接理解成類似java.lang.String
。如果對加密的方式有多種需求,可擴N種EncryptType
類型。
/** * @author: xu.dm * @since: 2022/3/9 16:54 * 自定義類型,用于在mybatis中表示加密類型 * 需要加密的字段使用EncryptType聲明 **/ public class EncryptType { private String value; public EncryptType() { } public EncryptType(String value) { this.value = value; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } @Override public String toString() { return value; } }
4.2 定義類型轉換處理器
AesUtils
工具類見上文描述。
轉換器繼承自mybatis
的BaseTypeHandler
,重寫值設置和值獲取的方法,在其過程中加入加密和解密邏輯。
/** * @author: xu.dm * @since: 2022/3/9 16:21 * 類型轉換器,處理EncryptType類型,用于數(shù)據(jù)加解密 **/ @MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes(EncryptType.class) public class EncryptTypeHandler extends BaseTypeHandler<EncryptType> { private String factor = "xu.dm118dAADF!@$"; @Override public void setNonNullParameter(PreparedStatement ps, int i, EncryptType parameter, JdbcType jdbcType) throws SQLException { if (parameter == null || parameter.getValue() == null) { ps.setString(i, null); return; } String encrypt = AesUtils.encrypt(parameter.getValue(),factor); ps.setString(i, encrypt); } @Override public EncryptType getNullableResult(ResultSet rs, String columnName) throws SQLException { String decrypt = AesUtils.decrypt(rs.getString(columnName), factor); if(decrypt==null || decrypt.length()==0){ decrypt = rs.getString(columnName); } return new EncryptType(decrypt); } @Override public EncryptType getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String decrypt = AesUtils.decrypt(rs.getString(columnIndex), factor); if(decrypt==null || decrypt.length()==0){ decrypt = rs.getString(columnIndex); } return new EncryptType(decrypt); } @Override public EncryptType getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String decrypt = AesUtils.decrypt(cs.getString(columnIndex), factor); if(decrypt==null || decrypt.length()==0){ decrypt = cs.getString(columnIndex); } return new EncryptType(decrypt); } }
4.3 配置類型轉換器的包路徑
這個配置是可選的,因為可以在mapper的映射xml文件中指定。
mybatis: #xml映射版才需要配置,純注解版本不需要 mapper-locations: classpath*:mapper/*.xml #多模塊指定sql映射文件的位置,需要在classpath后面多加一個星號 type-handlers-package: com.wood.encryption.handler
4.4 測試用的實體類
截取了部分代碼,關注代碼中使用EncryptType
類型的字段name和mobile。
/** * (TestUser)實體類 * * @author xu.dm * @since 2022-03-10 11:31:54 */ @Data public class TestUser extends BaseDO implements Serializable { private static final long serialVersionUID = -53491943096074862L; /** * ID */ private Long id; /** * 手機號碼 */ private EncryptType mobile; /** * 用戶登錄名稱 */ private String username; /** * 用戶名或昵稱 */ private EncryptType name; /** * 密碼 */ private String password; /** * email */ private String email; ... ... }
4.5 mapper接口文件
這個類沒有本質的變化,截取了部分代碼,注意EncryptType
類型的使用。
/** * (TestUser)表數(shù)據(jù)庫訪問層 * * @author xu.dm * @since 2022-03-10 11:31:54 */ public interface TestUserDao { /** * 查詢手機號碼,通過主鍵 * * @param id 主鍵 * @return 手機號碼 */ EncryptType queryMobileById(Long id); /** * 通過手機號碼查詢單條數(shù)據(jù) * * @param mobile 手機號碼 * @return 實例對象 */ List<TestUser> queryByMobile(EncryptType mobile); /** * 通過ID查詢單條數(shù)據(jù) * * @param id 主鍵 * @return 實例對象 */ TestUser queryById(Long id); /** * 查詢所有數(shù)據(jù),根據(jù)入?yún)?,決定是否模糊查詢 * * @param testUser 查詢條件 * * @return 對象列表 */ List<TestUser> queryByBlurry(TestUser testUser); /** * 統(tǒng)計總行數(shù) * * @param testUser 查詢條件 * @return 總行數(shù) */ long count(TestUser testUser); /** * 新增數(shù)據(jù) * * @param testUser 實例對象 * @return 影響行數(shù) */ int insert(TestUser testUser); /** * 修改數(shù)據(jù) * * @param testUser 實例對象 * @return 影響行數(shù) */ int update(TestUser testUser); }
4.6 mapper映射文件
沒有本質變化,截取了部分代碼,注意EncryptType
類型的使用。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.wood.system.dao.TestUserDao"> <resultMap type="com.wood.system.entity.TestUser" id="TestUserMap"> <result property="id" column="id" jdbcType="INTEGER"/> <result property="mobile" column="mobile" jdbcType="VARCHAR"/> <result property="username" column="username" jdbcType="VARCHAR"/> <result property="name" column="name" jdbcType="VARCHAR"/> <result property="password" column="password" jdbcType="VARCHAR"/> <result property="email" column="email" jdbcType="VARCHAR"/> <result property="state" column="state" jdbcType="VARCHAR"/> <result property="level" column="level" jdbcType="VARCHAR"/> <result property="companyId" column="company_id" jdbcType="INTEGER"/> <result property="deptId" column="dept_id" jdbcType="INTEGER"/> <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/> <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/> </resultMap> <!--查詢單個--> <select id="queryById" resultMap="TestUserMap"> select id, mobile, username, name, password, email, state, level, company_id, dept_id, create_time, update_time from test_user where id = #{id} </select> <!--查詢指定行數(shù)據(jù)--> <select id="queryByBlurry" resultMap="TestUserMap"> select id, mobile, username, name, password, email, state, level, company_id, dept_id, create_time, update_time from test_user <where> <if test="id != null"> and id = #{id} </if> <if test="mobile != null and mobile != ''"> and mobile = #{mobile} </if> <if test="username != null and username != ''"> and username = #{username} </if> <if test="name != null and name != ''"> and name = #{name} </if> ... ... </where> </select> <select id="queryMobileById" resultType="com.wood.encryption.type.EncryptType"> select mobile from test_user where id = #{id} </select> <select id="queryByMobile" resultType="com.wood.system.entity.TestUser"> select * from test_user where mobile = #{mobile} </select> <!--新增所有列--> <insert id="insert" keyProperty="id" useGeneratedKeys="false"> insert into test_user(id, mobile, username, name, password, email, state, level, company_id, dept_id, create_time, update_time) values (#{id}, #{mobile}, #{username}, #{name}, #{password}, #{email}, #{state}, #{level}, #{companyId}, #{deptId}, #{createTime}, #{updateTime}) </insert> <!--通過主鍵修改數(shù)據(jù)--> <update id="update"> update test_user <set> <if test="mobile != null and mobile != ''"> mobile = #{mobile}, </if> <if test="username != null and username != ''"> username = #{username}, </if> <if test="name != null and name != ''"> name = #{name}, </if> <if test="email != null and email != ''"> email = #{email}, </if> <if test="state != null and state != ''"> state = #{state}, </if> <if test="level != null and level != ''"> level = #{level}, </if> <if test="companyId != null"> company_id = #{companyId}, </if> <if test="deptId != null"> dept_id = #{deptId}, </if> <if test="updateTime != null"> update_time = #{updateTime}, </if> </set> where id = #{id} </update> </mapper>
到此這篇關于mybatis中數(shù)據(jù)加密與解密的實現(xiàn)的文章就介紹到這了,更多相關mybatis數(shù)據(jù)加密與解密內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
java中LinkedList使用迭代器優(yōu)化移除批量元素原理
本文主要介紹了java中LinkedList使用迭代器優(yōu)化移除批量元素原理,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-10-10Java通過切面實現(xiàn)統(tǒng)一處理Token設置用戶信息
這篇文章主要介紹了Java切面統(tǒng)一處理Token設置用戶信息,常見的后端開發(fā)中,接口請求中一般前端都是先通過用戶登錄獲取token,每次接口請求都需要在頭信息中攜帶token信息,后端每次都需要手動處理token信息,從token信息中解析獲取用戶信息,需要的朋友可以參考下2023-10-10IDEA的默認快捷鍵設置與Eclipse的常用快捷鍵的設置方法
這篇文章主要介紹了IDEA的默認快捷鍵設置與Eclipse的常用快捷鍵的設置方法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01Java?guava框架LoadingCache及CacheBuilder本地小容量緩存框架總結
Guava?Cache本地緩存框架主要是一種將本地數(shù)據(jù)緩存到內存中,但數(shù)據(jù)量并不能太大,否則將會占用過多的內存,本文給大家介紹Java?guava框架?LoadingCache及CacheBuilder?本地小容量緩存框架總結,感興趣的朋友一起看看吧2023-12-12