Springboot 数据安全传输加密与解密

时间:2022-03-16 18:50:07

Springboot 数据安全传输加密与解密

环境:springboot2.2.6.RELEASE、Vue+axios

通过继承RequestBodyAdviceAdapter实现对于请求的内容进行解密操作,实现ResponseBodyAdvice来对相应内容进行加密处理。

定义加密解密的接口:

SecretProcess.java

  1. public interface SecretProcess { 
  2.      
  3.     /** 
  4.      *  <p>数据加密</p> 
  5.      *  <p>时间:2020年12月24日-下午12:22:13</p> 
  6.      * @author xg 
  7.      * @param data 待加密数据 
  8.      * @return String 加密结果 
  9.      */ 
  10.     String encrypt(String data) ; 
  11.      
  12.     /** 
  13.      *  <p>数据解密</p> 
  14.      *  <p>时间:2020年12月24日-下午12:23:20</p> 
  15.      * @author xg 
  16.      * @param data 待解密数据 
  17.      * @return String 解密后的数据 
  18.      */ 
  19.     String decrypt(String data) ; 
  20.      
  21.     /** 
  22.      *  <p>加密算法格式:算法[/模式/填充]</p> 
  23.      *  <p>时间:2020年12月24日-下午12:32:49</p> 
  24.      * @author xg 
  25.      * @return String 
  26.      */ 
  27.     String getAlgorithm() ; 
  28.      
  29.     public static class Hex { 
  30.          
  31.         private static final char[] HEX = { '0''1''2''3''4''5''6''7''8''9'
  32.                 'a''b''c''d''e''f' }; 
  33.          
  34.         public static byte[] decode(CharSequence s) { 
  35.             int nChars = s.length(); 
  36.             if (nChars % 2 != 0) { 
  37.                 throw new IllegalArgumentException("16进制数据错误"); 
  38.             } 
  39.             byte[] result = new byte[nChars / 2]; 
  40.             for (int i = 0; i < nChars; i += 2) { 
  41.                 int msb = Character.digit(s.charAt(i), 16); 
  42.                 int lsb = Character.digit(s.charAt(i + 1), 16); 
  43.                 if (msb < 0 || lsb < 0) { 
  44.                     throw new IllegalArgumentException( 
  45.                         "Detected a Non-hex character at " + (i + 1) + " or " + (i + 2) + " position"); 
  46.                 } 
  47.                 result[i / 2] = (byte) ((msb << 4) | lsb); 
  48.             } 
  49.             return result; 
  50.         } 
  51.          
  52.         public static String encode(byte[] buf) { 
  53.             StringBuilder sb = new StringBuilder() ; 
  54.             for (int i = 0, leng = buf.length; i < leng; i++) { 
  55.                 sb.append(HEX[(buf[i] & 0xF0) >>> 4]).append(HEX[buf[i] & 0x0F]) ; 
  56.             } 
  57.             return sb.toString() ; 
  58.         } 
  59.          
  60.     } 
  61.      

该接口中定义了两个方法分别是加密与解密的方法,还有Hex类 该类用来对数据处理16进制的转换。

定义一个抽象类实现上面的接口,具体的加解密实现细节在该抽象类中

AbstractSecretProcess.java

  1. public abstract class AbstractSecretProcess implements SecretProcess { 
  2.      
  3.     @Resource 
  4.     private SecretProperties props ; 
  5.      
  6.     @Override 
  7.     public String decrypt(String data) { 
  8.         try { 
  9.             Cipher cipher = Cipher.getInstance(getAlgorithm()) ; 
  10.             cipher.init(Cipher.DECRYPT_MODE, keySpec()) ; 
  11.             byte[] decryptBytes = cipher.doFinal(Hex.decode(data)) ; 
  12.             return new String(decryptBytes) ; 
  13.         } catch (Exception e) { 
  14.             throw new RuntimeException(e) ; 
  15.         } 
  16.     } 
  17.      
  18.     @Override 
  19.     public String encrypt(String data) { 
  20.         try { 
  21.             Cipher cipher = Cipher.getInstance(getAlgorithm()) ; 
  22.             cipher.init(Cipher.ENCRYPT_MODE, keySpec()) ; 
  23.             return Hex.encode(cipher.doFinal(data.getBytes(Charset.forName("UTF-8")))) ; 
  24.         } catch (Exception e) { 
  25.             throw new RuntimeException(e) ; 
  26.         } 
  27.     } 
  28.      
  29.     /** 
  30.      *  <p>根据密钥生成不同的密钥材料</p> 
  31.      *  <p>目前支持:AES, DES</p> 
  32.      *  <p>时间:2020年12月25日-下午1:02:54</p> 
  33.      * @author xg 
  34.      * @param secretKey 密钥 
  35.      * @param algorithm 算法 
  36.      * @return Key 
  37.      */ 
  38.     public Key getKeySpec(String algorithm) { 
  39.         if (algorithm == null || algorithm.trim().length() == 0) { 
  40.             return null ; 
  41.         } 
  42.         String secretKey = props.getKey() ; 
  43.         switch (algorithm.toUpperCase()) { 
  44.             case "AES"
  45.                 return new SecretKeySpec(secretKey.getBytes(), "AES") ; 
  46.             case "DES"
  47.                 Key key = null ; 
  48.                 try { 
  49.                     DESKeySpec desKeySpec = new DESKeySpec(secretKey.getBytes()) ; 
  50.                     SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("DES") ; 
  51.                     key = secretKeyFactory.generateSecret(desKeySpec); 
  52.                 } catch (Exception e) { 
  53.                     throw new RuntimeException(e) ; 
  54.                 } 
  55.                 return key ; 
  56.             default
  57.                 return null ; 
  58.         } 
  59.     } 
  60.      
  61.     /** 
  62.      *  <p>生成密钥材料</p> 
  63.      *  <p>时间:2020年12月25日-上午11:35:03</p> 
  64.      * @author xg 
  65.      * @return Key 密钥材料 
  66.      */ 
  67.     public abstract Key keySpec() ; 
  68.      

该抽象类中提供了2中对称加密的密钥还原,分表是AES和DES算法。一个抽象方法,该抽象方法

keySpec该方法需要子类实现(具体使用的是哪种对称加密算法)。

具体加密算法的实现类

AESAlgorithm.java

  1. public class AESAlgorithm extends AbstractSecretProcess { 
  2.  
  3.     @Override 
  4.     public String getAlgorithm() { 
  5.         return "AES/ECB/PKCS5Padding"
  6.     } 
  7.      
  8.     @Override 
  9.     public Key keySpec() { 
  10.         return this.getKeySpec("AES") ; 
  11.     } 
  12.  

SecretProperties.java 属性配置类

  1. @Configuration 
  2. public class SecretConfig { 
  3.      
  4.     @Bean 
  5.     @ConditionalOnMissingBean(SecretProcess.class) 
  6.     public SecretProcess secretProcess() { 
  7.         return new AESAlgorithm() ; 
  8.     } 
  9.      
  10.     @Component 
  11.     @ConfigurationProperties(prefix = "secret"
  12.     public static class SecretProperties { 
  13.          
  14.         private Boolean enabled ; 
  15.         private String key ; 
  16.  
  17.         public Boolean getEnabled() { 
  18.             return enabled; 
  19.         } 
  20.  
  21.         public void setEnabled(Boolean enabled) { 
  22.             this.enabled = enabled; 
  23.         } 
  24.  
  25.         public String getKey() { 
  26.             return key
  27.         } 
  28.  
  29.         public void setKey(String key) { 
  30.             this.key = key
  31.         } 
  32.          
  33.     } 
  34.      

配置文件中如下配置:

  1. secret: 
  2.   key: aaaabbbbccccdddd #密钥 
  3.   enabled: true #是否开启加解密功能 

在项目中可能不是所有的方法都要进行数据的加密解密出来,所以接下来定义一个注解,只有添加有该注解的Controller类或是具体接口方法才进行数据的加密解密,如下:

SIProtection.java

  1. @Target({ElementType.METHOD, ElementType.TYPE}) 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. @Mapping 
  4. @Documented 
  5. public @interface SIProtection { 
  6.  

对请求内容进行解密出来,通过RequestBodyAdvice

DecryptRequestBodyAdivce.java

  1. @ControllerAdvice 
  2. @ConditionalOnProperty(name = "secret.enabled", havingValue = "true"
  3. public class DecryptRequestBodyAdivce extends RequestBodyAdviceAdapter { 
  4.  
  5.     @Resource 
  6.     private SecretProcess secretProcess ; 
  7.      
  8.     @Override 
  9.     public boolean supports(MethodParameter methodParameter, Type targetType, 
  10.             Class<? extends HttpMessageConverter<?>> converterType) { 
  11.         return methodParameter.getMethod().isAnnotationPresent(SIProtection.class)  
  12.                 || methodParameter.getMethod().getDeclaringClass().isAnnotationPresent(SIProtection.class) ; 
  13.     } 
  14.  
  15.     @Override 
  16.     public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, 
  17.             Class<? extends HttpMessageConverter<?>> converterType) throws IOException { 
  18.         String body = secretProcess.decrypt(inToString(inputMessage.getBody())) ; 
  19.         return new HttpInputMessage() { 
  20.             @Override 
  21.             public HttpHeaders getHeaders() { 
  22.                 return inputMessage.getHeaders(); 
  23.             } 
  24.             @Override 
  25.             public InputStream getBody() throws IOException { 
  26.                 return new ByteArrayInputStream(body.getBytes()) ; 
  27.             } 
  28.         } ; 
  29.     } 
  30.      
  31.     private String inToString(InputStream is) { 
  32.         byte[] buf = new byte[10 * 1024] ; 
  33.         int leng = -1 ; 
  34.         StringBuilder sb = new StringBuilder() ; 
  35.         try { 
  36.             while ((leng = is.read(buf)) != -1) { 
  37.                 sb.append(new String(buf, 0, leng)) ; 
  38.             } 
  39.             return sb.toString() ; 
  40.         } catch (IOException e) { 
  41.             throw new RuntimeException(e) ; 
  42.         } 
  43.     } 
  44.  

注意这里的:@ConditionalOnProperty(name = "secret.enabled", havingValue = "true")注解,只有开启了加解密功能才会生效。注意这里的supports方法

对响应内容加密出来

EncryptResponseBodyAdivce.java

  1. @ControllerAdvice 
  2. @ConditionalOnProperty(name = "secret.enabled", havingValue = "true"
  3. public class EncryptResponseBodyAdivce implements ResponseBodyAdvice<Object>  { 
  4.  
  5.     @Resource 
  6.     private SecretProcess secretProcess ; 
  7.  
  8.     @Override 
  9.     public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { 
  10.         return returnType.getMethod().isAnnotationPresent(SIProtection.class)  
  11.                 || returnType.getMethod().getDeclaringClass().isAnnotationPresent(SIProtection.class) ; 
  12.     } 
  13.  
  14.     @Override 
  15.     public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, 
  16.             Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, 
  17.             ServerHttpResponse response) { 
  18.         if (body == null) { 
  19.             return body ; 
  20.         } 
  21.         try { 
  22.             String jsonStr = new ObjectMapper().writeValueAsString(body) ; 
  23.             return secretProcess.encrypt(jsonStr) ; 
  24.         } catch (Exception e) { 
  25.             throw new RuntimeException(e) ; 
  26.         } 
  27.     } 

Controller应用

  1. @PostMapping("/save"
  2.     @SIProtection 
  3.     public R save(@RequestBody Users users) { 
  4.         return R.success(usersService.save(users)) ; 
  5.     } // 这对具体方法进行加解密 
  6.  
  7. @RestController 
  8. @RequestMapping("/users"
  9. @SIProtection  
  10. public class UsersController { // 对该Controller中的所有方法进行加解密处理 

前端

引入第三方插件:crypto-js

工具方法加解密:

  1. /** 
  2.      * 加密方法 
  3.      * @param data 待加密数据 
  4.      * @returns {string|*} 
  5.      */ 
  6.     encrypt (data) { 
  7.       let key = CryptoJS.enc.Utf8.parse(Consts.Secret.key
  8.       if (typeof data === 'object') { 
  9.         data = JSON.stringify(data) 
  10.       } 
  11.       let plainText = CryptoJS.enc.Utf8.parse(data) 
  12.       let secretText = CryptoJS.AES.encrypt(plainText, key, {mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7}).ciphertext.toString() 
  13.       return secretText 
  14.     }, 
  15.     /** 
  16.      * 解密数据 
  17.      * @param data 待解密数据 
  18.      */ 
  19.     decrypt (data) { 
  20.       let key = CryptoJS.enc.Utf8.parse(Consts.Secret.key
  21.       let secretText = CryptoJS.enc.Hex.parse(data) 
  22.       let encryptedBase64Str = CryptoJS.enc.Base64.stringify(secretText) 
  23.       let result = CryptoJS.AES.decrypt(encryptedBase64Str, key, {mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7}).toString(CryptoJS.enc.Utf8) 
  24.       return JSON.parse(result) 
  25.     } 

配置:

  1. let Consts = { 
  2.   Secret: { 
  3.     key'aaaabbbbccccdddd', // 必须16位(前后端要一致,密钥) 
  4.     urls: ['/users/save'
  5.   } 
  6. export default Consts 

这里的urls表示对那些请求进行拦截出来(加解密),这里也可以配置 "*" 表示对所有的请求出来。

axios请求前和响应后对数据进行加解密出来:

发送请求前:

  1. axios.interceptors.request.use((config) => { 
  2.       let uri = config.url 
  3.       if (uri.includes('?')) { 
  4.         uri = uri.substring(0, uri.indexOf('?')) 
  5.       } 
  6.       if (window.cfg.enableSecret === '1' && config.data && (Consts.Secret.urls.indexOf('*') > -1 || Consts.Secret.urls.indexOf(uri) > -1)) { 
  7.         let data = config.data 
  8.         let secretText = Utils.Secret.encrypt(data) 
  9.         config.data = secretText 
  10.       } 
  11.       return config 
  12.     }, (error) => { 
  13.       let errorMessage = '请求失败' 
  14.       store.dispatch(types.G_SHOW_ALERT, {title: '请求失败', content: errorMessage, showDetail: false, detailContent: String(error)}) 
  15.       return Promise.reject(error) 
  16.     }) 
  17. axios.interceptors.response.use((response) => { 
  18.       let uri = response.config.url 
  19.       if (uri.includes('?')) { 
  20.         uri = uri.substring(0, uri.indexOf('?')) 
  21.       } 
  22.       if (window.cfg.enableSecret === '1' && response.data && (Consts.Secret.urls.indexOf('*') > -1 || Consts.Secret.urls.indexOf(uri) > -1)) { 
  23.         let data = Utils.Secret.decrypt(response.data) 
  24.         if (data) { 
  25.           response.data = data 
  26.         } 
  27.       } 
  28.       return response 
  29.     }, (error) => { 
  30.       console.error(`test interceptors.response is in, ${error}`) 
  31.       return Promise.reject(error) 
  32.     }) 

这里的 window.cfg.enableSecret 配置是我自己项目中有个配置文件配置是否开启,这个大家可以根据自己的环境来实现。

测试:

Springboot 数据安全传输加密与解密

这里可以看到前端发起的请求内容已经被加密了

响应内容:

Springboot 数据安全传输加密与解密

完毕!!!

原文地址:https://www.toutiao.com/i6914092084014170628/