API 鉴权

By | 2021年12月31日

1 RSA 的可靠性

RSA加密算法是一种非对称加密算法。在公开密钥加密和电子商业中RSA被广泛使用。

对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA算法愈可靠。假如有人找到一种快速因数分解的算法的话,那么用RSA加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA钥匙才可能被强力方式解破。到目前为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被解破的。

javax.crypto.Cipher 类提供加密和解密功能,该类是JCE框架的核心。

2 提升网络安全的方式

前后端分离的好处就是后端只需要实现一套界面,所有前端即可通用。现在的前端分支很多,如 Web 前端、Android 端、iOS 端,甚至还有物联网等。

前后端的传输通过 HTTP 进行传输,也带来了一些安全问题,如果抓包、模拟请求、洪水攻击、参数劫持、网络爬虫等等。如何对非法请求进行有效拦截,保护合法请求的权益是这篇文章需要讨论的。

依据多年互联网后端开发经验,总结出以下提升网络安全的方式:

  • 采用 HTTPS 协议;
  • 密钥存储到服务端而非客户端,客户端应从服务端动态获取密钥;
  • 请求隐私接口,利用 Token 机制校验其合法性;
  • 对请求参数进行合法性校验;
  • 对请求参数进行签名认证,防止参数被篡改;
  • 对输入输出参数进行加密,客户端加密输入参数,服务端加密输出参数。

3 动态密钥的获取

3.1 实现思路

对于可逆加密算法,是需要通过密钥进行加解密,如果直接放到客户端,那么很容易反编译后拿到密钥,这是相当不安全的做法,因此考虑将密钥存储到服务端而非客户端,客户端应从服务端动态获取密钥,具体做法如下:

  1. 客户端先通过 RSA 算法生成一套客户端的公私钥对(clientPublicKey 和 clientPrivateKey);
  2. 调用 getRSA 接口,服务端会返回 serverPublicKey;
  3. 客户端拿到 serverPublicKey 后,用它作为公钥,对 clientPublicKey 进行 RSA 加密,然后调用 getKey 接口,将加密后的 clientPublicKey 传给服务端,服务端接收到请求后用自己的私钥 serverPrivateKey 解密它,然后用解密后的 clientPublicKey  加密秘钥 key 并传给客户端;
  4. 客户端拿到后以 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&timestamp=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

发表评论

您的电子邮箱地址不会被公开。