Gateway API接口安全设计(加密 、签名) - 图1

1 防止数据抓包窃取

1.1 风险简述

简述:当用户登录时,恶意攻击者可以用抓包工具可以拿到用户提交的表单信息,可以获取用户的账号密码,进而可以恶意访问网站。
Gateway API接口安全设计(加密 、签名) - 图2

1.2 RSA 非对称加密

1.2.1 RSA简介

RSA加密算法是一种非对称加密算法。在公开密钥加密和电子商业中RSA被广泛使用。RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。
1973年,在英国政府通讯总部工作的数学家克利福德·柯克斯(Clifford Cocks)在一个内部文件中提出了一个相同的算法,但他的发现被列入机密,一直到1997年才被发表。对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA算法愈可靠。
假如有人找到一种快速因数分解的算法的话,那么用RSA加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA钥匙才可能被强力方式解破。到目前为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被解破的。
1983年麻省理工学院在美国为RSA算法申请了专利。这个专利2000年9月21日失效。由于该算法在申请专利前就已经被发表了,在世界上大多数其它地区这个专利权不被承认。

1.2.2 RSA应用过程

非对称算法的在应用的过程如下:

  • 接收方生成公钥和私钥,公钥公开,私钥保留;
  • 发送方将要发送的消息采用公钥加密,得到密文,然后将密文发送给接收方;
  • 接收方收到密文后,用自己的私钥进行解密,获得明文。
    1.2.3 RSA工具类


    ```java import com.demo.excepiton.RsaException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher; import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; 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 java.util.HashMap; import java.util.Map;

@Slf4j public class RSAUtils {

  1. public static final String PUBLIC_KEY = "public_key";
  2. public static final String PRIVATE_KEY = "private_key";
  3. public static Map<String, String> generateRasKey() {
  4. Map<String, String> rs = new HashMap<>();
  5. try {
  6. // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
  7. KeyPairGenerator keyPairGen = null;
  8. keyPairGen = KeyPairGenerator.getInstance("RSA");
  9. keyPairGen.initialize(1024, new SecureRandom());
  10. // 生成一个密钥对,保存在keyPair中
  11. KeyPair keyPair = keyPairGen.generateKeyPair();
  12. // 得到私钥 公钥
  13. RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
  14. RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
  15. String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded()));
  16. // 得到私钥字符串
  17. String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded())));
  18. // 将公钥和私钥保存到Map
  19. rs.put(PUBLIC_KEY, publicKeyString);
  20. rs.put(PRIVATE_KEY, privateKeyString);
  21. } catch (Exception e) {
  22. log.error("RsaUtils invoke genKeyPair failed.", e);
  23. throw new RsaException("RsaUtils invoke genKeyPair failed.");
  24. }
  25. return rs;
  26. }
  27. public static String encrypt(String str, String publicKey) {
  28. try {
  29. //base64编码的公钥
  30. byte[] decoded = Base64.decodeBase64(publicKey);
  31. RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
  32. //RSA加密
  33. Cipher cipher = Cipher.getInstance("RSA");
  34. cipher.init(Cipher.ENCRYPT_MODE, pubKey);
  35. return Base64.encodeBase64String(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8)));
  36. } catch (Exception e) {
  37. log.error("RsaUtils invoke encrypt failed.", e);
  38. throw new RsaException("RsaUtils invoke encrypt failed.");
  39. }
  40. }
  41. public static String decrypt(String str, String privateKey) {
  42. try {
  43. //64位解码加密后的字符串
  44. byte[] inputByte = Base64.decodeBase64(str.getBytes(StandardCharsets.UTF_8));
  45. //base64编码的私钥
  46. byte[] decoded = Base64.decodeBase64(privateKey);
  47. RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
  48. //RSA解密
  49. Cipher cipher = Cipher.getInstance("RSA");
  50. cipher.init(Cipher.DECRYPT_MODE, priKey);
  51. return new String(cipher.doFinal(inputByte));
  52. } catch (Exception e) {
  53. log.error("RsaUtils invoke decrypt failed.", e);
  54. throw new RsaException("RsaUtils invoke decrypt failed.");
  55. }
  56. }

}

  1. <a name="FRFlg"></a>
  2. ##### <br /><br />
  3. RsaException: 是自定义异常。
  4. ```java
  5. @Getter
  6. public class RsaException extends RuntimeException {
  7. private final String message;
  8. public RsaException(String message) {
  9. this.message = message;
  10. }
  11. }

1.2.4 UNIT TEST单元测试


  1. import com.alibaba.fastjson.JSONObject;
  2. import com.demo.utils.RSAUtils;
  3. import org.junit.Test;
  4. import java.util.Map;
  5. /**
  6. * @Description:
  7. * @Author: rosh
  8. * @Date: 2021/10/25 22:30
  9. */
  10. public class RsaTest {
  11. /**
  12. * 用测试生成的公钥,私钥赋值
  13. */
  14. private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFtTlL61IqIGd+fRLUhJ0MjsqFXFJswCohJ45m51WvbxDPRP3gllW0WChk74D5JEOpMDSWo4C7RfoGlBRNW7kQ6qYGukYZ5jgYpzoT0+gp3on96fQXEyQJysv9xiTPIdmSXXVVj1HAOJw29RbzxIVKUSzzPXvEtXRTtCC1+wkAJQIDAQAB";
  15. private static final String PRIVATE_KEY = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAIW1OUvrUiogZ359EtSEnQyOyoVcUmzAKiEnjmbnVa9vEM9E/eCWVbRYKGTvgPkkQ6kwNJajgLtF+gaUFE1buRDqpga6RhnmOBinOhPT6Cneif3p9BcTJAnKy/3GJM8h2ZJddVWPUcA4nDb1FvPEhUpRLPM9e8S1dFO0ILX7CQAlAgMBAAECgYBC4amtbiKFa/wY61tV7pfYRjzLhKi+OUlZmD3E/4Z+4KGZ7DrJ8qkgMtDR3HO5LAikQrare1HTW2d7juqw32ascu+uDObf4yrYNKin+ZDLUYvIDfLhThPxnZJwQ/trdtfxO3VM//XbwZacmwYbAsYW/3QPUXwwOPAgbC2oth8kqQJBANKLyXcdjZx4cwJVl7xNeC847su8y6bPpcBASsaQloCIPiNBIg1h76dpfEGIQBYWJWbBsxtHe/MhOmz7fNFDS2sCQQCiktYZR0dZNH4eNX329LoRuBiltpr9tf36rVOlKr1GSHkLYEHF2qtyXV2mdrY8ZWpvuo3qm1oSLaqmop2rN9avAkBHk85B+IIUF77BpGeZVJzvMOO9z8lMRHuNCE5jgvQnbinxwkrZUdovh+T+QlvHJnBApslFFOBGn51FP5oHamFRAkEAmwZmPsinkrrpoKjlqz6GyCrC5hKRDWoj/IyXfKKaxpCJTH3HeoIghvfdO8Vr1X/n1Q8SESt+4mLFngznSMQAZQJBAJx07bCFYbA2IocfFV5LTEYTIiUeKdue2NP2yWqZ/+tB5H7jNwQTJmX1mn0W/sZm4+nJM7SjfETpNZhH49+rV6U=";
  16. /**
  17. * 生成公钥私钥
  18. */
  19. @Test
  20. public void generateRsaKey() {
  21. Map<String, String> map = RSAUtils.generateRasKey();
  22. System.out.println("随机生成的公钥为:" + map.get(RSAUtils.PUBLIC_KEY));
  23. System.out.println("随机生成的私钥为:" + map.get(RSAUtils.PRIVATE_KEY));
  24. }
  25. /**
  26. * 加密: Yeidauky/iN1/whevov2+ntzXJKAp2AHfESu5ixnDqH5iB7ww+TcfqJpDfkPHfb12Y0sVXw0gBHNJ4inkh7l2/SJBze3pKQU/mg3oyDokTia3JZIs+e80/iJcSfN+yA1JaqY+eJPYiBiOGAF2S6x0ynvJg/Wj0fwp2Tq3PDzRMo=
  27. */
  28. @Test
  29. public void testEncrypt() {
  30. JSONObject jsonObject = new JSONObject();
  31. jsonObject.put("username", "rosh");
  32. jsonObject.put("password", "123456");
  33. String str = jsonObject.toJSONString();
  34. String encrypt = RSAUtils.encrypt(str, PUBLIC_KEY);
  35. System.out.println(encrypt);
  36. }
  37. @Test
  38. public void testDecrypt() {
  39. String decrypt = RSAUtils.decrypt("Yeidauky/iN1/whevov2+ntzXJKAp2AHfESu5ixnDqH5iB7ww+TcfqJpDfkPHfb12Y0sVXw0gBHNJ4inkh7l2/SJBze3pKQU/mg3oyDokTia3JZIs+e80/iJcSfN+yA1JaqY+eJPYiBiOGAF2S6x0ynvJg/Wj0fwp2Tq3PDzRMo=",
  40. PRIVATE_KEY);
  41. System.out.println(decrypt);
  42. }
  43. }



Gateway API接口安全设计(加密 、签名) - 图3

1.3 案例

SpringCloud Gateway + SpringBoot + Nacos+redis
Gateway API接口安全设计(加密 、签名) - 图4
Gateway API接口安全设计(加密 、签名) - 图5

1.3.1 前端登录代码

后端把公钥跟前端约定好:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录页面</title>
  6. </head>
  7. <body>
  8. <h1>登录</h1>
  9. <from id="from">
  10. 账号:<input id="username" type="text"/>
  11. <br/>
  12. 密码:<input id="password" type="password"/>
  13. <br/>
  14. <input id="btn_login" type="button" value="登录"/>
  15. </from>
  16. <script src="js/jquery.min.js"></script>
  17. <script src="js/jsencrypt.js"></script>
  18. <script type="text/javascript">
  19. var encrypt = new JSEncrypt();
  20. encrypt.setPublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFtTlL61IqIGd+fRLUhJ0MjsqFXFJswCohJ45m51WvbxDPRP3gllW0WChk74D5JEOpMDSWo4C7RfoGlBRNW7kQ6qYGukYZ5jgYpzoT0+gp3on96fQXEyQJysv9xiTPIdmSXXVVj1HAOJw29RbzxIVKUSzzPXvEtXRTtCC1+wkAJQIDAQAB");
  21. $("#btn_login").click(function () {
  22. const username = $("#username").val();
  23. const password = $("#password").val();
  24. const form = {};
  25. form.username = username;
  26. form.password = password;
  27. $.ajax({
  28. url: "http://localhost:9000/api/user/login",
  29. data: encrypt.encrypt(JSON.stringify(form)),
  30. type: "POST",
  31. dataType: "json",
  32. contentType: "application/json;charset=utf-8",
  33. success: function (data) {
  34. console.log(data);
  35. }
  36. });
  37. })
  38. </script>
  39. </body>
  40. </html>

1.3.2 前端查询代码

设定公钥、token,token是登录成功后返回的值

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>查询测试</title>
  6. </head>
  7. <body>
  8. id:<input id="id_txt" type="text"/>
  9. <input id="btn_search" type="button" value="查询"/>
  10. <script src="js/jquery.min.js"></script>
  11. <script src="js/jsencrypt.js"></script>
  12. <script type="text/javascript">
  13. var encrypt = new JSEncrypt();
  14. encrypt.setPublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFtTlL61IqIGd+fRLUhJ0MjsqFXFJswCohJ45m51WvbxDPRP3gllW0WChk74D5JEOpMDSWo4C7RfoGlBRNW7kQ6qYGukYZ5jgYpzoT0+gp3on96fQXEyQJysv9xiTPIdmSXXVVj1HAOJw29RbzxIVKUSzzPXvEtXRTtCC1+wkAJQIDAQAB");
  15. $("#btn_search").click(function () {
  16. const id = $("#id_txt").val();
  17. const param = "id=" + id + "&requestId=" + getUuid();
  18. encrypt.encrypt(param);
  19. const url = "http://localhost:9000/api/user/detail?param=" + encrypt.encrypt(param);
  20. $.ajax({
  21. url: url,
  22. beforeSend: function (XMLHttpRequest) {
  23. XMLHttpRequest.setRequestHeader("token", "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIzYzE1ODczYS1iMGUxLTQyNzctYTRjOS1kYTMwNjdiYmE0NWIiLCJpYXQiOjE2MzUzMDYwMDAsInN1YiI6IntcInBhc3N3b3JkXCI6XCIxMjM0NTZcIixcInVzZXJJZFwiOjEsXCJ1c2VybmFtZVwiOlwiYWRtaW5cIn0iLCJleHAiOjE2MzU1NjUyMDB9.fIQi_cV2ZMszBVFV4GoIpGhCSENQKrDi8DsbArk7mGk");
  24. },
  25. type: "GET",
  26. success: function (data) {
  27. console.log(data);
  28. }
  29. });
  30. });
  31. function getUuid() {
  32. var s = [];
  33. var hexDigits = "0123456789abcdef";
  34. for (var i = 0; i < 32; i++) {
  35. s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
  36. }
  37. s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
  38. s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
  39. s[8] = s[13] = s[18] = s[23];
  40. var uuid = s.join("");
  41. return uuid;
  42. }
  43. </script>
  44. </body>
  45. </html>

1.3.3 GatewayFilterConfig

解密前端传来的参数并修改传参

  1. import com.demo.constant.UserConstant;
  2. import com.demo.excepiton.RSAException;
  3. import com.demo.utils.RSAUtils;
  4. import com.demo.utils.TokenUtils;
  5. import io.jsonwebtoken.Claims;
  6. import org.apache.commons.lang3.StringUtils;
  7. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
  8. import org.springframework.cloud.gateway.filter.GlobalFilter;
  9. import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
  10. import org.springframework.cloud.gateway.support.BodyInserterContext;
  11. import org.springframework.context.annotation.Configuration;
  12. import org.springframework.core.Ordered;
  13. import org.springframework.core.io.buffer.DataBuffer;
  14. import org.springframework.http.HttpHeaders;
  15. import org.springframework.http.ReactiveHttpOutputMessage;
  16. import org.springframework.http.server.reactive.ServerHttpRequest;
  17. import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
  18. import org.springframework.stereotype.Component;
  19. import org.springframework.util.AntPathMatcher;
  20. import org.springframework.web.reactive.function.BodyInserter;
  21. import org.springframework.web.reactive.function.BodyInserters;
  22. import org.springframework.web.reactive.function.server.HandlerStrategies;
  23. import org.springframework.web.reactive.function.server.ServerRequest;
  24. import org.springframework.web.server.ServerWebExchange;
  25. import reactor.core.publisher.Flux;
  26. import reactor.core.publisher.Mono;
  27. import java.lang.reflect.Field;
  28. import java.net.URI;
  29. /**
  30. * @Description:
  31. * @Author: rosh
  32. * @Date: 2021/10/26 22:24
  33. */
  34. @Configuration
  35. @Component
  36. public class GatewayFilterConfig implements GlobalFilter, Ordered {
  37. @Override
  38. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  39. //1 如果是登录不校验Token
  40. String requestUrl = exchange.getRequest().getPath().value();
  41. AntPathMatcher pathMatcher = new AntPathMatcher();
  42. if (!pathMatcher.match("/user/login", requestUrl)) {
  43. String token = exchange.getRequest().getHeaders().getFirst(UserConstant.TOKEN);
  44. Claims claim = TokenUtils.getClaim(token);
  45. if (StringUtils.isBlank(token) || claim == null) {
  46. return FilterUtils.invalidToken(exchange);
  47. }
  48. }
  49. //2 修改请求参数,并获取请求参数
  50. try {
  51. updateRequestParam(exchange);
  52. } catch (Exception e) {
  53. return FilterUtils.invalidUrl(exchange);
  54. }
  55. //3 获取请求体,修改请求体
  56. ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
  57. Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
  58. String encrypt = RSAUtils.decrypt(body, RSAConstant.PRIVATE_KEY);
  59. return Mono.just(encrypt);
  60. });
  61. //创建BodyInserter修改请求体
  62. BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
  63. HttpHeaders headers = new HttpHeaders();
  64. headers.putAll(exchange.getRequest().getHeaders());
  65. headers.remove(HttpHeaders.CONTENT_LENGTH);
  66. //创建CachedBodyOutputMessage并且把请求param加入
  67. CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
  68. return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
  69. ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
  70. @Override
  71. public Flux<DataBuffer> getBody() {
  72. return outputMessage.getBody();
  73. }
  74. };
  75. return chain.filter(exchange.mutate().request(decorator).build());
  76. }));
  77. }
  78. /**
  79. * 修改前端传的参数
  80. */
  81. private void updateRequestParam(ServerWebExchange exchange) throws NoSuchFieldException, IllegalAccessException {
  82. ServerHttpRequest request = exchange.getRequest();
  83. URI uri = request.getURI();
  84. String query = uri.getQuery();
  85. if (StringUtils.isNotBlank(query) && query.contains("param")) {
  86. String[] split = query.split("=");
  87. String param = RSAUtils.decrypt(split[1], RSAConstant.PRIVATE_KEY);
  88. Field targetQuery = uri.getClass().getDeclaredField("query");
  89. targetQuery.setAccessible(true);
  90. targetQuery.set(uri, param);
  91. }
  92. }
  93. @Override
  94. public int getOrder() {
  95. return 80;
  96. }
  97. }

1.3.4 GateWay 统一异常
  1. public abstract class AbstractExceptionHandler {
  2. protected JSONObject buildErrorMap(Throwable ex) {
  3. JSONObject json = new JSONObject();
  4. if (ex instanceof RSAException || ex instanceof IllegalArgumentException) {
  5. json.put("code", HttpStatus.BAD_REQUEST.value());
  6. if (StringUtils.isNotBlank(ex.getMessage())){
  7. json.put("msg", ex.getMessage());
  8. }else {
  9. json.put("msg", "无效的请求");
  10. }
  11. } else {
  12. json.put("code", HttpStatus.BAD_REQUEST.value());
  13. json.put("msg", "未知错误联系管理员");
  14. }
  15. return json;
  16. }
  17. }
  1. @Configuration
  2. public class GatewayExceptionConfig {
  3. @Primary
  4. @Bean
  5. @Order(Ordered.HIGHEST_PRECEDENCE)
  6. public ErrorWebExceptionHandler errorWebExceptionHandler(ObjectProvider<List<ViewResolver>> viewResolversProvider,
  7. ServerCodecConfigurer serverCodecConfigurer) {
  8. GatewayExceptionHandler gatewayExceptionHandler = new GatewayExceptionHandler();
  9. gatewayExceptionHandler.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList));
  10. gatewayExceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
  11. gatewayExceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
  12. return gatewayExceptionHandler;
  13. }
  14. }
  1. import com.alibaba.fastjson.JSONObject;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
  4. import org.springframework.http.HttpStatus;
  5. import org.springframework.http.MediaType;
  6. import org.springframework.http.codec.HttpMessageReader;
  7. import org.springframework.http.codec.HttpMessageWriter;
  8. import org.springframework.util.Assert;
  9. import org.springframework.web.reactive.function.BodyInserters;
  10. import org.springframework.web.reactive.function.server.RequestPredicates;
  11. import org.springframework.web.reactive.function.server.RouterFunctions;
  12. import org.springframework.web.reactive.function.server.ServerRequest;
  13. import org.springframework.web.reactive.function.server.ServerResponse;
  14. import org.springframework.web.reactive.result.view.ViewResolver;
  15. import org.springframework.web.server.ServerWebExchange;
  16. import reactor.core.publisher.Mono;
  17. import java.util.Collections;
  18. import java.util.List;
  19. import java.util.Map;
  20. @Slf4j
  21. public class GatewayExceptionHandler extends AbstractExceptionHandler implements ErrorWebExceptionHandler {
  22. private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
  23. private List<HttpMessageWriter<?>> messageWriters = Collections.emptyList();
  24. private List<ViewResolver> viewResolvers = Collections.emptyList();
  25. private ThreadLocal<JSONObject> exceptionHandlerResult = new ThreadLocal<>();
  26. public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
  27. Assert.notNull(messageReaders, "'messageReaders' must not be null");
  28. this.messageReaders = messageReaders;
  29. }
  30. public void setViewResolvers(List<ViewResolver> viewResolvers) {
  31. this.viewResolvers = viewResolvers;
  32. }
  33. public void setMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
  34. Assert.notNull(messageWriters, "'messageWriters' must not be null");
  35. this.messageWriters = messageWriters;
  36. }
  37. @Override
  38. public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
  39. JSONObject errorInfo = super.buildErrorMap(ex);
  40. if (exchange.getResponse().isCommitted()) {
  41. return Mono.error(ex);
  42. }
  43. exceptionHandlerResult.set(errorInfo);
  44. ServerRequest newRequest = ServerRequest.create(exchange, this.messageReaders);
  45. return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse).route(newRequest)
  46. .switchIfEmpty(Mono.error(ex))
  47. .flatMap(handler -> handler.handle(newRequest))
  48. .flatMap(response -> write(exchange, response));
  49. }
  50. protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
  51. Map<String, Object> result = exceptionHandlerResult.get();
  52. return ServerResponse.status(HttpStatus.OK)
  53. .contentType(MediaType.APPLICATION_JSON)
  54. .body(BodyInserters.fromValue(result));
  55. }
  56. private Mono<? extends Void> write(ServerWebExchange exchange,
  57. ServerResponse response) {
  58. exchange.getResponse().getHeaders().setContentType(response.headers().getContentType());
  59. return response.writeTo(exchange, new ResponseContext());
  60. }


  1. private class ResponseContext implements ServerResponse.Context {
  2. @Override
  3. public List<HttpMessageWriter<?>> messageWriters() {
  4. return GatewayExceptionHandler.this.messageWriters;
  5. }
  6. @Override
  7. public List<ViewResolver> viewResolvers() {
  8. return GatewayExceptionHandler.this.viewResolvers;
  9. }
  10. }
  11. }

1.3.5 JAVA业务代码
  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserController {
  4. @Autowired
  5. private UserService userService;
  6. @PostMapping("/login")
  7. public String login(@RequestBody UserForm userForm) {
  8. return userService.login(userForm);
  9. }
  10. @GetMapping("/detail")
  11. public JSONObject detail(@RequestParam("id") Long id) {
  12. return userService.detail(id);
  13. }
  14. }
  1. @Service
  2. public class UserService {
  3. private static final String USERNAME = "admin";
  4. private static final String PASSWORD = "123456";
  5. private static final Long USER_ID = 1L;
  6. /**
  7. * 模拟 登录 username = admin, password =123456,user_id 1L 登录成功 返回token
  8. */
  9. public String login(UserForm userForm) {
  10. String username = userForm.getUsername();
  11. String password = userForm.getPassword();
  12. if (USERNAME.equals(username) && PASSWORD.equals(password)) {
  13. JSONObject userInfo = new JSONObject();
  14. userInfo.put("username", USERNAME);
  15. userInfo.put("password", PASSWORD);
  16. userInfo.put("userId", USER_ID);
  17. return TokenUtils.createToken(userInfo.toJSONString());
  18. }
  19. return "账号密码不正确";
  20. }
  21. public JSONObject detail(Long id) {
  22. JSONObject jsonObject = new JSONObject();
  23. jsonObject.put("id", id);
  24. jsonObject.put("name", "admin");
  25. return jsonObject;
  26. }
  27. }

1.3.6 测试

登录:返回token
Gateway API接口安全设计(加密 、签名) - 图6
Gateway API接口安全设计(加密 、签名) - 图7
查询:
Gateway API接口安全设计(加密 、签名) - 图8

2 设置URL有效时长

为了增强URL安全性,前端在header中添加时间戳。

2.1 前端代码

在header中添加时间戳
Gateway API接口安全设计(加密 、签名) - 图9

2.2 后端验证时间戳

Gateway API接口安全设计(加密 、签名) - 图10

  1. private Long getDateTimestamp(HttpHeaders httpHeaders) {
  2. List<String> list = httpHeaders.get("timestamp");
  3. if (CollectionUtils.isEmpty(list)) {
  4. throw new IllegalArgumentException("拒绝服务");
  5. }
  6. long timestamp = Long.parseLong(list.get(0));
  7. long currentTimeMillis = System.currentTimeMillis();
  8. //有效时长为5分钟
  9. if (currentTimeMillis - timestamp > 1000 * 60 * 5) {
  10. throw new IllegalArgumentException("拒绝服务");
  11. }
  12. return timestamp;
  13. }

2.3 测试不传时间戳

Gateway API接口安全设计(加密 、签名) - 图11

3 确保URL唯一性

确保URL唯一性,前端请求中增加UUID,后端存入redis,有效时长为5分钟,5分钟重复提交拒绝服务

3.1 修改前端请求参数

Gateway API接口安全设计(加密 、签名) - 图12

3.2 后端增加验证RequestId

Gateway API接口安全设计(加密 、签名) - 图13

  1. private String getRequestId(HttpHeaders headers) {
  2. List<String> list = headers.get("requestId");
  3. if (CollectionUtils.isEmpty(list)) {
  4. throw new IllegalArgumentException(ERROR_MESSAGE);
  5. }
  6. String requestId = list.get(0);
  7. //如果requestId存在redis中直接返回
  8. String temp = redisTemplate.opsForValue().get(requestId);
  9. if (StringUtils.isNotBlank(temp)) {
  10. throw new IllegalArgumentException(ERROR_MESSAGE);
  11. }
  12. redisTemplate.opsForValue().set(requestId, requestId, 5, TimeUnit.MINUTES);
  13. return requestId;
  14. }

4 增加签名

最后一步,添加签名

4.1 前端增加签名

跟前端约定好,json数据按照ASCII升序排序。
登录页面:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录页面</title>
  6. </head>
  7. <body>
  8. <h1>登录</h1>
  9. <from id="from">
  10. 账号:<input id="username" type="text"/>
  11. <br/>
  12. 密码:<input id="password" type="password"/>
  13. <br/>
  14. <input id="btn_login" type="button" value="登录"/>
  15. </from>
  16. <script src="js/jquery.min.js"></script>
  17. <script src="js/jsencrypt.js"></script>
  18. <script src="js/md5.min.js"></script>
  19. <script type="text/javascript">
  20. var encrypt = new JSEncrypt();
  21. encrypt.setPublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFtTlL61IqIGd+fRLUhJ0MjsqFXFJswCohJ45m51WvbxDPRP3gllW0WChk74D5JEOpMDSWo4C7RfoGlBRNW7kQ6qYGukYZ5jgYpzoT0+gp3on96fQXEyQJysv9xiTPIdmSXXVVj1HAOJw29RbzxIVKUSzzPXvEtXRTtCC1+wkAJQIDAQAB");
  22. $("#btn_login").click(function () {
  23. //表单
  24. const username = $("#username").val();
  25. const password = $("#password").val();
  26. const form = {};
  27. form.username = username;
  28. form.password = password;
  29. //生成签名,也可以加盐
  30. const timestamp = Date.parse(new Date());
  31. const data = JSON.stringify(sort_ASCII(form));
  32. const requestId = getUuid();
  33. const sign = MD5(data + requestId + timestamp);
  34. $.ajax({
  35. url: "http://localhost:9000/api/user/login",
  36. beforeSend: function (XMLHttpRequest) {
  37. XMLHttpRequest.setRequestHeader("timestamp", timestamp);
  38. XMLHttpRequest.setRequestHeader("requestId", requestId);
  39. XMLHttpRequest.setRequestHeader("sign", sign);
  40. },
  41. data: encrypt.encrypt(data),
  42. type: "POST",
  43. dataType: "json",
  44. contentType: "application/json;charset=utf-8",
  45. success: function (data) {
  46. console.log(data);
  47. }
  48. });
  49. });
  50. function getUuid() {
  51. var s = [];
  52. var hexDigits = "0123456789abcdef";
  53. for (var i = 0; i < 32; i++) {
  54. s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
  55. }
  56. s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
  57. s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
  58. s[8] = s[13] = s[18] = s[23];
  59. var uuid = s.join("");
  60. return uuid;
  61. }
  62. function sort_ASCII(obj) {
  63. var arr = new Array();
  64. var num = 0;
  65. for (var i in obj) {
  66. arr[num] = i;
  67. num++;
  68. }
  69. var sortArr = arr.sort();
  70. var sortObj = {};
  71. for (var i in sortArr) {
  72. sortObj[sortArr[i]] = obj[sortArr[i]];
  73. }
  74. return sortObj;
  75. }
  76. </script>
  77. </body>
  78. </html>

4.2 增强读取Body类

  1. /**
  2. * @Description:
  3. * @Author: Rosh
  4. * @Date: 2021/10/27 11:03
  5. */
  6. public class MyCachedBodyOutputMessage extends CachedBodyOutputMessage {
  7. private Map<String, Object> paramMap;
  8. private Long dateTimestamp;
  9. private String requestId;
  10. private String sign;
  11. public MyCachedBodyOutputMessage(ServerWebExchange exchange, HttpHeaders httpHeaders) {
  12. super(exchange, httpHeaders);
  13. }
  14. public void initial(Map<String, Object> paramMap, String requestId, String sign, Long dateTimestamp) {
  15. this.paramMap = paramMap;
  16. this.requestId = requestId;
  17. this.sign = sign;
  18. this.dateTimestamp = dateTimestamp;
  19. }
  20. public Map<String, Object> getParamMap() {
  21. return paramMap;
  22. }
  23. public Long getDateTimestamp() {
  24. return dateTimestamp;
  25. }
  26. public String getRequestId() {
  27. return requestId;
  28. }
  29. public String getSign() {
  30. return sign;
  31. }
  32. }

4.3 修改GatewayFilterConfig


  1. import com.alibaba.fastjson.JSON;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.alibaba.nacos.common.utils.Md5Utils;
  4. import com.demo.constant.UserConstant;
  5. import com.demo.gateway.pojo.MyCachedBodyOutputMessage;
  6. import com.demo.utils.RSAUtils;
  7. import com.demo.utils.TokenUtils;
  8. import io.jsonwebtoken.Claims;
  9. import org.apache.commons.lang3.StringUtils;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
  12. import org.springframework.cloud.gateway.filter.GlobalFilter;
  13. import org.springframework.cloud.gateway.support.BodyInserterContext;
  14. import org.springframework.context.annotation.Configuration;
  15. import org.springframework.core.Ordered;
  16. import org.springframework.core.io.buffer.DataBuffer;
  17. import org.springframework.data.redis.core.RedisTemplate;
  18. import org.springframework.http.HttpHeaders;
  19. import org.springframework.http.ReactiveHttpOutputMessage;
  20. import org.springframework.http.server.reactive.ServerHttpRequest;
  21. import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
  22. import org.springframework.stereotype.Component;
  23. import org.springframework.util.AntPathMatcher;
  24. import org.springframework.util.CollectionUtils;
  25. import org.springframework.web.reactive.function.BodyInserter;
  26. import org.springframework.web.reactive.function.BodyInserters;
  27. import org.springframework.web.reactive.function.server.HandlerStrategies;
  28. import org.springframework.web.reactive.function.server.ServerRequest;
  29. import org.springframework.web.server.ServerWebExchange;
  30. import reactor.core.publisher.Flux;
  31. import reactor.core.publisher.Mono;
  32. import java.lang.reflect.Field;
  33. import java.net.URI;
  34. import java.util.List;
  35. import java.util.Map;
  36. import java.util.TreeMap;
  37. import java.util.concurrent.TimeUnit;
  38. /**
  39. * @Description:
  40. * @Author: rosh
  41. * @Date: 2021/10/26 22:24
  42. */
  43. @Configuration
  44. @Component
  45. public class GatewayFilterConfig implements GlobalFilter, Ordered {
  46. @Autowired
  47. private RedisTemplate<String, String> redisTemplate;
  48. private static final String ERROR_MESSAGE = "拒绝服务";
  49. @Override
  50. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  51. //1 获取时间戳
  52. Long dateTimestamp = getDateTimestamp(exchange.getRequest().getHeaders());
  53. //2 获取RequestId
  54. String requestId = getRequestId(exchange.getRequest().getHeaders());
  55. //3 获取签名
  56. String sign = getSign(exchange.getRequest().getHeaders());
  57. //4 如果是登录不校验Token
  58. String requestUrl = exchange.getRequest().getPath().value();
  59. AntPathMatcher pathMatcher = new AntPathMatcher();
  60. if (!pathMatcher.match("/user/login", requestUrl)) {
  61. String token = exchange.getRequest().getHeaders().getFirst(UserConstant.TOKEN);
  62. Claims claim = TokenUtils.getClaim(token);
  63. if (StringUtils.isBlank(token) || claim == null) {
  64. return FilterUtils.invalidToken(exchange);
  65. }
  66. }
  67. //5 修改请求参数,并获取请求参数
  68. Map<String, Object> paramMap;
  69. try {
  70. paramMap = updateRequestParam(exchange);
  71. } catch (Exception e) {
  72. return FilterUtils.invalidUrl(exchange);
  73. }
  74. //6 获取请求体,修改请求体
  75. ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
  76. Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
  77. String encrypt = RSAUtils.decrypt(body, RSAConstant.PRIVATE_KEY);
  78. JSONObject jsonObject = JSON.parseObject(encrypt);
  79. for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
  80. paramMap.put(entry.getKey(), entry.getValue());
  81. }
  82. checkSign(sign, dateTimestamp, requestId, paramMap);
  83. return Mono.just(encrypt);
  84. });
  85. //创建BodyInserter修改请求体
  86. BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
  87. HttpHeaders headers = new HttpHeaders();
  88. headers.putAll(exchange.getRequest().getHeaders());
  89. headers.remove(HttpHeaders.CONTENT_LENGTH);
  90. //创建CachedBodyOutputMessage并且把请求param加入,初始化校验信息
  91. MyCachedBodyOutputMessage outputMessage = new MyCachedBodyOutputMessage(exchange, headers);
  92. outputMessage.initial(paramMap, requestId, sign, dateTimestamp);
  93. return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
  94. ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
  95. @Override
  96. public Flux<DataBuffer> getBody() {
  97. Flux<DataBuffer> body = outputMessage.getBody();
  98. if (body.equals(Flux.empty())) {
  99. //验证签名
  100. checkSign(outputMessage.getSign(), outputMessage.getDateTimestamp(), outputMessage.getRequestId(), outputMessage.getParamMap());
  101. }
  102. return outputMessage.getBody();
  103. }
  104. };
  105. return chain.filter(exchange.mutate().request(decorator).build());
  106. }));
  107. }
  108. public void checkSign(String sign, Long dateTimestamp, String requestId, Map<String, Object> paramMap) {
  109. String str = JSON.toJSONString(paramMap) + requestId + dateTimestamp;
  110. String tempSign = Md5Utils.getMD5(str.getBytes());
  111. if (!tempSign.equals(sign)) {
  112. throw new IllegalArgumentException(ERROR_MESSAGE);
  113. }
  114. }
  115. /**
  116. * 修改前端传的参数
  117. */
  118. private Map<String, Object> updateRequestParam(ServerWebExchange exchange) throws NoSuchFieldException, IllegalAccessException {
  119. ServerHttpRequest request = exchange.getRequest();
  120. URI uri = request.getURI();
  121. String query = uri.getQuery();
  122. if (StringUtils.isNotBlank(query) && query.contains("param")) {
  123. String[] split = query.split("=");
  124. String param = RSAUtils.decrypt(split[1], RSAConstant.PRIVATE_KEY);
  125. Field targetQuery = uri.getClass().getDeclaredField("query");
  126. targetQuery.setAccessible(true);
  127. targetQuery.set(uri, param);
  128. return getParamMap(param);
  129. }
  130. return new TreeMap<>();
  131. }
  132. private Map<String, Object> getParamMap(String param) {
  133. Map<String, Object> map = new TreeMap<>();
  134. String[] split = param.split("&");
  135. for (String str : split) {
  136. String[] params = str.split("=");
  137. map.put(params[0], params[1]);
  138. }
  139. return map;
  140. }
  141. private String getSign(HttpHeaders headers) {
  142. List<String> list = headers.get("sign");
  143. if (CollectionUtils.isEmpty(list)) {
  144. throw new IllegalArgumentException(ERROR_MESSAGE);
  145. }
  146. return list.get(0);
  147. }
  148. private Long getDateTimestamp(HttpHeaders httpHeaders) {
  149. List<String> list = httpHeaders.get("timestamp");
  150. if (CollectionUtils.isEmpty(list)) {
  151. throw new IllegalArgumentException(ERROR_MESSAGE);
  152. }
  153. long timestamp = Long.parseLong(list.get(0));
  154. long currentTimeMillis = System.currentTimeMillis();
  155. //有效时长为5分钟
  156. if (currentTimeMillis - timestamp > 1000 * 60 * 5) {
  157. throw new IllegalArgumentException(ERROR_MESSAGE);
  158. }
  159. return timestamp;
  160. }
  161. private String getRequestId(HttpHeaders headers) {
  162. List<String> list = headers.get("requestId");
  163. if (CollectionUtils.isEmpty(list)) {
  164. throw new IllegalArgumentException(ERROR_MESSAGE);
  165. }
  166. String requestId = list.get(0);
  167. //如果requestId存在redis中直接返回
  168. String temp = redisTemplate.opsForValue().get(requestId);
  169. if (StringUtils.isNotBlank(temp)) {
  170. throw new IllegalArgumentException(ERROR_MESSAGE);
  171. }
  172. redisTemplate.opsForValue().set(requestId, requestId, 5, TimeUnit.MINUTES);
  173. return requestId;
  174. }
  175. @Override
  176. public int getOrder() {
  177. return 80;
  178. }
  179. }



4.4 测试登录

发现验签成功
Gateway API接口安全设计(加密 、签名) - 图14

4.5 测试查询

验签成功
Gateway API接口安全设计(加密 、签名) - 图15