Spring Cloud Gateway API接口安全設(shè)計(jì)(加密 应又、簽名)

本文屬于轉(zhuǎn)載宙项,原文鏈接為:https://mp.weixin.qq.com/s/HN2JmeJf-8pLPsq3WYlzUg

1 防止數(shù)據(jù)抓包竊取

1.1 風(fēng)險(xiǎn)簡(jiǎn)述

簡(jiǎn)述:當(dāng)用戶登錄時(shí),惡意攻擊者可以用抓包工具可以拿到用戶提交的表單信息丁频,可以獲取用戶的賬號(hào)密碼杉允,進(jìn)而可以惡意訪問網(wǎng)站。


image.png

1.2 RSA 非對(duì)稱加密

1.2.1 RSA簡(jiǎn)介

RSA加密算法是一種非對(duì)稱加密算法席里。在公開密鑰加密和電子商業(yè)中RSA被廣泛使用叔磷。RSA是1977年由羅納德·李維斯特(Ron Rivest)、阿迪·薩莫爾(Adi Shamir)和倫納德·阿德曼(Leonard Adleman)一起提出的奖磁。當(dāng)時(shí)他們?nèi)硕荚诼槭±砉W(xué)院工作改基。RSA就是他們?nèi)诵帐祥_頭字母拼在一起組成的。

1973年咖为,在英國政府通訊總部工作的數(shù)學(xué)家克利福德·柯克斯(Clifford Cocks)在一個(gè)內(nèi)部文件中提出了一個(gè)相同的算法秕狰,但他的發(fā)現(xiàn)被列入機(jī)密,一直到1997年才被發(fā)表躁染。對(duì)極大整數(shù)做因數(shù)分解的難度決定了RSA算法的可靠性鸣哀。換言之,對(duì)一極大整數(shù)做因數(shù)分解愈困難吞彤,RSA算法愈可靠我衬。

假如有人找到一種快速因數(shù)分解的算法的話叹放,那么用RSA加密的信息的可靠性就肯定會(huì)極度下降。但找到這樣的算法的可能性是非常小的挠羔。今天只有短的RSA鑰匙才可能被強(qiáng)力方式解破井仰。到目前為止,世界上還沒有任何可靠的攻擊RSA算法的方式破加。只要其鑰匙的長(zhǎng)度足夠長(zhǎng)俱恶,用RSA加密的信息實(shí)際上是不能被解破的。

1983年麻省理工學(xué)院在美國為RSA算法申請(qǐng)了專利范舀。這個(gè)專利2000年9月21日失效合是。由于該算法在申請(qǐng)專利前就已經(jīng)被發(fā)表了,在世界上大多數(shù)其它地區(qū)這個(gè)專利權(quán)不被承認(rèn)尿背。

1.2.2 RSA應(yīng)用過程

非對(duì)稱算法的在應(yīng)用的過程如下:

接收方生成公鑰和私鑰端仰,公鑰公開,私鑰保留田藐;
發(fā)送方將要發(fā)送的消息采用公鑰加密,得到密文吱七,然后將密文發(fā)送給接收方汽久;
接收方收到密文后,用自己的私鑰進(jìn)行解密踊餐,獲得明文景醇。

1.2.3 RSA工具類
package com.demo.utils;

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 {

    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類用于生成公鑰和私鑰對(duì),基于RSA算法生成對(duì)象
            KeyPairGenerator keyPairGen = null;
            keyPairGen = KeyPairGenerator.getInstance("RSA");
            keyPairGen.initialize(1024, new SecureRandom());
            // 生成一個(gè)密鑰對(duì)吝岭,保存在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: 是自定義異常三痰。另外,搜索公眾號(hào)Java架構(gòu)師技術(shù)后臺(tái)回復(fù)“Spring”窜管,獲取一份驚喜禮包散劫。

@Getter
public class RsaException extends RuntimeException {

    private final String message;

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

}
1.2.4 UT
package com.rosh;


import com.alibaba.fastjson.JSONObject;
import com.demo.utils.RSAUtils;
import org.junit.Test;

import java.util.Map;


/**
 * @Description:
 * @Author: rosh
 * @Date: 2021/10/25 22:30
 */
public class RsaTest {

    /**
     *  用測(cè)試生成的公鑰,私鑰賦值
     */
    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("隨機(jī)生成的公鑰為:" + map.get(RSAUtils.PUBLIC_KEY));
        System.out.println("隨機(jī)生成的私鑰為:" + 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);
    }


}

1.3 案例

SpringCloud Gateway + SpringBoot + Nacos+redis


image.png

image.png
1.3.1 前端登錄代碼

后端把公鑰跟前端約定好:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登錄頁面</title>

</head>
<body>

<h1>登錄</h1>


<from id="from">
    賬號(hào):<input id="username" type="text"/>
    <br/>
    密碼:<input id="password" type="password"/>
    <br/>
    <input id="btn_login" type="button" value="登錄"/>
</from>



<script src="js/jquery.min.js"></script>
<script src="js/jsencrypt.js"></script>


<script type="text/javascript">
    var encrypt = new JSEncrypt();
    encrypt.setPublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFtTlL61IqIGd+fRLUhJ0MjsqFXFJswCohJ45m51WvbxDPRP3gllW0WChk74D5JEOpMDSWo4C7RfoGlBRNW7kQ6qYGukYZ5jgYpzoT0+gp3on96fQXEyQJysv9xiTPIdmSXXVVj1HAOJw29RbzxIVKUSzzPXvEtXRTtCC1+wkAJQIDAQAB");
    $("#btn_login").click(function () {
        const username = $("#username").val();
        const password = $("#password").val();
        const form = {};
        form.username = username;
        form.password = password;
        $.ajax({
            url: "http://localhost:9000/api/user/login",
            data: encrypt.encrypt(JSON.stringify(form)),
            type: "POST",
            dataType: "json",
            contentType: "application/json;charset=utf-8",
            success: function (data) {
                console.log(data);
            }
        });
    })
</script>


</body>
</html>
1.3.2 前端查詢代碼

設(shè)定公鑰幕帆、token获搏,token是登錄成功后返回的值

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>查詢測(cè)試</title>
</head>
<body>


id:<input id="id_txt" type="text"/>
<input id="btn_search" type="button" value="查詢"/>


<script src="js/jquery.min.js"></script>
<script src="js/jsencrypt.js"></script>
<script type="text/javascript">
    var encrypt = new JSEncrypt();
    encrypt.setPublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFtTlL61IqIGd+fRLUhJ0MjsqFXFJswCohJ45m51WvbxDPRP3gllW0WChk74D5JEOpMDSWo4C7RfoGlBRNW7kQ6qYGukYZ5jgYpzoT0+gp3on96fQXEyQJysv9xiTPIdmSXXVVj1HAOJw29RbzxIVKUSzzPXvEtXRTtCC1+wkAJQIDAQAB");

    $("#btn_search").click(function () {
        const id = $("#id_txt").val();
        const param = "id=" + id + "&requestId=" + getUuid();
        encrypt.encrypt(param);
        const url = "http://localhost:9000/api/user/detail?param=" + encrypt.encrypt(param);
        $.ajax({
            url: url,
            beforeSend: function (XMLHttpRequest) {
                XMLHttpRequest.setRequestHeader("token", "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIzYzE1ODczYS1iMGUxLTQyNzctYTRjOS1kYTMwNjdiYmE0NWIiLCJpYXQiOjE2MzUzMDYwMDAsInN1YiI6IntcInBhc3N3b3JkXCI6XCIxMjM0NTZcIixcInVzZXJJZFwiOjEsXCJ1c2VybmFtZVwiOlwiYWRtaW5cIn0iLCJleHAiOjE2MzU1NjUyMDB9.fIQi_cV2ZMszBVFV4GoIpGhCSENQKrDi8DsbArk7mGk");
            },
            type: "GET",
            success: function (data) {
                console.log(data);
            }
        });
    });


    function getUuid() {
        var s = [];
        var hexDigits = "0123456789abcdef";
        for (var i = 0; i < 32; i++) {
            s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
        }
        s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
        s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
        s[8] = s[13] = s[18] = s[23];
        var uuid = s.join("");
        return uuid;
    }

</script>


</body>
</html>
1.3.3 GatewayFilterConfig

解密前端傳來的參數(shù)并修改傳參

package com.demo.gateway.config;

import com.demo.constant.UserConstant;
import com.demo.excepiton.RSAException;
import com.demo.utils.RSAUtils;
import com.demo.utils.TokenUtils;
import io.jsonwebtoken.Claims;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.lang.reflect.Field;
import java.net.URI;


/**
 * @Description:
 * @Author: rosh
 * @Date: 2021/10/26 22:24
 */
@Configuration
@Component
public class GatewayFilterConfig implements GlobalFilter, Ordered {


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1 如果是登錄不校驗(yàn)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 修改請(qǐng)求參數(shù),并獲取請(qǐng)求參數(shù)
        try {
            updateRequestParam(exchange);
        } catch (Exception e) {
            return FilterUtils.invalidUrl(exchange);
        }
        //3 獲取請(qǐng)求體,修改請(qǐng)求體
        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);
        });

        //創(chuàng)建BodyInserter修改請(qǐng)求體
        BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        //創(chuàng)建CachedBodyOutputMessage并且把請(qǐng)求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());
        }));

    }

    /**
     * 修改前端傳的參數(shù)
     */
    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;
    }
}
1.3.4 GateWay 統(tǒng)一異常
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", "無效的請(qǐng)求");
            }

        } else {
            json.put("code", HttpStatus.BAD_REQUEST.value());
            json.put("msg", "未知錯(cuò)誤聯(lián)系管理員");
        }
        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;
        }
    }
}
1.3.5 JAVA業(yè)務(wù)代碼
@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 "賬號(hào)密碼不正確";
    }


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

登錄:返回token


image.png

image.png

查詢:


image.png

2 設(shè)置URL有效時(shí)長(zhǎng)

為了增強(qiáng)URL安全性,前端在header中添加時(shí)間戳失乾。另外常熙,搜索公眾號(hào)頂級(jí)科技后臺(tái)回復(fù)“API”,獲取一份驚喜禮包碱茁。

2.1 前端代碼

在header中添加時(shí)間戳


image.png

2.2 后端驗(yàn)證時(shí)間戳

image.png
 private Long getDateTimestamp(HttpHeaders httpHeaders) {
        List<String> list = httpHeaders.get("timestamp");
        if (CollectionUtils.isEmpty(list)) {
            throw new IllegalArgumentException("拒絕服務(wù)");
        }
        long timestamp = Long.parseLong(list.get(0));
        long currentTimeMillis = System.currentTimeMillis();
        //有效時(shí)長(zhǎng)為5分鐘
        if (currentTimeMillis - timestamp > 1000 * 60 * 5) {
            throw new IllegalArgumentException("拒絕服務(wù)");
        }
        return timestamp;
    }

2.3 測(cè)試不傳時(shí)間戳

image.png

3 確保URL唯一性

確保URL唯一性裸卫,前端請(qǐng)求中增加UUID,后端存入redis纽竣,有效時(shí)長(zhǎng)為5分鐘墓贿,5分鐘重復(fù)提交拒絕服務(wù)

3.1 修改前端請(qǐng)求參數(shù)

image.png

3.2 后端增加驗(yàn)證RequestId

image.png
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;
    }

4 增加簽名

最后一步,添加簽名

4.1 前端增加簽名

跟前端約定好,json數(shù)據(jù)按照ASCII升序排序募壕。

登錄頁面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登錄頁面</title>

</head>
<body>

<h1>登錄</h1>


<from id="from">
    賬號(hào):<input id="username" type="text"/>
    <br/>
    密碼:<input id="password" type="password"/>
    <br/>
    <input id="btn_login" type="button" value="登錄"/>
</from>




<script src="js/jquery.min.js"></script>
<script src="js/jsencrypt.js"></script>
<script src="js/md5.min.js"></script>

<script type="text/javascript">
    var encrypt = new JSEncrypt();
    encrypt.setPublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFtTlL61IqIGd+fRLUhJ0MjsqFXFJswCohJ45m51WvbxDPRP3gllW0WChk74D5JEOpMDSWo4C7RfoGlBRNW7kQ6qYGukYZ5jgYpzoT0+gp3on96fQXEyQJysv9xiTPIdmSXXVVj1HAOJw29RbzxIVKUSzzPXvEtXRTtCC1+wkAJQIDAQAB");
    $("#btn_login").click(function () {
        //表單
        const username = $("#username").val();
        const password = $("#password").val();
        const form = {};
        form.username = username;
        form.password = password;
        //生成簽名调炬,也可以加鹽
        const timestamp = Date.parse(new Date());
        const data = JSON.stringify(sort_ASCII(form));
        const requestId = getUuid();
        const sign = MD5(data + requestId + timestamp);


        $.ajax({
            url: "http://localhost:9000/api/user/login",
            beforeSend: function (XMLHttpRequest) {
                XMLHttpRequest.setRequestHeader("timestamp", timestamp);
                XMLHttpRequest.setRequestHeader("requestId", requestId);
                XMLHttpRequest.setRequestHeader("sign", sign);
            },
            data: encrypt.encrypt(data),
            type: "POST",
            dataType: "json",
            contentType: "application/json;charset=utf-8",
            success: function (data) {
                console.log(data);
            }
        });
    });


    function getUuid() {
        var s = [];
        var hexDigits = "0123456789abcdef";
        for (var i = 0; i < 32; i++) {
            s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
        }
        s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
        s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
        s[8] = s[13] = s[18] = s[23];
        var uuid = s.join("");
        return uuid;
    }

    function sort_ASCII(obj) {
        var arr = new Array();
        var num = 0;
        for (var i in obj) {
            arr[num] = i;
            num++;
        }
        var sortArr = arr.sort();
        var sortObj = {};
        for (var i in sortArr) {
            sortObj[sortArr[i]] = obj[sortArr[i]];
        }
        return sortObj;
    }

</script>


</body>
</html>

4.2 增強(qiáng)讀取Body類

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

    private Map<String, Object> paramMap;

    private Long dateTimestamp;

    private String requestId;

    private String sign;

    public MyCachedBodyOutputMessage(ServerWebExchange exchange, HttpHeaders httpHeaders) {
        super(exchange, httpHeaders);
    }

    public void initial(Map<String, Object> 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;
    }
}

4.3 修改GatewayFilterConfig

package com.demo.gateway.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.common.utils.Md5Utils;
import com.demo.constant.UserConstant;
import com.demo.gateway.pojo.MyCachedBodyOutputMessage;
import com.demo.utils.RSAUtils;
import com.demo.utils.TokenUtils;
import io.jsonwebtoken.Claims;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.lang.reflect.Field;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;


/**
 * @Description:
 * @Author: rosh
 * @Date: 2021/10/26 22:24
 */
@Configuration
@Component
public class GatewayFilterConfig implements GlobalFilter, Ordered {


    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    private static final String ERROR_MESSAGE = "拒絕服務(wù)";


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1 獲取時(shí)間戳
        Long dateTimestamp = getDateTimestamp(exchange.getRequest().getHeaders());
        //2 獲取RequestId
        String requestId = getRequestId(exchange.getRequest().getHeaders());
        //3 獲取簽名
        String sign = getSign(exchange.getRequest().getHeaders());
        //4 如果是登錄不校驗(yàn)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 修改請(qǐng)求參數(shù),并獲取請(qǐng)求參數(shù)
        Map<String, Object> paramMap;
        try {
            paramMap = updateRequestParam(exchange);
        } catch (Exception e) {
            return FilterUtils.invalidUrl(exchange);
        }
        //6 獲取請(qǐng)求體,修改請(qǐng)求體
        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);
        });

        //創(chuàng)建BodyInserter修改請(qǐng)求體

        BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        //創(chuàng)建CachedBodyOutputMessage并且把請(qǐng)求param加入,初始化校驗(yàn)信息
        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())) {
                        //驗(yàn)證簽名
                        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);
        }
    }

    /**
     * 修改前端傳的參數(shù)
     */
    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();
        //有效時(shí)長(zhǎng)為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;
    }
}

4.4 測(cè)試登錄

發(fā)現(xiàn)驗(yàn)簽成功


image.png

4.5 測(cè)試查詢

驗(yàn)簽成功


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市舱馅,隨后出現(xiàn)的幾起案子缰泡,更是在濱河造成了極大的恐慌,老刑警劉巖代嗤,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棘钞,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡干毅,警方通過查閱死者的電腦和手機(jī)宜猜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來硝逢,“玉大人姨拥,你說我怎么就攤上這事∏耄” “怎么了叫乌?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)徽缚。 經(jīng)常有香客問我憨奸,道長(zhǎng),這世上最難降的妖魔是什么凿试? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任排宰,我火速辦了婚禮,結(jié)果婚禮上那婉,老公的妹妹穿的比我還像新娘板甘。我一直安慰自己,他們只是感情好吧恃,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布虾啦。 她就那樣靜靜地躺著,像睡著了一般痕寓。 火紅的嫁衣襯著肌膚如雪傲醉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天呻率,我揣著相機(jī)與錄音硬毕,去河邊找鬼。 笑死礼仗,一個(gè)胖子當(dāng)著我的面吹牛吐咳,可吹牛的內(nèi)容都是我干的逻悠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼韭脊,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼童谒!你這毒婦竟也來了挖腰?” 一聲冷哼從身側(cè)響起径荔,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤昏苏,失蹤者是張志新(化名)和其女友劉穎驶兜,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忆某,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡衡载,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年允乐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了篓吁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茫因。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖杖剪,靈堂內(nèi)的尸體忽然破棺而出冻押,到底是詐尸還是另有隱情,我是刑警寧澤盛嘿,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布翼雀,位于F島的核電站,受9級(jí)特大地震影響孩擂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜箱熬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一类垦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧城须,春花似錦蚤认、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至良瞧,卻和暖如春陪汽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背褥蚯。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工挚冤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赞庶。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓训挡,卻偏偏與公主長(zhǎng)得像澳骤,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子澜薄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容