[TOC]


SpringCloud Gateway API接口安全设计 - 图1
防止数据抓包窃取
SpringCloud Gateway API接口安全设计 - 图2

风险简述

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



SpringCloud Gateway API接口安全设计 - 图4
RSA 非对称加密
SpringCloud Gateway API接口安全设计 - 图5

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日失效。由于该算法在申请专利前就已经被发表了,在世界上大多数其它地区这个专利权不被承认。

RSA应用过程

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

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

import java.util.Map;

@Slf4j public class RSAUtils {

public static final String PUBLIC_KEY = "public_key";

public static final String PRIVATE_KEY = "private_key";


public static Map<String, String> generateRasKey() {
    Map<String, String> rs = new HashMap<>();
    try {
        // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
        KeyPairGenerator keyPairGen = null;
        keyPairGen = KeyPairGenerator.getInstance("RSA");
        keyPairGen.initialize(1024, new SecureRandom());
        // 生成一个密钥对,保存在keyPair中
        KeyPair keyPair = keyPairGen.generateKeyPair();
        // 得到私钥 公钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded()));
        // 得到私钥字符串
        String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded())));
        // 将公钥和私钥保存到Map
        rs.put(PUBLIC_KEY, publicKeyString);
        rs.put(PRIVATE_KEY, privateKeyString);
    } catch (Exception e) {
        log.error("RsaUtils invoke genKeyPair failed.", e);
        throw new RsaException("RsaUtils invoke genKeyPair failed.");
    }
    return rs;
}


public static String encrypt(String str, String publicKey) {
    try {
        //base64编码的公钥
        byte[] decoded = Base64.decodeBase64(publicKey);
        RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
        //RSA加密
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, pubKey);
        return Base64.encodeBase64String(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8)));
    } catch (Exception e) {
        log.error("RsaUtils invoke encrypt failed.", e);
        throw new RsaException("RsaUtils invoke encrypt failed.");
    }
}


public static String decrypt(String str, String privateKey) {

    try {
        //64位解码加密后的字符串
        byte[] inputByte = Base64.decodeBase64(str.getBytes(StandardCharsets.UTF_8));
        //base64编码的私钥
        byte[] decoded = Base64.decodeBase64(privateKey);
        RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
        //RSA解密
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, priKey);
        return new String(cipher.doFinal(inputByte));
    } catch (Exception e) {
        log.error("RsaUtils invoke decrypt failed.", e);
        throw new RsaException("RsaUtils invoke decrypt failed.");
    }

}

}



RsaException: 是自定义异常

@Getter public class RsaException extends RuntimeException {

private final String message;

public RsaException(String message) {
    this.message = message;
}

}



<a name="NkRDw"></a>
##### 1.2.4 UT

package com.rosh;

public class RsaTest {

/**
 *  用测试生成的公钥,私钥赋值
 */
private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFtTlL61IqIGd+fRLUhJ0MjsqFXFJswCohJ45m51WvbxDPRP3gllW0WChk74D5JEOpMDSWo4C7RfoGlBRNW7kQ6qYGukYZ5jgYpzoT0+gp3on96fQXEyQJysv9xiTPIdmSXXVVj1HAOJw29RbzxIVKUSzzPXvEtXRTtCC1+wkAJQIDAQAB";

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=";


/**
 *  生成公钥私钥
 */
@Test
public void generateRsaKey() {
    Map<String, String> map = RSAUtils.generateRasKey();
    System.out.println("随机生成的公钥为:" + map.get(RSAUtils.PUBLIC_KEY));
    System.out.println("随机生成的私钥为:" + map.get(RSAUtils.PRIVATE_KEY));

}

/**
 * 加密: Yeidauky/iN1/whevov2+ntzXJKAp2AHfESu5ixnDqH5iB7ww+TcfqJpDfkPHfb12Y0sVXw0gBHNJ4inkh7l2/SJBze3pKQU/mg3oyDokTia3JZIs+e80/iJcSfN+yA1JaqY+eJPYiBiOGAF2S6x0ynvJg/Wj0fwp2Tq3PDzRMo=
 */
@Test
public void testEncrypt() {
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("username", "rosh");
    jsonObject.put("password", "123456");
    String str = jsonObject.toJSONString();
    String encrypt = RSAUtils.encrypt(str, PUBLIC_KEY);
    System.out.println(encrypt);
}

@Test
public void testDecrypt() {

    String decrypt = RSAUtils.decrypt("Yeidauky/iN1/whevov2+ntzXJKAp2AHfESu5ixnDqH5iB7ww+TcfqJpDfkPHfb12Y0sVXw0gBHNJ4inkh7l2/SJBze3pKQU/mg3oyDokTia3JZIs+e80/iJcSfN+yA1JaqY+eJPYiBiOGAF2S6x0ynvJg/Wj0fwp2Tq3PDzRMo=",
            PRIVATE_KEY);

    System.out.println(decrypt);
}

}



![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673247643-d5ef5119-7e3d-4c68-9059-05f0ca0a3bb9.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=uaacee4a6&margin=%5Bobject%20Object%5D&originHeight=562&originWidth=1080&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ue05a9837-c14e-4775-beec-df56eea4cb0&title=)
<a name="jI6kR"></a>
#### 案例
SpringCloud Gateway + SpringBoot + Nacos+redis<br />![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673247858-00e329da-e51e-47e0-83e1-4c75e2fe4015.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u15e2a6df&margin=%5Bobject%20Object%5D&originHeight=443&originWidth=463&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u3c03dbd3-5798-4d20-99cd-448ec3a8de1&title=)<br />![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673248397-0cf92884-d75b-4544-bd37-89ad788c4670.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ub896b7e2&margin=%5Bobject%20Object%5D&originHeight=339&originWidth=442&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u10365b3f-bbc7-405e-afe4-377a690edfc&title=)
<a name="PkzF7"></a>
##### 前端登录代码
后端把公钥跟前端约定好:

<!DOCTYPE html>

登录

账号:
密码:
<a name="O5n1N"></a>
##### 前端查询代码
设定公钥、token,token是登录成功后返回的值

<!DOCTYPE html>

id:



<a name="aBjwR"></a>
##### GatewayFilterConfig
解密前端传来的参数并修改传参

package com.demo.gateway.config;

public class GatewayFilterConfig implements GlobalFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    //1 如果是登录不校验Token
    String requestUrl = exchange.getRequest().getPath().value();
    AntPathMatcher pathMatcher = new AntPathMatcher();
    if (!pathMatcher.match("/user/login", requestUrl)) {
        String token = exchange.getRequest().getHeaders().getFirst(UserConstant.TOKEN);
        Claims claim = TokenUtils.getClaim(token);
        if (StringUtils.isBlank(token) || claim == null) {
            return FilterUtils.invalidToken(exchange);
        }
    }
    //2 修改请求参数,并获取请求参数
    try {
        updateRequestParam(exchange);
    } catch (Exception e) {
        return FilterUtils.invalidUrl(exchange);
    }
    //3 获取请求体,修改请求体
    ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
    Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
        String encrypt = RSAUtils.decrypt(body, RSAConstant.PRIVATE_KEY);
        return Mono.just(encrypt);
    });

    //创建BodyInserter修改请求体
    BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
    HttpHeaders headers = new HttpHeaders();
    headers.putAll(exchange.getRequest().getHeaders());
    headers.remove(HttpHeaders.CONTENT_LENGTH);
    //创建CachedBodyOutputMessage并且把请求param加入
    CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
    return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
        ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };
        return chain.filter(exchange.mutate().request(decorator).build());
    }));

}

/**
 * 修改前端传的参数
 */
private void updateRequestParam(ServerWebExchange exchange) throws NoSuchFieldException, IllegalAccessException {
    ServerHttpRequest request = exchange.getRequest();
    URI uri = request.getURI();
    String query = uri.getQuery();
    if (StringUtils.isNotBlank(query) && query.contains("param")) {
        String[] split = query.split("=");
        String param = RSAUtils.decrypt(split[1], RSAConstant.PRIVATE_KEY);
        Field targetQuery = uri.getClass().getDeclaredField("query");
        targetQuery.setAccessible(true);
        targetQuery.set(uri, param);
    }
}


@Override
public int getOrder() {
    return 80;
}

}

<a name="nARNv"></a>
##### GateWay 统一异常

public abstract class AbstractExceptionHandler { protected JSONObject buildErrorMap(Throwable ex) { JSONObject json = new JSONObject(); if (ex instanceof RSAException || ex instanceof IllegalArgumentException) { json.put(“code”, HttpStatus.BAD_REQUEST.value()); if (StringUtils.isNotBlank(ex.getMessage())){ json.put(“msg”, ex.getMessage()); }else { json.put(“msg”, “无效的请求”); }

    } else {
        json.put("code", HttpStatus.BAD_REQUEST.value());
        json.put("msg", "未知错误联系管理员");
    }
    return json;
}

}



@Configuration public class GatewayExceptionConfig {

@Primary
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ErrorWebExceptionHandler errorWebExceptionHandler(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                                         ServerCodecConfigurer serverCodecConfigurer) {
    GatewayExceptionHandler gatewayExceptionHandler = new GatewayExceptionHandler();
    gatewayExceptionHandler.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList));
    gatewayExceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
    gatewayExceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
    return gatewayExceptionHandler;
}

}



package com.demo.gateway.exception;

import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.util.Assert; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;

import java.util.Collections; import java.util.List; import java.util.Map;

@Slf4j public class GatewayExceptionHandler extends AbstractExceptionHandler implements ErrorWebExceptionHandler {

private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();


private List<HttpMessageWriter<?>> messageWriters = Collections.emptyList();


private List<ViewResolver> viewResolvers = Collections.emptyList();


private ThreadLocal<JSONObject> exceptionHandlerResult = new ThreadLocal<>();


public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
    Assert.notNull(messageReaders, "'messageReaders' must not be null");
    this.messageReaders = messageReaders;
}


public void setViewResolvers(List<ViewResolver> viewResolvers) {
    this.viewResolvers = viewResolvers;
}


public void setMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
    Assert.notNull(messageWriters, "'messageWriters' must not be null");
    this.messageWriters = messageWriters;
}

@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
    JSONObject errorInfo = super.buildErrorMap(ex);
    if (exchange.getResponse().isCommitted()) {
        return Mono.error(ex);
    }
    exceptionHandlerResult.set(errorInfo);
    ServerRequest newRequest = ServerRequest.create(exchange, this.messageReaders);
    return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse).route(newRequest)
            .switchIfEmpty(Mono.error(ex))
            .flatMap(handler -> handler.handle(newRequest))
            .flatMap(response -> write(exchange, response));

}


protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
    Map<String, Object> result = exceptionHandlerResult.get();
    return ServerResponse.status(HttpStatus.OK)
            .contentType(MediaType.APPLICATION_JSON)
            .body(BodyInserters.fromValue(result));
}


private Mono<? extends Void> write(ServerWebExchange exchange,
                                   ServerResponse response) {
    exchange.getResponse().getHeaders().setContentType(response.headers().getContentType());
    return response.writeTo(exchange, new ResponseContext());
}

private class ResponseContext implements ServerResponse.Context {

    @Override
    public List<HttpMessageWriter<?>> messageWriters() {
        return GatewayExceptionHandler.this.messageWriters;
    }

    @Override
    public List<ViewResolver> viewResolvers() {
        return GatewayExceptionHandler.this.viewResolvers;
    }
}

}

<a name="Z2JpP"></a>
##### JAVA业务代码

@RestController @RequestMapping(“/user”) public class UserController {

@Autowired
private UserService userService;

@PostMapping("/login")
public String login(@RequestBody UserForm userForm) {

    return userService.login(userForm);
}

@GetMapping("/detail")
public JSONObject detail(@RequestParam("id") Long id) {

    return userService.detail(id);
}

}

@Service public class UserService {

private static final String USERNAME = "admin";

private static final String PASSWORD = "123456";

private static final Long USER_ID = 1L;


/**
 * 模拟 登录 username = admin, password =123456,user_id 1L 登录成功 返回token
 */
public String login(UserForm userForm) {

    String username = userForm.getUsername();
    String password = userForm.getPassword();

    if (USERNAME.equals(username) && PASSWORD.equals(password)) {
        JSONObject userInfo = new JSONObject();
        userInfo.put("username", USERNAME);
        userInfo.put("password", PASSWORD);
        userInfo.put("userId", USER_ID);
        return TokenUtils.createToken(userInfo.toJSONString());
    }

    return "账号密码不正确";
}


public JSONObject detail(Long id) {
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("id", id);
    jsonObject.put("name", "admin");
    return jsonObject;
}

}



<a name="xRSps"></a>
##### 测试
登录:返回token<br />![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673249563-7fbddfc5-7938-48cf-980e-69784503dcf7.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ude526d92&margin=%5Bobject%20Object%5D&originHeight=538&originWidth=1080&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u646a8ec6-74d0-4363-b8d9-0be04c7f29f&title=)<br />![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673248936-2787c93e-746e-4138-ac6f-11371cfec680.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u84c05424&margin=%5Bobject%20Object%5D&originHeight=441&originWidth=1080&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u66199d29-dc1a-47ce-b862-78288f5eca2&title=)<br />查询:<br />![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673249147-6fdba7cc-5564-4ed9-8e77-40b0798c70f1.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ud02a33eb&margin=%5Bobject%20Object%5D&originHeight=737&originWidth=976&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ue60036a0-426d-4094-b5ce-509cd4dd488&title=)
<a name="ViGir"></a>
### <br /><br />
![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673250222-e7f69a1d-7e9e-489c-a1e7-a5cefe178d38.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u3ed5d2ef&margin=%5Bobject%20Object%5D&originHeight=18&originWidth=14&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u914830a9-279b-40fc-89b2-f36e7c3c6f2&title=)<br />**设置URL有效时长**<br />![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673250129-86eacc36-2bb6-462e-9a5d-a867355c4963.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u704739b5&margin=%5Bobject%20Object%5D&originHeight=18&originWidth=14&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ub9578318-937b-4ffe-8984-d092d5fa4c2&title=)

为了增强URL安全性,前端在header中添加时间戳。
<a name="alqGj"></a>
#### 前端代码
在header中添加时间戳<br />![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673250428-8312e49b-5e41-4e95-8f1f-0b45e0dbfbad.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u27068808&margin=%5Bobject%20Object%5D&originHeight=308&originWidth=1080&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u457f3b8d-ac9b-4801-a315-433b4b54845&title=)
<a name="ThnGr"></a>
#### 2.2 后端验证时间戳
![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673251097-0c45d1c9-87ae-44f4-b8a0-1d53c77d4b04.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=uc90f9d6e&margin=%5Bobject%20Object%5D&originHeight=620&originWidth=900&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u55180372-46c0-436b-8c72-d51ae60a04b&title=)

private Long getDateTimestamp(HttpHeaders httpHeaders) { List list = httpHeaders.get(“timestamp”); if (CollectionUtils.isEmpty(list)) { throw new IllegalArgumentException(“拒绝服务”); } long timestamp = Long.parseLong(list.get(0)); long currentTimeMillis = System.currentTimeMillis(); //有效时长为5分钟 if (currentTimeMillis - timestamp > 1000 60 5) { throw new IllegalArgumentException(“拒绝服务”); } return timestamp; }



<a name="S3WdP"></a>
#### 测试不传时间戳
![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673251362-6c000122-922b-4b01-9893-c0ca4d9fb0e4.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u31459af6&margin=%5Bobject%20Object%5D&originHeight=757&originWidth=1075&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ua55ef352-478b-46c7-b9e9-37e077eeb89&title=)
<a name="aJmKK"></a>
### <br /><br />
![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673251660-7a61908d-6e16-434c-92a7-c04a6e18877b.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u55fd8211&margin=%5Bobject%20Object%5D&originHeight=18&originWidth=14&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ua467fea2-bb13-4220-9ead-2191e9f60b3&title=)<br />**确保URL唯一性**<br />![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673251586-19f01ffd-a6bd-40ed-b3cd-2e6e85e725a2.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u3845e170&margin=%5Bobject%20Object%5D&originHeight=18&originWidth=14&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=uf21d9882-d346-426d-b006-427df447a74&title=)

确保URL唯一性,前端请求中增加UUID,后端存入redis,有效时长为5分钟,5分钟重复提交拒绝服务
<a name="CBvZI"></a>
#### 修改前端请求参数
![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673251686-68c1b8e4-6b13-4378-8b7d-a378137f49ec.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u5f2f004b&margin=%5Bobject%20Object%5D&originHeight=674&originWidth=1080&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u68c3403f-0b16-457a-8762-99841abada3&title=)
<a name="sEs3n"></a>
#### 3.2 后端增加验证RequestId
![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673251801-0ab7fcd7-ecf2-45cf-a5d8-665f04744e15.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u87603bab&margin=%5Bobject%20Object%5D&originHeight=573&originWidth=971&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u6d2631e7-39e9-4f91-8deb-6f521a5da8b&title=)

private String getRequestId(HttpHeaders headers) { List list = headers.get(“requestId”); if (CollectionUtils.isEmpty(list)) { throw new IllegalArgumentException(ERROR_MESSAGE); } String requestId = list.get(0); //如果requestId存在redis中直接返回 String temp = redisTemplate.opsForValue().get(requestId); if (StringUtils.isNotBlank(temp)) { throw new IllegalArgumentException(ERROR_MESSAGE); } redisTemplate.opsForValue().set(requestId, requestId, 5, TimeUnit.MINUTES); return requestId; }



<a name="ZHDAd"></a>
### <br /><br />
![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673251854-f7b94272-06cf-4d42-a8b9-a805f821b0b5.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u0bd3dd1b&margin=%5Bobject%20Object%5D&originHeight=18&originWidth=14&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u602a685e-def3-4efc-95bf-e3bb1a73454&title=)<br />**增加签名**<br />![](https://cdn.nlark.com/yuque/0/2022/png/8432623/1654673252156-12713452-c857-417a-bb7f-4175fcd2ccfb.png#clientId=u45f5e417-a6ed-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u3b981baa&margin=%5Bobject%20Object%5D&originHeight=18&originWidth=14&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=uf4451daa-b781-4663-9322-0457c76c0d4&title=)

最后一步,添加签名<br />**前端增加签名**<br />跟前端约定好,json数据按照ASCII升序排序。<br />登录页面:

<!DOCTYPE html>

登录

账号:
密码:



<a name="mSEDE"></a>
#### 增强读取Body类

/**

  • @Description:
  • @Author: Rosh
  • @Date: 2021/10/27 11:03 */ public class MyCachedBodyOutputMessage extends CachedBodyOutputMessage {

    private Map paramMap;

    private Long dateTimestamp;

    private String requestId;

    private String sign;

    public MyCachedBodyOutputMessage(ServerWebExchange exchange, HttpHeaders httpHeaders) {

     super(exchange, httpHeaders);
    

    }

    public void initial(Map paramMap, String requestId, String sign, Long dateTimestamp) {

     this.paramMap = paramMap;
     this.requestId = requestId;
     this.sign = sign;
     this.dateTimestamp = dateTimestamp;
    

    }

public Map<String, Object> getParamMap() {
    return paramMap;
}

public Long getDateTimestamp() {
    return dateTimestamp;
}

public String getRequestId() {
    return requestId;
}

public String getSign() {
    return sign;
}

}

<a name="TWskV"></a>
#### 4.3 修改GatewayFilterConfig

package com.demo.gateway.config;

public class GatewayFilterConfig implements GlobalFilter, Ordered {

@Autowired
private RedisTemplate<String, String> redisTemplate;


private static final String ERROR_MESSAGE = "拒绝服务";


@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    //1 获取时间戳
    Long dateTimestamp = getDateTimestamp(exchange.getRequest().getHeaders());
    //2 获取RequestId
    String requestId = getRequestId(exchange.getRequest().getHeaders());
    //3 获取签名
    String sign = getSign(exchange.getRequest().getHeaders());
    //4 如果是登录不校验Token
    String requestUrl = exchange.getRequest().getPath().value();
    AntPathMatcher pathMatcher = new AntPathMatcher();
    if (!pathMatcher.match("/user/login", requestUrl)) {
        String token = exchange.getRequest().getHeaders().getFirst(UserConstant.TOKEN);
        Claims claim = TokenUtils.getClaim(token);
        if (StringUtils.isBlank(token) || claim == null) {
            return FilterUtils.invalidToken(exchange);
        }
    }
    //5 修改请求参数,并获取请求参数
    Map<String, Object> paramMap;
    try {
        paramMap = updateRequestParam(exchange);
    } catch (Exception e) {
        return FilterUtils.invalidUrl(exchange);
    }
    //6 获取请求体,修改请求体
    ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
    Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
        String encrypt = RSAUtils.decrypt(body, RSAConstant.PRIVATE_KEY);
        JSONObject jsonObject = JSON.parseObject(encrypt);
        for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
            paramMap.put(entry.getKey(), entry.getValue());
        }
        checkSign(sign, dateTimestamp, requestId, paramMap);
        return Mono.just(encrypt);
    });

    //创建BodyInserter修改请求体

    BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
    HttpHeaders headers = new HttpHeaders();
    headers.putAll(exchange.getRequest().getHeaders());
    headers.remove(HttpHeaders.CONTENT_LENGTH);
    //创建CachedBodyOutputMessage并且把请求param加入,初始化校验信息
    MyCachedBodyOutputMessage outputMessage = new MyCachedBodyOutputMessage(exchange, headers);
    outputMessage.initial(paramMap, requestId, sign, dateTimestamp);
    return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
        ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public Flux<DataBuffer> getBody() {
                Flux<DataBuffer> body = outputMessage.getBody();
                if (body.equals(Flux.empty())) {
                    //验证签名
                    checkSign(outputMessage.getSign(), outputMessage.getDateTimestamp(), outputMessage.getRequestId(), outputMessage.getParamMap());
                }
                return outputMessage.getBody();
            }
        };
        return chain.filter(exchange.mutate().request(decorator).build());
    }));

}

public void checkSign(String sign, Long dateTimestamp, String requestId, Map<String, Object> paramMap) {
    String str = JSON.toJSONString(paramMap) + requestId + dateTimestamp;
    String tempSign = Md5Utils.getMD5(str.getBytes());
    if (!tempSign.equals(sign)) {
        throw new IllegalArgumentException(ERROR_MESSAGE);
    }
}

/**
 * 修改前端传的参数
 */
private Map<String, Object> updateRequestParam(ServerWebExchange exchange) throws NoSuchFieldException, IllegalAccessException {
    ServerHttpRequest request = exchange.getRequest();
    URI uri = request.getURI();
    String query = uri.getQuery();
    if (StringUtils.isNotBlank(query) && query.contains("param")) {
        String[] split = query.split("=");
        String param = RSAUtils.decrypt(split[1], RSAConstant.PRIVATE_KEY);
        Field targetQuery = uri.getClass().getDeclaredField("query");
        targetQuery.setAccessible(true);
        targetQuery.set(uri, param);
        return getParamMap(param);
    }
    return new TreeMap<>();
}


private Map<String, Object> getParamMap(String param) {
    Map<String, Object> map = new TreeMap<>();
    String[] split = param.split("&");
    for (String str : split) {
        String[] params = str.split("=");
        map.put(params[0], params[1]);
    }
    return map;
}


private String getSign(HttpHeaders headers) {
    List<String> list = headers.get("sign");
    if (CollectionUtils.isEmpty(list)) {
        throw new IllegalArgumentException(ERROR_MESSAGE);
    }
    return list.get(0);
}

private Long getDateTimestamp(HttpHeaders httpHeaders) {
    List<String> list = httpHeaders.get("timestamp");
    if (CollectionUtils.isEmpty(list)) {
        throw new IllegalArgumentException(ERROR_MESSAGE);
    }
    long timestamp = Long.parseLong(list.get(0));
    long currentTimeMillis = System.currentTimeMillis();
    //有效时长为5分钟
    if (currentTimeMillis - timestamp > 1000 * 60 * 5) {
        throw new IllegalArgumentException(ERROR_MESSAGE);
    }
    return timestamp;
}

private String getRequestId(HttpHeaders headers) {
    List<String> list = headers.get("requestId");
    if (CollectionUtils.isEmpty(list)) {
        throw new IllegalArgumentException(ERROR_MESSAGE);
    }
    String requestId = list.get(0);
    //如果requestId存在redis中直接返回
    String temp = redisTemplate.opsForValue().get(requestId);
    if (StringUtils.isNotBlank(temp)) {
        throw new IllegalArgumentException(ERROR_MESSAGE);
    }
    redisTemplate.opsForValue().set(requestId, requestId, 5, TimeUnit.MINUTES);
    return requestId;
}


@Override
public int getOrder() {
    return 80;
}

} ```

测试登录

发现验签成功
SpringCloud Gateway API接口安全设计 - 图6

测试查询

验签成功
SpringCloud Gateway API接口安全设计 - 图7



SpringCloud Gateway API接口安全设计 - 图8
地址
SpringCloud Gateway API接口安全设计 - 图9

https://gitee.com/zhurongsheng/springcloud-gateway-rsa