1 RSA 的可靠性
RSA加密算法是一种非对称加密算法。在公开密钥加密和电子商业中RSA被广泛使用。
对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA算法愈可靠。假如有人找到一种快速因数分解的算法的话,那么用RSA加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA钥匙才可能被强力方式解破。到目前为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被解破的。
javax.crypto.Cipher 类提供加密和解密功能,该类是JCE框架的核心。
2 提升网络安全的方式
前后端分离的好处就是后端只需要实现一套界面,所有前端即可通用。现在的前端分支很多,如 Web 前端、Android 端、iOS 端,甚至还有物联网等。
前后端的传输通过 HTTP 进行传输,也带来了一些安全问题,如果抓包、模拟请求、洪水攻击、参数劫持、网络爬虫等等。如何对非法请求进行有效拦截,保护合法请求的权益是这篇文章需要讨论的。
依据多年互联网后端开发经验,总结出以下提升网络安全的方式:
- 采用 HTTPS 协议;
- 密钥存储到服务端而非客户端,客户端应从服务端动态获取密钥;
- 请求隐私接口,利用 Token 机制校验其合法性;
- 对请求参数进行合法性校验;
- 对请求参数进行签名认证,防止参数被篡改;
- 对输入输出参数进行加密,客户端加密输入参数,服务端加密输出参数。
3 动态密钥的获取
3.1 实现思路
对于可逆加密算法,是需要通过密钥进行加解密,如果直接放到客户端,那么很容易反编译后拿到密钥,这是相当不安全的做法,因此考虑将密钥存储到服务端而非客户端,客户端应从服务端动态获取密钥,具体做法如下:
- 客户端先通过 RSA 算法生成一套客户端的公私钥对(clientPublicKey 和 clientPrivateKey);
- 调用 getRSA 接口,服务端会返回 serverPublicKey;
- 客户端拿到 serverPublicKey 后,用它作为公钥,对 clientPublicKey 进行 RSA 加密,然后调用 getKey 接口,将加密后的 clientPublicKey 传给服务端,服务端接收到请求后用自己的私钥 serverPrivateKey 解密它,然后用解密后的 clientPublicKey 加密秘钥 key 并传给客户端;
- 客户端拿到后以 clientPrivateKey 为私钥对其解密,得到最终的密钥,此流程结束。
注意: 上述提到数据均不能保存到文件里,必须保存到内存中,因为只有保存到内存中,黑客才拿不到这些核心数据,所以每次使用获取的密钥前先判断内存中的密钥是否存在,不存在,则需要获取。
下面我们可以看到客户端将这个 key用于输入参数签名认证。
3.2 实现代码
key.yml
api: encrypt: key: d7b86f6a234abcda rsa: publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCOsOa1cauULBOV/aasb1NqaMMFHoSyqYNFsd3mcgV/Q9a8MbqXCkyQhpxt704WKFape 1PPtcPb0NkKwCn6gCk3WO1AjnlSnmvGh82Ogav+9Rp4SNfUOAJtE/DQMnNwY2/2MUUjZFvsk9OlP6v1ZR1Kg+5UBihEeA6gUnBlmbd/NwIDAQAB privateKey: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI6w5rVxq5QsE5X9pqxvU2powwUehLKpg0Wx3eZyBX9D1rwxupcKTJCG nG3vThYoVql7U8+1w9vQ2QrAKfqAKTdY7UCOeVKea8aHzY6Bq/71GnhI19Q4Am0T8NAyc3Bjb/YxRSNkW+yT06U/q/VlHUqD7lQGKER4DqBScGWZt383Ag MBAAECgYAEAiIo/K12NxrBvuNcuq/cMF8yGJ5fqnVektWJ8LAI2C4DEV6NeaOW98ETMYK/CpkMn8NF9XQwC5jdPXKwb8M4mRD7uAdwbNWoAKOOydi7lWYP L3ht4fwt+0kojfFQSsJsZulp94DChLIcN+UFdjkWCeznTdN4RQeJlVFJqCSm8QJBAL+P+LwkvfUiIgEIJhYDZZ5OQsWEGmtBDtqxARuc59K6Bra54eUagJ WBBCbeHLDXs/NqjRfbGVlrvN8AdWJxLT8CQQC+sHcZOfVELveAS/QdTFiFbkQKQ4vapvPILZ1uvTFeImA0n/3tkGbsk00/AsbfBkgizvlOa3gWLkZ6aG7V wRgJAkEAuZ05OKSpYzMVm8ZXkRDtj/zo+hXMu4woZoMIPcdFYYxbIQbv+Vw6p6KBcV/akQgRF5Vw7WKhJ2IbekEpfJ+JZQJARnf6G2VywRbGOjBXbzhWgk DEfjKGDXCjKHfK9TCPfOUhPnFdqpwxnP22jzGcgrDUmaB5O0S15SSGwRe01eN82QJAOuwlT5xWL7i9BS8YNAcKkuFagMe44trmS+wSI2UEakr6dXsyo1yn muH540nmnOWyLlQEh3pkEPbayZErc3u3ZA==
注意:key.yml 是微服务配置文件,放在服务器端。
EncryptController.java
/** 客户端启动,发送请求到服务端,服务端用RSA算法生成一对公钥和私钥,我们简称为pubkey1,prikey1,将公钥pubkey1返回给客户端。 客户端拿到服务端返回的公钥pubkey1后,自己用RSA算法生成一对公钥和私钥,我们简称为pubkey2,prikey2, 并将公钥pubkey2通过公钥pubkey1加密,加密之后传输给服务端。 此时服务端收到客户端传输的密文,用私钥prikey1进行解密,因为数据是用公钥pubkey1加密的, 通过解密就可以得到客户端生成的公钥pubkey2 然后自己在生成对称加密,也就是我们的AES,其实也就是相对于我们配置中的那个16的长度的加密key, 生成了这个key之后我们就用公钥pubkey2进行加密,返回给客户端,因为只有客户端有pubkey2对应的私钥prikey2, 只有客户端才能解密,客户端得到数据之后,用prikey2进行解密操作, 得到AES的加密key,最后就用加密key进行数据传输的加密,至此整个流程结束。 */ @RestController @RequestMapping("/open/encrypt") public class EncryptController { @Autowired private EncryptOpenService encryptOpenService; @RequestMapping(value = "getRSA", method = RequestMethod.POST) @DisabledEncrypt public SingleResult<RSAResponse> getRSA() { return encryptOpenService.getRSA(); } @RequestMapping(value = "getKey", method = RequestMethod.POST) @DisabledEncrypt public SingleResult<KeyResponse> getKey(@Valid @RequestBody KeyRequest request) throws Exception { return encryptOpenService.getKey(request); } }
EncryptOpenService.java
@Service public class EncryptOpenService{ @Value("${rsa.publicKey}") private String publicKey; @Value("${rsa.privateKey}") private String privateKey; @Value("${api.encrypt.key}") private String key; public SingleResult<RSAResponse> getRSA() { RSAResponse response = RSAResponse.options() .setServerPublicKey(publicKey) .build(); return SingleResult.buildSuccess(response); } public SingleResult<KeyResponse> getKey(KeyRequest request)throws Exception { // 用服务器端 RSA 私钥解密 clientPublicKey String clientPublicKey = RsaUtils.privateDecrypt(request.getClientEncryptPublicKey(), RsaUtils.getPrivateKey(privateKey)); // 用解密的客户端公钥 clientPublicKey 加密 encrypt key String encryptKey = RsaUtils.publicEncrypt(key, RsaUtils.getPublicKey(clientPublicKey)); KeyResponse response = KeyResponse.options() .setKey(encryptKey) .build(); return SingleResult.buildSuccess(response); } }
KeyRequest.java
public class KeyRequest { /** * 客户端自己生成的加密后公钥 */ @NotNull private String clientEncryptPublicKey; public String getClientEncryptPublicKey() { return clientEncryptPublicKey; } public void setClientEncryptPublicKey(String clientEncryptPublicKey) { this.clientEncryptPublicKey = clientEncryptPublicKey; } }
KeyResponse.class
public class KeyResponse { /** * 整个系统所有加密算法共用的密钥 */ private String key; public static class Builder{ private String key; public Builder setKey(String key){ this.key = key; return this; } public KeyResponse build(){ return new KeyResponse(this); } } public static Builder options(){ return new Builder(); } private KeyResponse(Builder builder){ this.key = builder.key; } public String getKey() { return key; } }
RSAResponse .class
public class RSAResponse extends BaseResponse{ private String serverPublicKey; private String serverPrivateKey; public static class Builder{ private String serverPublicKey; private String serverPrivateKey; public Builder setServerPublicKey(String serverPublicKey){ this.serverPublicKey = serverPublicKey; return this; } public Builder setServerPrivateKey(String serverPrivateKey){ this.serverPrivateKey = serverPrivateKey; return this; } public RSAResponse build(){ return new RSAResponse(this); } } public static Builder options(){ return new Builder(); } public RSAResponse(Builder builder){ this.serverPrivateKey = builder.serverPrivateKey; this.serverPublicKey = builder.serverPublicKey; } public String getServerPrivateKey() { return serverPrivateKey; } public String getServerPublicKey() { return serverPublicKey; } }
RsaUtils.class 和 AesUtils.class 参考:
com.wanghua.toolkit.common.util.security.encryption.RsaUtils
com.wanghua.toolkit.common.util.security.encryption.AesUtils
4 接口请求的合法性校验
4.1 实现思路
对于一些隐私接口(即必须要登录才能调用的接口),我们需要校验其合法性,即只有登录用户才能成功调用,具体思路如下:
调用登录或注册接口成功后,服务端会返回 Token(设置较短有效时间)和 refreshToken(设定较长有效时间);
隐私接口每次请求接口在请求头带上 Token,如 header(“token”,token),若服务端 返回403错误,则调用 refreshToken 接口获取新的 Token 重新调用接口,若 refreshToken 接口继续返回403,则跳转到登录界面。
注意:很早以前我们一般使用session,sessionId存储在cookie或URL中,现在做平台,一般都用token,实现无状态登录,以便接口可以跨平台用。
4.2 实现代码
用户认证具体实现时,一般可以使用 shiro 框架,具体不再这里讲了。
5 输入参数的合法性校验
请参考 @Valid + BindingResult 。
6 输入参数签名认证
6.1 实现思路
我们请求的接口是通过 HTTP/HTTPS 传输的,一旦参数被拦截,很有可能被黑客篡改,并传回给服务端,为了防止这种情况发生,我们需要对参数进行签名认证,保证传回的参数是合法性,具体思路如下。
请求接口前,将 Token、Timstamp 和接口需要的参数按照 ASCII 升序排列,拼接成 url=key1=value1&key2=value2,如 name=xxx×tamp=xxx&token=xxx,进行 MD5(url+salt),得到 Signature,将 Token、Signature、Timestamp 放到请求头传给服务端,如 header(“token”,token)、header(“timestamp”,timestamp)、header(“signature”,signature)。
注意: salt 即为动态获取的密钥 key。
6.2 实现代码
参考:
com.wanghua.toolkit.common.util.security.UrlParameterSignatureUtil
com.wanghua.toolkit.common.util.security.SignatureUtilTest
注意:上面的参考只是测试代码,实际项目中验证逻辑应该在拦截器 HandlerInterceptor.preHandle 里统一处理。
7 输入输出参数加密
为了保护数据,比如说防爬虫,需要对输入输出参数进行加密,客户端加密输入参数传回服务端,服务端解密输入参数执行请求;服务端返回数据时对其加密,客户端拿到数据后解密数据,获取最终的数据。这样,即便别人知道了参数地址,也无法模拟请求数据。
由于客户端、服务端都已各自拥有RSA公钥私钥对,且各自都知道对方的公钥,因此实现输入输出参数加密并不难了,这里就不写出来了。
参考:
com.wanghua.toolkit.common.controller.EncryptControllerTest