系統要求高安全性,試題的數據須要加密,防止在數據庫層面進行數據泄漏。java
但試題的加密還與密碼的加密不一樣,用戶密碼的加密可使用Hash
算法,沒法解密,不光在數據庫中,即使是咱們的程序也沒法獲悉用戶的密碼。面試
試題的加密要求數據庫層面是加密數據,從數據庫中查詢數據後,再對數據進行解密,發送給前臺。接口是通過精密的認證與鑑權的,保證安全。算法
最終對比各類方案,決定採用converter
的實現方案。spring
如上圖所示:當數據寫入前,對數據進行加密;數據查詢後,對數據進行解密。數據庫
全部的加密解密工做,均由自定義converter
完成。數組
照着Demo
寫了一個使用AES
加解密的converter
。安全
/** * 加密解決轉換器 * 對象字段數據 與 數據表列之間轉換 */ public class EncryptConverter implements AttributeConverter<String, String> { private static final Logger logger = LoggerFactory.getLogger(EncryptConverter.class); /** * AES 密鑰 */ private static final byte[] VALUE = "XQRhrQnGNFJf1WaSGOOJEjNhDjRPMG5N".getBytes(StandardCharsets.UTF_8); /** * 加密/解密 算法 */ private static final String ALGORITHM = "AES"; /** * 加密/解密 密鑰 */ private static final Key KEY = new SecretKeySpec(VALUE, ALGORITHM); /** * 加密過程 * 從對象字段數據 到 數據庫列 */ @Override public String convertToDatabaseColumn(String data) { String result; try { logger.debug("獲取算法"); Cipher cipher = Cipher.getInstance(ALGORITHM); logger.debug("設置加密模式與加密密鑰"); cipher.init(Cipher.ENCRYPT_MODE, KEY); logger.debug("獲取原始內容"); byte[] rawData = data.getBytes(StandardCharsets.UTF_8); logger.debug("加密"); byte[] encryptedData = cipher.doFinal(rawData); logger.debug("加密後的字節數組編碼爲字符串"); result = new String(encryptedData); } catch (IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException("encrypt error!", e); } return result; } @Override public String convertToEntityAttribute(String data) { String result; try { logger.debug("獲取算法"); Cipher cipher = Cipher.getInstance(ALGORITHM); logger.debug("設置解密模式與解密密鑰"); cipher.init(Cipher.DECRYPT_MODE, KEY); logger.debug("獲取加密字節數組"); byte[] encryptedData = data.getBytes(StandardCharsets.UTF_8); logger.debug("解密爲原始內容"); byte[] rawData = cipher.doFinal(encryptedData); logger.debug("解密"); result = new String(rawData); } catch (IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException("decrypt error!", e); } return result; } }
再對要加密的字段添加@Convert
註解,指明converter
爲以前編寫的EncryptConverter.class
。app
@Entity public class Information { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Convert(converter = EncryptConverter.class) private String content; }
跑一個單元測試看看是否生效。ide
@Test void encrypt() { Information information = new Information(); information.setContent("測試內容"); informationRepository.save(information); Optional<Information> optional = informationRepository.findById(information.getId()); System.out.println(optional); }
出錯了,忽然發現事情並不簡單。函數
由於異常處理得比較好,定位錯誤十分迅速:
// JPA異常:使用AttributeConverter時發生錯誤。 org.springframework.orm.jpa.JpaSystemException: Error attempting to apply AttributeConverter; nested exception is javax.persistence.PersistenceException: Error attempting to apply AttributeConverter. // 由這個錯誤引發:持久化錯誤:使用AttributeConverter時發生錯誤。 Caused by: javax.persistence.PersistenceException: Error attempting to apply AttributeConverter. // 由這個錯誤引發:運行錯誤:加密錯誤! Caused by: java.lang.RuntimeException: decrypt error! // 由這個錯誤引發:非法的塊大小異常,當解密時輸入長度必須是16的倍數。 Caused by: javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher.
同時,加密後的數據也有問題:
ag�U�)q\�|����
通過調試與錯誤排查,發現問題出在字節數組與字符串的轉換上。
logger.debug("加密"); byte[] encryptedData = cipher.doFinal(rawData); logger.debug("加密後的字節數組編碼爲字符串"); result = new String(encryptedData);
通讀String
類源碼,解決該問題。
在String
類內部實現中,字符串與字節數組之間的轉換,是經過編碼與解碼實現的。
getBytes
方法,將字符串轉換爲字節數組,即字符到二進制的編碼。
public byte[] getBytes(Charset charset) { if (charset == null) throw new NullPointerException(); return StringCoding.encode(charset, value, 0, value.length); }
String
類默認採用UTF-8
編碼,編碼時調用UTF_8
類內部的encode
方法,將字符數組編碼爲字節數組。
encode
源碼,建議閱讀。
public int encode(char[] sa, int sp, int len, byte[] da) { int sl = sp + len; int dp = 0; int dlASCII = dp + Math.min(len, da.length); // ASCII only optimized loop while (dp < dlASCII && sa[sp] < '\u0080') da[dp++] = (byte) sa[sp++]; while (sp < sl) { char c = sa[sp++]; if (c < 0x80) { // Have at most seven bits da[dp++] = (byte)c; } else if (c < 0x800) { // 2 bytes, 11 bits da[dp++] = (byte)(0xc0 | (c >> 6)); da[dp++] = (byte)(0x80 | (c & 0x3f)); } else if (Character.isSurrogate(c)) { if (sgp == null) sgp = new Surrogate.Parser(); int uc = sgp.parse(c, sa, sp - 1, sl); if (uc < 0) { if (malformedInputAction() != CodingErrorAction.REPLACE) return -1; da[dp++] = repl; } else { da[dp++] = (byte)(0xf0 | ((uc >> 18))); da[dp++] = (byte)(0x80 | ((uc >> 12) & 0x3f)); da[dp++] = (byte)(0x80 | ((uc >> 6) & 0x3f)); da[dp++] = (byte)(0x80 | (uc & 0x3f)); sp++; // 2 chars } } else { // 3 bytes, 16 bits da[dp++] = (byte)(0xe0 | ((c >> 12))); da[dp++] = (byte)(0x80 | ((c >> 6) & 0x3f)); da[dp++] = (byte)(0x80 | (c & 0x3f)); } } return dp; }
String
類中的bytes
構造函數,將字節數組轉換爲字符串,即二進制到字符的解碼。
public String(byte bytes[]) { this(bytes, 0, bytes.length); }
與編碼相似,解碼時調用UTF_8
類內部的decode
方法,將字節數組解碼爲字符數組。
decode
源碼,建議閱讀。
public int decode(byte[] sa, int sp, int len, char[] da) { final int sl = sp + len; int dp = 0; int dlASCII = Math.min(len, da.length); ByteBuffer bb = null; // only necessary if malformed // ASCII only optimized loop while (dp < dlASCII && sa[sp] >= 0) da[dp++] = (char) sa[sp++]; while (sp < sl) { int b1 = sa[sp++]; if (b1 >= 0) { // 1 byte, 7 bits: 0xxxxxxx da[dp++] = (char) b1; } else if ((b1 >> 5) == -2 && (b1 & 0x1e) != 0) { // 2 bytes, 11 bits: 110xxxxx 10xxxxxx if (sp < sl) { int b2 = sa[sp++]; if (isNotContinuation(b2)) { if (malformedInputAction() != CodingErrorAction.REPLACE) return -1; da[dp++] = replacement().charAt(0); sp--; // malformedN(bb, 2) always returns 1 } else { da[dp++] = (char) (((b1 << 6) ^ b2)^ (((byte) 0xC0 << 6) ^ ((byte) 0x80 << 0))); } continue; } if (malformedInputAction() != CodingErrorAction.REPLACE) return -1; da[dp++] = replacement().charAt(0); return dp; } else if ((b1 >> 4) == -2) { // 3 bytes, 16 bits: 1110xxxx 10xxxxxx 10xxxxxx if (sp + 1 < sl) { int b2 = sa[sp++]; int b3 = sa[sp++]; if (isMalformed3(b1, b2, b3)) { if (malformedInputAction() != CodingErrorAction.REPLACE) return -1; da[dp++] = replacement().charAt(0); sp -= 3; bb = getByteBuffer(bb, sa, sp); sp += malformedN(bb, 3).length(); } else { char c = (char)((b1 << 12) ^ (b2 << 6) ^ (b3 ^ (((byte) 0xE0 << 12) ^ ((byte) 0x80 << 6) ^ ((byte) 0x80 << 0)))); if (Character.isSurrogate(c)) { if (malformedInputAction() != CodingErrorAction.REPLACE) return -1; da[dp++] = replacement().charAt(0); } else { da[dp++] = c; } } continue; } if (malformedInputAction() != CodingErrorAction.REPLACE) return -1; if (sp < sl && isMalformed3_2(b1, sa[sp])) { da[dp++] = replacement().charAt(0); continue; } da[dp++] = replacement().charAt(0); return dp; } else if ((b1 >> 3) == -2) { // 4 bytes, 21 bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if (sp + 2 < sl) { int b2 = sa[sp++]; int b3 = sa[sp++]; int b4 = sa[sp++]; int uc = ((b1 << 18) ^ (b2 << 12) ^ (b3 << 6) ^ (b4 ^ (((byte) 0xF0 << 18) ^ ((byte) 0x80 << 12) ^ ((byte) 0x80 << 6) ^ ((byte) 0x80 << 0)))); if (isMalformed4(b2, b3, b4) || // shortest form check !Character.isSupplementaryCodePoint(uc)) { if (malformedInputAction() != CodingErrorAction.REPLACE) return -1; da[dp++] = replacement().charAt(0); sp -= 4; bb = getByteBuffer(bb, sa, sp); sp += malformedN(bb, 4).length(); } else { da[dp++] = Character.highSurrogate(uc); da[dp++] = Character.lowSurrogate(uc); } continue; } if (malformedInputAction() != CodingErrorAction.REPLACE) return -1; b1 &= 0xff; if (b1 > 0xf4 || sp < sl && isMalformed4_2(b1, sa[sp] & 0xff)) { da[dp++] = replacement().charAt(0); continue; } sp++; if (sp < sl && isMalformed4_3(sa[sp])) { da[dp++] = replacement().charAt(0); continue; } da[dp++] = replacement().charAt(0); return dp; } else { if (malformedInputAction() != CodingErrorAction.REPLACE) return -1; da[dp++] = replacement().charAt(0); } } return dp; }
若是閱讀過源碼以後,或者對編碼比較熟悉,應該明白問題出在哪裏了。
解碼過程當中可能出現解碼錯誤的狀況。
UTF-8
中,一個字符確定對應着一個編碼,可是一個編碼,不必定對應着一個字符。
logger.debug("加密"); byte[] encryptedData = cipher.doFinal(rawData); logger.debug("加密後的字節數組編碼爲字符串"); result = new String(encryptedData);
因此,加密後的字節數組不符合UTF-8
編碼規則,解碼錯誤。
而後,數據庫中加密後的數據就這樣了:
ag�U�)q\�|����
解密的時候,查詢這個異常的字符串,再調用getBytes
方法將其編碼爲字節數組。
由於解碼的錯誤,因此這裏的編碼結果也不正確,因此解密中獲取的字節數組與原加密後的字節數組不一樣。
如上圖所示:左側爲編碼時的字節數組各下標內容,右側爲解碼時的字節數組各下標內容。
二者不等價,這在Java
術語中稱爲DATA LOSS
。
因此會有這句建議:
String is not a good container for binary data.
將原須要進行編碼的new String(bytes)
修改成對二進制數據無損的方法調用便可。
我這裏採用Base64
方式對字節數組進行編碼解碼,保證數據無損。
完整代碼:
/** * 加密解決轉換器 * 對象字段數據 與 數據表列之間轉換 */ public class EncryptConverter implements AttributeConverter<String, String> { private static final Logger logger = LoggerFactory.getLogger(EncryptConverter.class); /** * AES 密鑰 */ private static final byte[] VALUE = "XQRhrQnGNFJf1WaSGOOJEjNhDjRPMG5N".getBytes(StandardCharsets.UTF_8); /** * 加密/解密 算法 */ private static final String ALGORITHM = "AES"; /** * 加密/解密 密鑰 */ private static final Key KEY = new SecretKeySpec(VALUE, ALGORITHM); /** * 加密過程 * 從對象字段數據 到 數據庫列 */ @Override public String convertToDatabaseColumn(String data) { String result; try { logger.debug("獲取算法"); Cipher cipher = Cipher.getInstance(ALGORITHM); logger.debug("設置加密模式與加密密鑰"); cipher.init(Cipher.ENCRYPT_MODE, KEY); logger.debug("獲取原始內容"); byte[] rawData = data.getBytes(StandardCharsets.UTF_8); logger.debug("加密"); byte[] encryptedData = cipher.doFinal(rawData); logger.debug("BASE64 將加密後的字節數組編碼爲字符串"); result = Base64.getEncoder().encodeToString(encryptedData); } catch (IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException("encrypt error!", e); } return result; } @Override public String convertToEntityAttribute(String data) { String result; try { logger.debug("獲取算法"); Cipher cipher = Cipher.getInstance(ALGORITHM); logger.debug("設置解密模式與解密密鑰"); cipher.init(Cipher.DECRYPT_MODE, KEY); logger.debug("BASE64 將編碼後的字符串解碼爲加密字節數組"); byte[] encryptedData = Base64.getDecoder().decode(data); logger.debug("解密爲原始內容"); byte[] rawData = cipher.doFinal(encryptedData); logger.debug("解密"); result = new String(rawData); } catch (IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException("decrypt error!", e); } return result; } }
再次運行該單元測試。
@Test void encrypt() { Information information = new Information(); information.setContent("測試內容"); informationRepository.save(information); Optional<Information> optional = informationRepository.findById(information.getId()); System.out.println(optional); }
數據表中數據完成加密:
數據查詢後自動解密:
源碼,由於面試而變了味道。
程序,由於資本而變得功利。
你,還記得寫代碼的初心嗎?