对称加密只有一个秘钥,加密和解密都是用同一个秘钥,所以叫做对称加密。
非对称加密有两个秘钥,一个是公钥,一个是私钥。非对称的特点在于,公钥加密的私钥可以解密,但私钥加密的,公钥解不出来,只能验证是否由私钥进行加密
目前常见的加密方式是有两种,一种是对称加密(AES为代表),一种是非对称加密(RSA为代表)
特点:只需交换公钥;公/秘钥机制,公钥加密,私钥解密(或者私钥加密,公钥解密);公钥负责加密,私钥负责解密;私钥负责签名,公钥负责验证
缺点:加解密速度慢,特别是解密
特点:加解密用同一秘钥
优点:速度快,效率高;
缺点:秘钥交换问题
对称加密(AES)的优势在于加密较快,**但**劣势在于秘钥一旦给出去就不安全了。非对称加密(RSA)的优势在于安全,就算提供公钥出去,别人也解密不了数据,但劣势是加密速度较慢
实际使用的过程中常常将两者组合使用(AES+RSA),这样可以安全的传输AES秘钥,避免了RSA加密的慢速度
加密后的数据可能不具备可读性,因此我们一般需要对加密后的数据再使用 Base64 算法进行编码,获取可读字符串。换言之,AES 或者RSA加密方法的返回值是一个 Base64 编码之后的字符串,AES或者RSA 解密方法的参数也是一个 Base64 编码之后的字符串,先对该字符串进行解码,然后再解密。
public class Config { public static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding"; public static final String RSA_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; //必须是PKCS8格式 public static final String CLIENT_PRIVATE_KEY = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO/8ucCgOTJ7DCPC" + "rCCL1VKDnUX61QnxwbAvpGp1/lletEIcjUouM7F0VvMHzViNLvpw7N7NBHPa+5gO" + "js68t9hKMUh+a6RTE34SWIqSDRPCzDKVWugsFb04o3vRl3rZ1z6B+QDdW7xwOhEr" + "PPoEqmjjIOjQPcU6xs0SPzSimOa1AgMBAAECgYAO5m0OBaSnerZNPhf7yVLMVbmd" + "D67MeEMjUkHuDjdlixi8BhPLqESzXtrLKg/Y0KM7D2nVh3sgSldWoIjDUzpCx8Z2" + "yHLU1K2wakMdBgEF3xeJPxxZRpP+earl0SyLTA4hMxl48uAjn/mkPgzoMgQkqyQz" + "5HOWjjsCLJFyEvqmoQJBAP5cBk0KXpHnCMgOupbi/pXDyaF1o+dCE97GaEdrV/0P" + "uwDfYDYfY3wzd1QM7C4b4MmE+SNVpC0W9PyaMONJlN0CQQDxiPiGdwX9actMNJea" + "JZ+k3BjCN+mM6Px7j/mtYcXWNZkyCXSXUBI62drZ0htenrh2qwichMlMgNJClvG6" + "Gu+5AkEA30R7q2gstrkrNh/nnMZHXcJr3DPc2QNhWayin/4TT+hc51krpJZMxxqN" + "5dMqBRcnavwzi9aCs6lxBcF6pCdUaQJANhd7uPls4PzRZ6abkQz9/LjB3rUQ29rN" + "uIpc2yR7XuawAVG2x7BJ9N4XMhLoyD75hrH1AsCGKFjtPbZ6OjiQGQJAF2DbIodC" + "uYb6eMZ8ux1Ab0wBEWWc5+iGgEVBNh22uZ/klE1/C0+KKzZhqgzaA/vPapq6dhuJ" + "sNXlJia10PwYrQ=="; public static final String CLIENT_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDv/LnAoDkyewwjwqwgi9VSg51F" + "+tUJ8cGwL6Rqdf5ZXrRCHI1KLjOxdFbzB81YjS76cOzezQRz2vuYDo7OvLfYSjFI" + "fmukUxN+EliKkg0TwswylVroLBW9OKN70Zd62dc+gfkA3Vu8cDoRKzz6BKpo4yDo" + "0D3FOsbNEj80opjmtQIDAQAB"; public static final String SERVER_PRIVATE_KEY = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAPGkxlAJPKR3BRxT" + "PIeB3pDv117j8XbpuEik5UIOlY3GUtAV1sad5NNDUAnP/DB80yAQ8ycm9Xdkutuo" + "f25Xlb7w0bRQNpfJlijx9eF8PsB6t63r8KAfWJlqbNHgN8AMK9P5XzVyN4YiEnUl" + "Jh/EYiwLiYzflNnmnnfRrI4nUo8fAgMBAAECgYEAvwTxm81heeV4Tcbi33/jUBG4" + "4BMzCzyA6DQp4wkiYju3tTS+Xq3seLEKcWdPxYi3YO7lODsM6j/fksrlSXXFMe1i" + "ZAF3FNuDVZPz2zdFYS8vh6kdlDHMJAUnU/POMMWJ880MQDtkwTuzH8Tao8OKcAP4" + "kc0QuG00wOrmuE+5gZECQQD9bqZkJsN+tj3+pxs57azy6B6gOqgm54/ujB+u63XU" + "rO9Sf57asgF4OfUFltaVhjlUMSrWcgp6f4HSy7hBSKJpAkEA9BeML5iDIHOgTIws" + "+ID55ELbzO7A/YtcYnUU09mkKCdonMXbXke+EhLApf5vX9ZmreoEfJCdsTnMEcQi" + "fkjkRwJBALpf2TXl2/cfhs/zjG45f+rTEVK8UFTsDklb+yDkQC87TnTZLbWfGr2T" + "wcFugDhOEXL9BYfXLiWQB6VB9Crug6ECQGEmTiFTbj0oSBCvaeauTsdO5PS3whAn" + "u2lkeBmpcfCZXsWm6hyoKTpARHTMw789Mjjd/1Mkq96xxkr76U6h7FkCQHRc2elg" + "Dh84wqHIptwa+moosVvd7aSzktuOB4CQRO10qKkSHVFuI+sl47A4KGzH/nX9ydUm" + "tpsTnQAlXwBczd4="; public static final String SERVER_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDxpMZQCTykdwUcUzyHgd6Q79de" + "4/F26bhIpOVCDpWNxlLQFdbGneTTQ1AJz/wwfNMgEPMnJvV3ZLrbqH9uV5W+8NG0" + "UDaXyZYo8fXhfD7Aeret6/CgH1iZamzR4DfADCvT+V81cjeGIhJ1JSYfxGIsC4mM" + "35TZ5p530ayOJ1KPHwIDAQAB"; }
import javax.crypto.Cipher; import javax.crypto.spec.OAEPParameterSpec; import javax.crypto.spec.PSource; import java.security.*; import java.security.spec.MGF1ParameterSpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import org.springframework.util.Base64Utils; public class RSACipher { /** * 获取公钥 * @param key 密钥字符串(经过base64编码) * @return 公钥 */ public static PublicKey getPublicKey(String key) throws Exception { // 按照X.509标准对其进行编码的密钥 X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64Utils.decode(key.getBytes())); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); // 生成公钥 PublicKey publicKey = keyFactory.generatePublic(keySpec); return publicKey; } /** * 获取私钥 * @param key 密钥字符串(经过base64编码) * @return 私钥 */ public static PrivateKey getPrivateKey(String key) throws Exception { // 按照PKCS8格式标准对其进行编码的密钥,首先要将key进行base64解码 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64Utils.decode(key.getBytes())); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); // 生成私钥 PrivateKey privateKey = keyFactory.generatePrivate(keySpec); return privateKey; } /** * 加密方法 * @param publicKey 公钥 * @param raw 待加密明文 * @return 加密后的密文 */ public static byte[] encrypt(String publicKey, byte[] raw) throws Exception { Key key = getPublicKey(publicKey); Cipher cipher = Cipher.getInstance(Config.RSA_ALGORITHM); // 初始化 cipher.init(Cipher.ENCRYPT_MODE, key, new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT)); byte[] encryption = cipher.doFinal(raw); // 最后将加密后的数据进行base64编码 return Base64Utils.encode(encryption); } /** * 解密方法 * @param privateKey 私钥 * @param enc 待解密密文 * @return 解密后的明文 */ public static byte[] decrypt(String privateKey, byte[] enc) throws Exception { Key key = getPrivateKey(privateKey); Cipher cipher = Cipher.getInstance(Config.RSA_ALGORITHM); // 初始化 cipher.init(Cipher.DECRYPT_MODE, key, new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT)); // 先进行base64解密,然后解码 return cipher.doFinal(Base64Utils.decode(enc)); } /** * 签名 * @param privateKey 私钥 * @param content 要进行签名的内容 * @return 签名 */ public static String sign(String privateKey, byte[] content) { try { // privateKey进行base64编码,然后生成PKCS8格式私钥 PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(Base64Utils.decode(privateKey.getBytes())); KeyFactory key = KeyFactory.getInstance("RSA"); PrivateKey priKey = key.generatePrivate(priPKCS8); // 签名摘要算法 Signature signature = Signature.getInstance("SHA256WithRSA"); // 用私钥初始化此对象以进行签名 signature.initSign(priKey); // 使用指定的字节数组更新签名或验证 signature.update(content); // 获得签名字节 byte[] signed = signature.sign(); // 进行base64编码返回 return new String(Base64Utils.encode(signed)); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 验签 * @param publicKey 公钥 * @param content 要验签的内容 * @param sign 签名 * @return 验签结果 */ public static boolean checkSign(String publicKey, byte[] content, String sign) { try { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); // 进行base64解码 byte[] encodedKey = Base64Utils.decodeFromString(publicKey); // 生成公钥 PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey)); // 签名摘要算法 Signature signature = Signature.getInstance("SHA256WithRSA"); // 用公钥初始化签名 signature.initVerify(pubKey); // 使用指定的字节数组更新签名或验证 signature.update(content); // base64解码后进行验证 return signature.verify(Base64Utils.decodeFromString(sign)); } catch (Exception e) { e.printStackTrace(); } return false; } public static void main(String[] args) throws Exception { //客户端代码 String text = "hello"; //使用服务端公钥加密 byte[] encryptText = RSACipher.encrypt(Config.SERVER_PUBLIC_KEY, text.getBytes()); System.out.println("加密后:\n" + new String(encryptText)); //使用客户端私钥签名 String signature = RSACipher.sign(Config.CLIENT_PRIVATE_KEY, encryptText); System.out.println("签名:\n" + signature); //服务端代码 //使用客户端公钥验签 boolean result = RSACipher.checkSign(Config.CLIENT_PUBLIC_KEY, encryptText, signature); System.out.println("验签:\n" + result); //使用服务端私钥解密 byte[] decryptText = RSACipher.decrypt(Config.SERVER_PRIVATE_KEY, encryptText); System.out.println("解密后:\n" + new String(decryptText)); } }
输出结果
加密后: ODdEkwo1RgRW8UMoHXPKe9Gwcp6lTCkg4P/Ra3gfkrO+Fw6pSgo0H54nMC5sYSsoUVy1wy2/QXeLSwR6Obfl7SU7DeW+XdGee83O2kgdsDQPbYFwlPYTd0cdOmWwZxtgEOIB9d5G75Iut4kci15vrhXZVtku92U+7aNwtYimSDQ= 签名: RL1qIScizRyu79/y+r2TN2FL/bSQDxnDj4JlDwSZM6XZR7CL7u5ZjLNHbsSYpHaCv9qKMS4ump50LyF+go05dsPjWZOvFNkgcm9LepkDP1qm8AzKdTGwlzhdBmy2397Ed8uBrQocFGj/721Y2xM/Db0nt7r54zKZkDXbMMlsd9k= 验签: true 解密后: hello
import org.springframework.util.Base64Utils; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom; public class AESCipher { public static SecureRandom random = new SecureRandom(); /** * 获取随机16位key,key必须要是10的整数倍,否则会出错 */ public static String getRandom(int length) { StringBuilder ret = new StringBuilder(); for (int i = 0; i < length; i++) { // 输出字母还是数字 boolean isChar = (random.nextInt(2) % 2 == 0); // 字符串 if (isChar) { // 取得大写字母还是小写字母 int choice = random.nextInt(2) % 2 == 0 ? 65 : 97; ret.append((char) (choice + random.nextInt(26))); } else { // 数字 ret.append(random.nextInt(10)); } } return ret.toString(); } /** * 加密方法,使用key充当向量iv,增加加密算法的强度 * 更加安全 * @param key 密钥 * @param raw 需要加密的内容 * @return */ public static String encrypt(byte[] key, String raw) throws Exception { // 第一次加密 SecretKeySpec secretKey = new SecretKeySpec(key, "AES"); byte[] enCodeFormat = secretKey.getEncoded(); // 获取二次加密的key SecretKeySpec secondSecretKey = new SecretKeySpec(enCodeFormat, "AES"); Cipher cipher = Cipher.getInstance(Config.AES_ALGORITHM); // 向量iv,增加加密算法的强度 IvParameterSpec iv = new IvParameterSpec(key); // 初始化加密器 cipher.init(Cipher.ENCRYPT_MODE, secondSecretKey, iv); // 加密 byte[] result = cipher.doFinal(raw.getBytes()); // 进行base64编码 return Base64Utils.encodeToString(result); } /** * 解密方法,使用key充当向量iv,增加加密算法的强度 * @param key 密钥 * @param enc 待解密内容 * @return */ public static String decrypt(byte[] key, String enc) throws Exception { SecretKeySpec secretKey = new SecretKeySpec(key, "AES"); byte[] enCodeFormat = secretKey.getEncoded(); // 二次加密 SecretKeySpec secondSecretKey = new SecretKeySpec(enCodeFormat, "AES"); Cipher cipher = Cipher.getInstance(Config.AES_ALGORITHM); IvParameterSpec iv = new IvParameterSpec(key); // 初始化 cipher.init(Cipher.DECRYPT_MODE, secondSecretKey, iv); // 首先进行base64解码 byte[] bytes = Base64Utils.decodeFromString(enc); // 解密 byte[] result = cipher.doFinal(bytes); return new String(result); } public static void main(String[] args) throws Exception { //客户端代码 String text = "hello"; //随机生成16位aes密钥,也可以自己指定16位 byte[] aesKey = getRandom(16).getBytes(); String encryptText = AESCipher.encrypt(aesKey, text); System.out.println("加密后:\n" + encryptText); String decryptText = AESCipher.decrypt(aesKey, encryptText); System.out.println("解密后:\n" + decryptText); } }
输出结果
加密后: hwkYAF9eXj/dytmDBD30xg== 解密后: hello
加密解密本身并不是难事,问题是在何时去处理?定义一个过滤器,将请求和响应分别拦截下来进行处理也是一个办法,这种方式虽然粗暴,但是灵活,因为可以拿到一手的请求参数和响应数据。不过 SpringMVC 中给我们提供了 ResponseBodyAdvice和 RequestBodyAdvice,利用这两个工具可以对请求和响应进行预处理,非常方便。
参考:
RSA+AES混合加密-JavaWebSpringBoot自定义starter
因为我们这个工具是为 Web 项目开发的,以后必然使用在 Web 环境中,所以这里添加依赖时 scope 设置为 provided
org.springframework.boot spring-boot-starter-web provided 2.7.0
scope几个属性介绍
public class RespBean { private Integer status; private String msg; private Object obj; public static RespBean build() { return new RespBean(); } public static RespBean ok(String msg) { return new RespBean(200, msg, null); } public static RespBean ok(String msg, Object obj) { return new RespBean(200, msg, obj); } public static RespBean error(String msg) { return new RespBean(500, msg, null); } public static RespBean error(String msg, Object obj) { return new RespBean(500, msg, obj); } private RespBean() { } private RespBean(Integer status, String msg, Object obj) { this.status = status; this.msg = msg; this.obj = obj; } public Integer getStatus() { return status; } public RespBean setStatus(Integer status) { this.status = status; return this; } public String getMsg() { return msg; } public RespBean setMsg(String msg) { this.msg = msg; return this; } public Object getObj() { return obj; } public RespBean setObj(Object obj) { this.obj = obj; return this; } }
加密这块有多种方案可以选择,对称加密、非对称加密,其中对称加密又可以使用 AES、DES、3DES 等不同算法,这里我们使用 Java 自带的 Cipher 来实现对称加密,使用 AES 算法
public class AESUtils { private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding"; // 获取 cipher private static Cipher getCipher(byte[] key, int model) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance(AES_ALGORITHM); cipher.init(model, secretKeySpec); return cipher; } // AES加密 public static String encrypt(byte[] data, byte[] key) throws Exception { Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE); return Base64.getEncoder().encodeToString(cipher.doFinal(data)); } // AES解密 public static byte[] decrypt(byte[] data, byte[] key) throws Exception { Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE); return cipher.doFinal(Base64.getDecoder().decode(data)); } }
接下来我们定义两个注解 @Decrypt 和 @Encrypt。在以后使用的过程中,哪个接口方法添加了 @Encrypt 注解就对哪个接口的数据加密返回,哪个接口/参数添加了 @Decrypt 注解就对哪个接口/参数进行解密。另外就是 @Decrypt 可以用在参数上
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.PARAMETER}) public @interface Decrypt { } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Encrypt { }
定义一个 EncryptProperties 类来读取用户配置的 key,这样就可以自定义key。这里设置了默认值,以后如果用户想自己配置 key,只需要在 application.properties 中配置 spring.encrypt.key=xxx 即可。
@ConfigurationProperties(prefix = "spring.encrypt") @Component public class EncryptProperties { // 这一块一定要16位或者整数倍,最多256 private final static String DEFAULT_KEY = "www.shawn222.com"; private String key = DEFAULT_KEY; public String getKey() { return key; } public void setKey(String key) { this.key = key; } }
ResponseBodyAdvice 在你使用了 @ResponseBody 注解的时候才会生效,RequestBodyAdvice 在你使用了 @RequestBody 注解的时候才会生效,换言之,前后端都是 JSON 交互的时候,这两个才有用
我们自定义 EncryptResponse 类实现 ResponseBodyAdvice接口,泛型表示接口的返回类型,这里一共要实现两个方法
另外需要注意,自定义的 ResponseBodyAdvice 需要用 @ControllerAdvice 注解来标记。
@EnableConfigurationProperties(EncryptProperties.class) @ControllerAdvice public class EncryptResponse implements ResponseBodyAdvice{ private ObjectMapper om = new ObjectMapper(); @Autowired EncryptProperties encryptProperties; @Override public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType) { return returnType.hasMethodAnnotation(Encrypt.class); } @Override public RespBean beforeBodyWrite(RespBean body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { byte[] keyBytes = encryptProperties.getKey().getBytes(); try { if (body.getMsg()!=null) { body.setMsg(AESUtils.encrypt(body.getMsg().getBytes(),keyBytes)); } if (body.getObj() != null) { body.setObj(AESUtils.encrypt(om.writeValueAsBytes(body.getObj()), keyBytes)); } } catch (Exception e) { e.printStackTrace(); } return body; } }
首先大家注意,DecryptRequest 类我们没有直接实现 RequestBodyAdvice 接口,而是继承自 RequestBodyAdviceAdapter 类,该类是 RequestBodyAdvice 接口的子类,并且实现了接口中的一些方法,这样当我们继承自 RequestBodyAdviceAdapter 时,就只需要根据自己实际需求实现某几个方法即可。
@EnableConfigurationProperties(EncryptProperties.class) @ControllerAdvice public class DecryptRequest extends RequestBodyAdviceAdapter { @Autowired EncryptProperties encryptProperties; @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class extends HttpMessageConverter>> converterType) { return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class); } @Override public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class extends HttpMessageConverter>> converterType) throws IOException { byte[] body = new byte[inputMessage.getBody().available()]; inputMessage.getBody().read(body); try { byte[] decrypt = AESUtils.decrypt(body, encryptProperties.getKey().getBytes()); final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt); return new HttpInputMessage() { @Override public InputStream getBody() throws IOException { return bais; } @Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); } }; } catch (Exception e) { e.printStackTrace(); } return super.beforeBodyRead(inputMessage, parameter, targetType, converterType); } }
// 换成自己的包路径 @Configuration @ComponentScan("com.example.encryption") public class EncryptAutoConfiguration { }
最后,resources 目录下定义 META-INF,然后再定义 spring.factories 文件,这样当项目启动时,就会自动加载该配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.encryption.EncryptAutoConfiguration
安装到本地仓库比较简单,直接 mvn install,或者在 IDEA 中,点击右边的 Maven,然后双击 install
发不到线上我们可以使用 JitPack来做。首先我们在 GitHub 上创建一个仓库,将我们的代码上传上去,上传成功后,点击右边的 Create a new release 按钮,发布一个正式版
发布成功后,打开 jitpack,输入仓库的完整路径,点击 lookup 按钮,查找到之后,再点击 Get it 按钮完成构建,构建成功后,JitPack 上会给出项目引用方式,新建项目时引入即可
创建实体类
public class User { private Long id; private String username; //省略 getter/setter }
创建测试类,第一个接口使用了 @Encrypt 注解,所以会对该接口的数据进行加密(如果不使用该注解就不加密),第二个接口使用了 @Decrypt 所以会对上传的参数进行解密,注意 @Decrypt 注解既可以放在方法上也可以放在参数上。
@RestController public class HelloController { @GetMapping("/user") @Encrypt public RespBean getUser() { User user = new User(); user.setId((long) 99); user.setUsername("javaboy"); return RespBean.ok("ok", user); } @PostMapping("/user") public RespBean addUser(@RequestBody @Decrypt User user) { System.out.println("user = " + user); return RespBean.ok("ok", user); } }
参考文章
如何优雅的实现 SpringBoot 接口参数加密解密?
为什么使用 Java Cipher 要指定转换模式?
Hutool加密解密
【网络】java密码安全