背景
随着Internet网的广泛应用,信息安全问题日益突出,零碎间的接口交互,每个申请都有可能被抓取到数据、被伪造申请去获取数据或者攻打服务,所以接口平安至关重要。
API接口要做到:
- 防假装攻打:第三方歹意调用接口。
- 防篡改攻打:申请在传输过程被批改。
- 防重放攻打:申请被截获后,被多次重放。
- 防数据信息透露:被截获到零碎数据,例如账号、明码、交易信息等。
计划
工夫戳 + 流水号
为了避免重放攻打,申请信息里退出:
- 退出工夫戳,1分钟内无效。
- 退出流水号,流水号在工夫戳无效工夫范畴内需保障惟一。
签名
为了防篡改攻打,申请信息里减少签名信息(sign),将申请的内容拼装
和加密
,服务端做同样的事件,而后将加密后的签名跟传过来的签名做等值比拟。
常见的一种签名形式是:将申请参数&值
按天然排序拼接后加密。例如:/test?a=5&c=1&b=3&z=value
,那拼接后sign
的值就是a=5&b=3&c=1&nonce=7536080117&time=1618851899342&z=value
加密后的后果,留神这里也要将工夫戳和流水号一起拼进去。最终的申请串是:/test?a=5&c=1&b=3&z=value&nonce=7536080117&time=1618851899342&sign=加密串
。nonce
、time
、sign
也能够放在申请头里。
这里有用到加密,就波及的秘钥的问题,能够线下调配APP ID和secret
,申请串里加上appId=xxx
,同时参加到sign
的加密内容里。
咱们的零碎传的数据是json串
的形式,并且是放在HTTP的body
里,所以签名就不是拼接申请参数
的加密形式,而是appSecret+time+nonce
拼装后的内容加密(也能够将json串一并拼接进去),同时appId
、time
、nonce
、sign
是放在申请头里。
加密
为了避免数据信息透露,咱们须要将业务数据
加密传输,同时签名里也有用到加密,这里就波及到了加密算法。
- 对称加密:较传统的加密体制,通信单方在加/解密过程中应用他们共享的繁多密钥,鉴于其算法简略和加密速度快的长处,目前依然是支流的明码体制之一,然而相比非对称加密的安全性就没那么高。
- 非对称加密:它有一对秘钥(
公钥
和私钥
),通过公钥加密的内容,只有私钥才能够解开,而通过私钥加密的内容,只有公钥才能够解开;算法的密钥很长,具备较好的安全性,但加密的计算量很大,加密速度较慢限度了其利用范畴。
为什么非对称加密比对称加密慢?
因为对称加密次要的运算是位运算,速度十分快,如果应用硬件计算,速度会更快。然而非对称加密计算个别都比较复杂,比方 RSA,它外面波及到大数乘法、大数模等等运算,在电路上实现“加法”比异或
要麻烦的多,况且前面还有一个模运算。
AES和RSA联合
因为RSA
加解密速度慢,不适宜大量数据文件加密。如果应用AES
对传输数据加密,应用RSA来加密AES的密钥
,就能够综合施展AES和RSA的长处同时防止它们毛病来实现一种新的数据加密计划。
最终计划
- 线下调配APP ID、Secret和RSA公钥。
- 随机生成16位的
AES秘钥
。 - 申请头退出工夫戳,1分钟内无效。
- 申请头退出流水号,流水号在工夫戳无效工夫范畴内需保障惟一。
- 申请头退出APP ID。
- 申请头退出
加密后的AES秘钥
,通过RAS公钥加密。 - 申请头退出签名,签名算法为:AES加密(APPID + secret + 加密后的AES秘钥 + 工夫戳 + 流水号)。这里之所以额定加一个secret,是假如被第三方拿到公钥,还有这一层爱护。
- 申请body里的
业务数据
传的是AES加密后的内容。
示例代码
加密工具类
<code class="JAVA">import java.io.UnsupportedEncodingException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; /** * @author * @data * @description */ public class SecurityUtils { private static final String DEFAULT_CODING = "UTF-8"; private static final String AES_ALGORITHM = "AES"; private static final String RSA_ALGORITHM = "RSA"; private static final int RSA_KEYSIZE = 1024; /** * 填充形式 */ private static final String AES_CIPHER_PADDING = "AES/ECB/PKCS5Padding"; private static final int PASSWORD_LENGTH = 16; private SecurityUtils() { } /** * AES加密 * * @param content * @param password * @return java.lang.String * @author * @date */ public static String aes128Encrypt(String content, String password) { try { checkPassword(password); Cipher cipher = Cipher.getInstance(AES_CIPHER_PADDING); cipher.init(Cipher.ENCRYPT_MODE, buildAes128SecretKey(password)); byte[] result = cipher.doFinal(getContentByte(content)); return encodeContentByte(result); } catch (Exception e) { throw new RuntimeException("aes 128 encrypt error!", e); } } /** * @param content * @param password * @return java.lang.String * @author * @date */ public static String aes128Decrypt(String content, String password) { try { checkPassword(password); Cipher cipher = Cipher.getInstance(AES_CIPHER_PADDING); cipher.init(Cipher.DECRYPT_MODE, buildAes128SecretKey(password)); byte[] result = cipher.doFinal(decodeContentByte(content)); return new String(result, DEFAULT_CODING); } catch (Exception e) { throw new RuntimeException("aes 128 decrypt error!", e); } } /** * @param password * @author * @date */ private static void checkPassword(String password) { if (StringUtils.isBlank(password) || StringUtils.length(password) != PASSWORD_LENGTH) { throw new IllegalArgumentException("Password not available!"); } } /** * 生成加密秘钥 * * @param password * @return javax.crypto.spec.SecretKeySpec * @author * @date */ private static SecretKeySpec buildAes128SecretKey(String password) { try { //返回生成指定算法密钥生成器的 KeyGenerator 对象 KeyGenerator kgen = KeyGenerator.getInstance(AES_ALGORITHM); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed(password.getBytes(DEFAULT_CODING)); kgen.init(128, random); SecretKey secretKey = kgen.generateKey(); return new SecretKeySpec(secretKey.getEncoded(), AES_ALGORITHM); } catch (Exception e) { throw new RuntimeException("build aes 128 secret key error!", e); } } /** * @return com.example.laboratory.security.util.SecurityUtils.RsaKey * @author * @date */ public static RsaKey generateRsaKey() { KeyPairGenerator keyPairGen; try { keyPairGen = KeyPairGenerator.getInstance(RSA_ALGORITHM); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("generate rsa key error!", e); } keyPairGen.initialize(RSA_KEYSIZE); KeyPair keyPair = keyPairGen.generateKeyPair(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); return new RsaKey(encodeContentByte(publicKey.getEncoded()), encodeContentByte(privateKey.getEncoded())); } /** * @param publicKey * @return java.security.PublicKey * @author * @date */ private static PublicKey buildRsaPublicKey(String publicKey) { try { //通过X509编码的Key指令取得公钥对象 KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(decodeContentByte(publicKey)); return keyFactory.generatePublic(x509KeySpec); } catch (Exception e) { throw new RuntimeException("build rsa public key error!", e); } } /** * @param privateKey * @return java.security.PrivateKey * @author * @date */ private static PrivateKey buildRsaPrivateKey(String privateKey) { try { //通过PKCS#8编码的Key指令取得私钥对象 KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(decodeContentByte(privateKey)); return keyFactory.generatePrivate(pkcs8KeySpec); } catch (Exception e) { throw new RuntimeException("build rsa private key error!", e); } } /** * RSA默认对加密内容有最大限度,超出限度会呈现"IllegalBlockSizeException: Data must not be longer than 117 bytes" * 如果要对长内容做加密,就用RSA分段加密的形式 * * @param content * @param publicKey * @return java.lang.String * @author * @date */ public static String rsaPublicEncrypt(String content, String publicKey) { try { Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, buildRsaPublicKey(publicKey)); byte[] encryptedData = cipher.doFinal(getContentByte(content)); return encodeContentByte(encryptedData); } catch (Exception e) { throw new RuntimeException("rsa public encrypt error!", e); } } /** * @param content * @param privateKey * @return java.lang.String * @author * @date */ public static String rsaPrivateDecrypt(String content, String privateKey) { try { Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, buildRsaPrivateKey(privateKey)); byte[] decryptData = cipher.doFinal(decodeContentByte(content)); return new String(decryptData, DEFAULT_CODING); } catch (Exception e) { throw new RuntimeException("rsa public encrypt error!", e); } } /** * @param content * @return byte[] * @author * @date */ private static byte[] getContentByte(String content) throws UnsupportedEncodingException { return content.getBytes(DEFAULT_CODING); } /** * @param bytes * @return java.lang.String * @author * @date */ private static String encodeContentByte(byte[] bytes) { return Base64.encodeBase64String(bytes); } /** * @param content * @return byte[] * @author * @date */ private static byte[] decodeContentByte(String content) { <span style="color:transparent">来源gaodai#ma#com搞*代#码网</span> return Base64.decodeBase64(content); } /** * @author * @date */ @Data @AllArgsConstructor @NoArgsConstructor static class RsaKey { private String publicKey; private String privateKey; } public static void main(String[] args) throws Exception { String content = "测试DEMO"; System.out.println("-------AES-------"); String password = "123456789abcdefg"; String encrypt = SecurityUtils.aes128Encrypt(content, password); System.out.println("aes encrypt: " + encrypt); System.out.println(SecurityUtils.aes128Decrypt(encrypt, password)); System.out.println("-------RSA-------"); RsaKey rsaKey = SecurityUtils.generateRsaKey(); System.out.println("ras public key:" + rsaKey.getPublicKey()); System.out.println("ras private key:" + rsaKey.getPrivateKey()); encrypt = SecurityUtils.rsaPublicEncrypt(content, rsaKey.getPublicKey()); System.out.println("rsa encrypt: " + encrypt); System.out.println(SecurityUtils.rsaPrivateDecrypt(encrypt, rsaKey.getPrivateKey())); } }
测试用例:服务端接口
<code class="JAVA">import com.example.laboratory.security.util.SecurityUtils; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.StringJoiner; import java.util.concurrent.TimeUnit; import lombok.AllArgsConstructor; import lombok.Data; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author * @date */ @RestController @RequestMapping("/sign") public class SignController { public static String KEY_PUBLIC = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD0dxq1W8t3rAmZSHRLAddGAWtX5wlDdtLXHoC5MUkXO6EFO/b+z+8jPfqxr4hgnnPseIM7qRx78sEKfI0iu/HzbfBz7uctF5+PU9m7axYfpHDHrl/59bdHBXiIA9/9UMcy+3aDEU6B4lnkhrJWD8OLUOrgWuy/uaQmGB3Dm0m6wwIDAQAB"; public static String KEY_PRIVATE = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAPR3GrVby3esCZlIdEsB10YBa1fnCUN20tcegLkxSRc7oQU79v7P7yM9+rGviGCec+x4gzupHHvywQp8jSK78fNt8HPu5y0Xn49T2btrFh+kcMeuX/n1t0cFeIgD3/1QxzL7doMRToHiWeSGslYPw4tQ6uBa7L+5pCYYHcObSbrDAgMBAAECgYEAhVbZiIYTCqkZazPrymWsp5Bqnj1z/go3ogIPL/PD7BooD5TPedislMpfjL8zYY/LpvVsjwQEd07HIBMjYAinRJCO1K5J6esWZ43nqp96Mf4bulqFW34uvP3vPS+yAbZ/GI+KkmXGgB9zn5cCWLVqlTZSgrF/dZXMlXR5gVHoP5ECQQD8d21IV2J0Y5yTY0TjdylE3rZI2386hw6/SE2kxmdh6GAAgFafYQ0k/TYx3J9JjervtC5zAhDMHBqBKIdXiWudAkEA9+MCZyX0kavooymzZUDnu5XYkwZON73wtdKICjnYj7cOHdhTB4rInf7xJRwvwBk4Ct/5erNb6lm2/SIj7wDh3wJAdqJ4C+JkNWUJkoi3OlwoXGB7L8lVA9+rIl+LfL5unidf1Vx5V/N3BcamzM9rWlkB6Rm2Kfzyf7dFDSRKVOwSUQJAVrgM7CbkE14PiZ0aDE8TgpVeabjn/iotnn4jZ2hrMYO5pYk7KsVLf7JjjDb7IXnxGCTYsysx+Z8fHBkodwFZAwJAE706uG6seIi1zQAgnbioR4oYBOdQEnFMVOHDmv+0iaK/LQVUS99tZV6x+HzN2lZAxwt9ta+szWijvNGwneRa1Q=="; public static String APP_ID = "testApp"; // 假如被拿到公钥,还有这一层爱护 public static String APP_SECRET = "testSecret123456"; /** * 申请容许时间差 */ private static int TIME_RANGE_MINUTES = 2; /** * 签名:惟一标识串前缀 */ private static String SIGN_UQKEY_PREFIX = "SIGN:"; private LoadingCache<String, String> signCache; private Map<String, AppData> appCache; /** * @author * @date */ public SignController() { signCache = CacheBuilder.newBuilder() .refreshAfterWrite(TIME_RANGE_MINUTES * 60 + 1, TimeUnit.SECONDS) .build(new CacheLoader<String, String>() { @Override public String load(String key) { return buildCacheUniqueValue(key); } }); appCache = new HashMap<>(); appCache.put(APP_ID, new AppData(APP_SECRET, KEY_PRIVATE)); } /** * @param requestBody * @param sign * @param appId * @param time * @param nonce * @param encodeSecretKey * @return java.lang.String * @author * @date */ @PostMapping("/encode") public String encode(@RequestBody String requestBody, @RequestHeader("sign") String sign, @RequestHeader("appId") String appId, @RequestHeader("time") Long time, @RequestHeader("nonce") String nonce, @RequestHeader("secretKey") String encodeSecretKey) { checkAccessFrequently(time); String secretKey = decodeSecretKey(appId, encodeSecretKey); checkSign(sign, appId, time, nonce, secretKey, encodeSecretKey); checkUnique(appId, time, nonce); return SecurityUtils.aes128Decrypt(requestBody, secretKey); } /** * @param appId * @param secretKey * @return java.lang.String * @author * @date */ private String decodeSecretKey(String appId, String secretKey) { return SecurityUtils.rsaPrivateDecrypt(secretKey, appCache.get(appId).privateKey); } /** * @param sign * @param appId * @param time * @param nonce * @param secretKey * @param encodeSecretKey * @author * @date */ private void checkSign(String sign, String appId, Long time, String nonce, String secretKey, String encodeSecretKey) { String signStr = new StringJoiner(":") .add(appId) .add(appCache.get(appId).appSecret) .add(encodeSecretKey) .add(String.valueOf(time)) .add(nonce) .toString(); String encodeSign = SecurityUtils.aes128Encrypt(signStr, secretKey); if (!encodeSign.equals(sign)) { throw new RuntimeException("签名谬误!"); } } /** * @param appId * @param time * @param nonce * @author * @date */ private void checkUnique(String appId, Long time, String nonce) { String key = new StringJoiner(":") .add(SIGN_UQKEY_PREFIX) .add(appId) .add(String.valueOf(time)) .add(nonce) .toString(); if (signCache.getIfPresent(key) != null) { throw new RuntimeException("申请反复!"); } try { if (!signCache.get(key).equals(buildCacheUniqueValue(key))) { throw new RuntimeException("申请反复!"); } } catch (Exception e) { throw new RuntimeException("判断申请是否反复异样!", e); } } /** * @param key * @return java.lang.String * @author * @date */ private String buildCacheUniqueValue(String key) { return key + ":" + Thread.currentThread().getName(); } /** * 判断拜访工夫戳是否在以后工夫一分钟高低 * * @param timestamp * @author * @date */ private void checkAccessFrequently(long timestamp) { Date date = new Date(timestamp); LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); LocalDateTime beforeDateTime = LocalDateTime.now().minusMinutes(TIME_RANGE_MINUTES); LocalDateTime afterDateTime = LocalDateTime.now().plusMinutes(TIME_RANGE_MINUTES); if (localDateTime.isBefore(beforeDateTime) || localDateTime.isAfter(afterDateTime)) { throw new RuntimeException("申请工夫戳超出范围!"); } } @Data @AllArgsConstructor static class AppData { private String appSecret; private String privateKey; } }
测试用例:申请端单元测试
<code class="JAVA">import com.example.laboratory.security.util.SecurityUtils; import java.util.StringJoiner; import lombok.SneakyThrows; import org.apache.commons.lang3.RandomStringUtils; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; /** * @author * @data * @description */ public class SignControllerTest extends SimpleResultControllerTest { private String aesSecretKey; /** * @author * @date */ @Before public void before() { aesSecretKey = RandomStringUtils.randomNumeric(16); } /** * @author * @date */ @Test @SneakyThrows public void encode() { String content = "{\"data1\":\"测试API接口平安校验\",\"data2\":" + RandomStringUtils.randomNumeric(10) + "}"; System.out.println(content); String result = post("/sign/encode", SecurityUtils.aes128Encrypt(content, aesSecretKey), String.class); Assert.assertEquals(result, content); } /** * @param builder * @author * @date */ protected void replenishHttpServletRequestBuilder(MockHttpServletRequestBuilder builder) { String appId = SignController.APP_ID; long time = System.currentTimeMillis(); String nonce = RandomStringUtils.randomNumeric(10); String encodeSecret = SecurityUtils.rsaPublicEncrypt(aesSecretKey, SignController.KEY_PUBLIC); String signStr = new StringJoiner(":") .add(SignController.APP_ID) .add(SignController.APP_SECRET) .add(encodeSecret) .add(String.valueOf(time)) .add(nonce) .toString(); String encodeSign = SecurityUtils.aes128Encrypt(signStr, aesSecretKey); builder.header("appId", appId) .header("time", time) .header("nonce", nonce) .header("sign", encodeSign) .header("secretKey", encodeSecret); } }
web端计划
如果是web端接口交互,能够参考以下阿里、京东的做法:
《京东post登陆参数js剖析,明码加密的RSA加密实现》
《淘宝sign加密算法》
参考
《接口非对称加密+Retrofit2》
《Android采纳AES+RSA的加密机制对http申请进行加密》
《支付宝领取加密规定梳理,写的太好了!》
《阿里一面:如何保障API接口数据安全?》