稷然如此

  • 首页
  • 文章分类
    • AI
    • Android
    • Java
    • Shell
    • Vue
    • C#
    • Python
    • 数据库
    • 组件
    • 其他
    • Game
  • 常用命令
    • Docker
    • Git
    • Linux
  • 操作系统
    • CentOS
    • Ubuntu
    • Windows
    • Kylin
  • 工具
    • IntelliJ IDEA
    • Visual Studio Code
稷然如此
不积跬步,无以至千里
  1. 首页
  2. 文章分类
  3. Java
  4. 正文

Spring Cloud Gateway 网关 SM2 加解密

2023年11月7日 1432点热度 0人点赞

 

过程备注:

1.网关多个 filter 重复调用的问题

比如:负载均衡,跨域设置等过滤器。可在重复调用的重写 filter 中增加判断:

if(ServerWebExchangeUtils.isAlreadyRouted(exchange)){
        return chain.filter(exchange);
}

2.微服务生成的密钥对以json格式保存数据库和Redis,校验token有效性时,优先读取Redis。返回网关的响应报文,直接返回SM2Key对象(从数据库或Redis取出缓存做个反序列化)。网关返回给请求方只是客户端公私钥,而网关的本地缓存则保存整个SM2Key对象(这个对象不需要序列化为响应报文内容),供后续请求加解密处理。

3.刷新token也需要返回新的密钥对。

4.解密过滤器中的GetURI重写方法,会重复调用。因为其他过滤器如果有调用到GetURI,会调用解密过滤器的GetURI,导致重复解密,暂时以“单例”模式的笨办法处理。

5.简单测试了下,不加密和加密请求响应大概在30~50ms左右,未做压力测试,高并发情况下,网关可能存在性能瓶颈。

 

1、引用工具包

引用 Hutool 和加密算法库 Bouncy Castle Crypto
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.8.22</version>
</dependency>

<dependency>
	<groupId>org.bouncycastle</groupId>
	<artifactId>bcprov-jdk15to18</artifactId>
	<version>1.79</version>
</dependency>

2、创建sm2密钥对对象

package com.akim.cloud.framework.common.util.crypto;

import lombok.Data;

/**
 * @author akim
 * @version 1.0
 * @date 2023/3/29 14:10
 * @desc sm2密钥对
 */
@Data
public class SM2Key {
    /**
     * 服务端加密公钥,对应私钥由客户端持有(clientPrivateKey)
     */
    private String serverPublicKey;

    /**
     * 服务端解密私钥,对应公钥由客户端持有(clientPublicKey)
     */
    private String serverPrivateKey;

    /**
     * 客户端加密公钥,对应私钥由服务端持有(serverPrivateKey)
     */
    private String clientPublicKey;

    /**
     * 客户端解密私钥,对应公钥由服务端持有(serverPublicKey)
     */
    private String clientPrivateKey;
}

3、创建sm2工具类

package com.akim.cloud.framework.common.util.crypto;

import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;

/**
 * @author akim
 * @version 1.0
 * @date 2023/3/29 14:10
 * @desc sm2工具类
 */
public class SM2Util {
    /**
     * 生成前后端加解密密钥对
     *
     * @return
     */
    public static SM2Key generate() {
        SM2Key sm2Key = new SM2Key();
        SM2 sm2 = SmUtil.sm2();
        // 设置服务端公钥
        sm2Key.setServerPublicKey(HexUtil.encodeHexStr(BCUtil.encodeECPublicKey(sm2.getPublicKey())));
        // 设置客户端私钥
        sm2Key.setClientPrivateKey(HexUtil.encodeHexStr(BCUtil.encodeECPrivateKey(sm2.getPrivateKey())));
        sm2 = SmUtil.sm2();
        // 设置客户端公钥
        sm2Key.setClientPublicKey(HexUtil.encodeHexStr(BCUtil.encodeECPublicKey(sm2.getPublicKey())));
        // 设置客户端私钥
        sm2Key.setServerPrivateKey(HexUtil.encodeHexStr(BCUtil.encodeECPrivateKey(sm2.getPrivateKey())));
        return sm2Key;
    }

    public static String encrypt(String publicKey, String data) {
        return SmUtil.sm2(null, publicKey)
                .encryptHex(data.getBytes(), KeyType.PublicKey)
                .substring(2); // 去掉04
    }

    public static String decrypt(String privateKey, String data) {
        return SmUtil.sm2(privateKey, null)
                .decryptStr(data.startsWith("04") ? data : "04" + data, KeyType.PrivateKey);
    }

//    public static void main(String[] args) {
//        try {
//            SM2Key sm2Key = SM2Util.generate();
//            String testStr = "我是测试内容";
//            // 服务端加密
//            String enStr = SM2Util.encrypt(sm2Key.getServerPublicKey(), testStr);
//            System.out.println("服务端加密结果:" + enStr);
//            // 客户端解密
//            String deStr = SM2Util.decrypt(sm2Key.getClientPrivateKey(), enStr);
//            System.out.println("客户端解密结果:" + deStr);
//
//            // 客户端加密
//            String enStr2 = SM2Util.encrypt(sm2Key.getClientPublicKey(), testStr);
//            System.out.println("客户端加密结果:" + enStr);
//            // 服务端解密
//            String deStr2 = SM2Util.decrypt(sm2Key.getServerPrivateKey(), enStr2);
//            System.out.println("服务端解密结果:" + deStr2);
//        } catch (Exception e) {
//            System.out.println(e.getMessage());
//        }
//    }
}

4、创建加密配置类

package com.akim.cloud.gateway.common.crypto.config;

import com.akim.cloud.gateway.common.crypto.adapter.CryptoFormatterAdapter;
import com.akim.cloud.gateway.filter.crypto.DecryptFilter;
import com.akim.cloud.gateway.filter.crypto.EncryptFilter;
import com.akim.cloud.gateway.common.crypto.rewrite.RequestRewriter;
import com.akim.cloud.gateway.common.crypto.rewrite.ResponseRewriter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
 * @author akim
 * @version 1.0
 * @date 2023/11/5 23:21
 * @desc 加密配置
 * 后续可以通过配置文件增加加解密类型进行拓展
 */
@Configuration
@ConditionalOnProperty(value = "akim.secret.enabled", havingValue = "true", matchIfMissing = true)
@Slf4j
public class CryptoConfiguration {
    /**
     * 免加密接口配置
     */
    public static final String EXCLUDE_PATH_CONFIG_KEY = "#{'${akim.secret.excluded.paths}'.split(',')}";

    /**
     * 注册入参解密全局拦截器
     * 免加密配置项中的接口出参不进行加密处理
     *
     * @param decryptFilterFactory 入参解密拦截器工厂
     * @param requestRewriter      RequestBody参数解密RewriteFunction
     * @return
     */
    @Bean
    public DecryptFilter decryptParameterFilter(
            @Autowired ModifyRequestBodyGatewayFilterFactory decryptFilterFactory,
            @Autowired RequestRewriter requestRewriter,
            @Value(EXCLUDE_PATH_CONFIG_KEY) List<String> excludedPaths
    ) {
        log.info("初始化入参解密全局拦截器");
        return new DecryptFilter(decryptFilterFactory, requestRewriter, excludedPaths);
    }

    /**
     * 注册出参加密拦截器
     * 免加密配置项中的接口出参不进行加密处理
     *
     * @param encryptFilterFactory 出参加密拦截器工厂,对Content-Type为JSON的响应内容加密处理
     * @param responseRewriter     ResponseBody参数加密RewriteFunction
     * @param excludedPaths        免加密接口配置
     * @return
     */
    @Bean
    public EncryptFilter encryptResponseFilter(
            @Autowired ModifyResponseBodyGatewayFilterFactory encryptFilterFactory,
            @Autowired ResponseRewriter responseRewriter,
            @Value(EXCLUDE_PATH_CONFIG_KEY) List<String> excludedPaths
    ) {
        log.info("初始化出参加密全局拦截器");
        return new EncryptFilter(encryptFilterFactory, responseRewriter, excludedPaths);
    }

    /**
     * 入参解密重写
     *
     * @param cryptoFormatterAdapter 加解密格式化适配器
     * @return
     */
    @Bean
    RequestRewriter requestRewrite(@Autowired CryptoFormatterAdapter cryptoFormatterAdapter) {
        return new RequestRewriter(cryptoFormatterAdapter);
    }

    /**
     * 出参加密拦截器工厂,对Content-Type为JSON的响应内容加密处理
     *
     * @param cryptoFormatterAdapter 加解密格式化适配器
     * @return
     */
    @Bean
    ResponseRewriter responseRewriter(@Autowired CryptoFormatterAdapter cryptoFormatterAdapter) {
        return new ResponseRewriter(cryptoFormatterAdapter);
    }
}

5、配置application.yaml

akim:
  # 是否启用SM2国密全报文加密
  secret:
    enabled: true
    # 免加密接口配置
    excluded:
      # 以“,”逗号分隔
      paths: /api/auth/login

6、创建请求解密入参过滤器

package com.akim.cloud.gateway.filter.crypto;

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.map.MapUtil;
import com.akim.cloud.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.akim.cloud.framework.common.exception.util.ServiceExceptionUtil;
import com.akim.cloud.gateway.common.crypto.CryptoFactory;
import com.akim.cloud.gateway.common.crypto.rewrite.RequestRewriter;
import com.akim.cloud.gateway.util.SecurityFrameworkUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.List;

/**
 * @author akim
 * @version 1.0
 * @date 2023/11/2 15:52
 * @desc 请求解密入参过滤器
 */
@Slf4j
public class DecryptFilter implements GlobalFilter, Ordered {
    private final ModifyRequestBodyGatewayFilterFactory decryptFilterFactory;

    private final RequestRewriter requestRewriter;

    private final List<String> excludedPaths;

    private URI currentUrl;

    public DecryptFilter(
            ModifyRequestBodyGatewayFilterFactory decryptFilterFactory,
            RequestRewriter requestRewriter,
            List<String> excludedPaths) {
        this.decryptFilterFactory = decryptFilterFactory;
        this.requestRewriter = requestRewriter;
        this.excludedPaths = excludedPaths;
    }

    /**
     * 解密请求过滤器
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        /**
         * 1.不处理免加密接口
         * 2.不处理未登录请求(特殊情况:令牌已经过期【code = 401】),DecryptFilter 在 TokenAuthenticationFilter 之后
         */
        if (ListUtil.indexOfAll(excludedPaths, exchange.getRequest().getURI().getPath()::equals).length > 0
                || SecurityFrameworkUtils.getLoginUserCryptoKey(exchange) == null)
            return chain.filter(exchange);

        // 适合 JSON 和 Form 提交的请求
        MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
        if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType))
            return decryptFilterFactory
                    .apply(new ModifyRequestBodyGatewayFilterFactory.Config().setRewriteFunction(String.class, String.class, requestRewriter))
                    .filter(webExchangeDecorator(exchange), chain);

        // get 请求
        return chain.filter(exchange.mutate().request(requestDecorate(exchange)).build());
    }

    /**
     * 排序
     * TokenAuthenticationFilter 校验登录情况之后
     * @return
     */
    @Override
    public int getOrder() {
        // TokenAuthenticationFilter 之后,需要密钥对解密
        // TODO 认证后做解密处理,-99之后的执行的filter在获取URI的时候会重复调用重写方法getURI,导致重复解密,浪费资源,在没有更好的解决方案前,以“单例”模式返回首次解密后的URI
        return -99;
    }

    /**
     * 请求参数拦截
     * @param exchange
     * @return
     */
    private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange) {
        currentUrl = null;
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public URI getURI() {
                // TODO 认证后做解密处理,order = -99之后的执行的 filter 在获取 URI 的时候会重复调用重写方法 getURI,导致重复解密,浪费资源,在没有更好的解决方案前,以“单例”模式返回首次解密后的URI
                if (currentUrl != null) return currentUrl;
                currentUrl = super.getURI();
                // 获取原始QueryString请求参数
                MultiValueMap<String, String> originQueryParams = exchange.getRequest().getQueryParams();
                // 不带参数的请求直接转发
                if (MapUtil.isEmpty(originQueryParams)) return super.getURI();
                // 如果启用了全报文加密,接收到的请求不是约定好的加密请求作异常处理
                if (!originQueryParams.containsKey("p"))
                    throw ServiceExceptionUtil.exception(GlobalErrorCodeConstants.CRYPTO_DECRYPT_ERROR);
                // 处理QueryString请求参数解密
                // 获取密文
                List<String> encrypted = originQueryParams.get("p");
                UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(super.getURI());
                // 清空原有queryString
                uriComponentsBuilder.query(null);

                for (String encrypt : encrypted) {
                    // 解密并放入queryString中
                    uriComponentsBuilder.query(CryptoFactory.getInstance().decryptBySm2(exchange, encrypt));
                }

                // build(true) 不会再次进行URL编码
                currentUrl = uriComponentsBuilder.build(true).toUri();
                return currentUrl;
            }
        };
    }

    /**
     * JSON 和 Form 提交的请求拦截
     * @param delegate
     * @return
     */
    private ServerWebExchangeDecorator webExchangeDecorator(ServerWebExchange delegate) {
        return new ServerWebExchangeDecorator(delegate) {
            @Override
            public ServerHttpRequest getRequest() {
                return requestDecorate(delegate);
            }
        };
    }
}

7、创建响应加密出参过滤器

package com.akim.cloud.gateway.filter.crypto;

import cn.hutool.core.collection.ListUtil;
import com.akim.cloud.gateway.common.crypto.rewrite.ResponseRewriter;
import com.akim.cloud.gateway.util.SecurityFrameworkUtils;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

/**
 * @author akim
 * @version 1.0
 * @date 2023/11/2 15:53
 * @desc 响应加密出参过滤器
 */
public class EncryptFilter implements GlobalFilter, Ordered {
    protected static final List<MediaType> MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8);

    private final ModifyResponseBodyGatewayFilterFactory encryptFilterFactory;

    private final ResponseRewriter responseRewriter;

    private final List<String> excludedPaths;

    public EncryptFilter(
            ModifyResponseBodyGatewayFilterFactory encryptFilterFactory,
            ResponseRewriter responseRewriter,
            List<String> excludedPaths
    ) {
        this.encryptFilterFactory = encryptFilterFactory;
        this.responseRewriter = responseRewriter;
        this.excludedPaths = excludedPaths;
    }

    /**
     * 请求响应加密过滤
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        /**
         * 1.不处理免加密接口
         * 2.不处理未登录请求(特殊情况:令牌已经过期【code = 401】),EncryptFilter 在 TokenAuthenticationFilter 之后
         */
        if (ListUtil.indexOfAll(excludedPaths, exchange.getRequest().getURI().getPath()::equals).length > 0
                || SecurityFrameworkUtils.getLoginUserCryptoKey(exchange) == null)
            return chain.filter(exchange);

        return chain.filter(exchange.mutate().response(decoratedResponse(exchange, chain)).build());
    }

    /**
     * 过滤器顺序
     * 重写 response 必须在 NettyWriteResponseFilter 之前
     * @return
     */
    @Override
    public int getOrder() {
        // 重写 response 必须在 NettyWriteResponseFilter 之前
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
    }

    private ServerHttpResponseDecorator decoratedResponse(ServerWebExchange exchange, GatewayFilterChain chain) {
        return new ServerHttpResponseDecorator(exchange.getResponse()) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                // 只处理返回json格式的响应信息
                if (MEDIA_TYPES.contains(exchange.getResponse().getHeaders().getContentType()) && body instanceof Flux) {
                    // 通过RewriteFunction重写ResponseBody
                    return encryptFilterFactory.apply(new ModifyResponseBodyGatewayFilterFactory.Config().setRewriteFunction(String.class, String.class, responseRewriter)).filter(exchange, chain);
                } else {
                    return super.writeWith(body);
                }
            }

            @Override
            public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
                return writeWith(Flux.from(body).flatMapSequential(publisher -> publisher));
            }
        };
    }
}

8、创建加密类型枚举类

package com.akim.cloud.gateway.common.crypto.enums;

/**
 * @author akim
 * @version 1.0
 * @date 2023/11/5 23:20
 * @desc 加密类型
 */
public enum CryptoFormatterType {
    /**
     * sm2 解密
     */
    SM2_DECRYPT,
    /**
     * sm2 加密
     */
    SM2_ENCRYPT
}

9、创建加密适配器

package com.akim.cloud.gateway.common.crypto.adapter;

import com.akim.cloud.gateway.common.crypto.CryptoFactory;
import com.akim.cloud.gateway.common.crypto.enums.CryptoFormatterType;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;

/**
 * @author akim
 * @version 1.0
 * @date 2023/11/5 23:20
 * @desc 加密适配器
 * 可以在这里处理多种类型加密,比如还有SM3、SM4等等
 */
@Configuration(proxyBeanMethods = false)
public class CryptoFormatterAdapter {
    public String format(ServerWebExchange exchange, CryptoFormatterType type, String body) {
        switch (type) {
            case SM2_DECRYPT:
                return CryptoFactory.getInstance().decryptBySm2(exchange, body);
            case SM2_ENCRYPT:
                return CryptoFactory.getInstance().encryptBySm2(exchange, body);
            default:
                return null;
        }
    }
}

10、创建请求参数重写类

package com.akim.cloud.gateway.common.crypto.rewrite;

import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import com.akim.cloud.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.akim.cloud.framework.common.exception.util.ServiceExceptionUtil;
import com.akim.cloud.gateway.common.crypto.adapter.CryptoFormatterAdapter;
import com.akim.cloud.gateway.common.crypto.enums.CryptoFormatterType;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author akim
 * @version 1.0
 * @date 2023/11/2 15:53
 * @desc 请求参数重写
 */
public class RequestRewriter implements RewriteFunction<String, String> {
    /**
     * 加解密序列化适配器
     */
    private final CryptoFormatterAdapter cryptoFormatterAdapter;

    public RequestRewriter(CryptoFormatterAdapter cryptoFormatterAdapter) {
        this.cryptoFormatterAdapter = cryptoFormatterAdapter;
    }

    @Override
    public Publisher<String> apply(ServerWebExchange exchange, String body) {
        return Mono.just(decryptBody(exchange, body));
    }

    /**
     * 解密请求参数
     * @param exchange
     * @param params 请求参数
     * @return 解密后字符串
     */
    protected String decryptBody(ServerWebExchange exchange, String params) {
        // body不为空且非hex字符串(sm2加密报文),按异常处理
        if (StrUtil.isNotEmpty(params) && !Validator.isHex(params))
            throw ServiceExceptionUtil.exception(GlobalErrorCodeConstants.CRYPTO_DECRYPT_ERROR);
        // 解密
        return cryptoFormatterAdapter.format(exchange, CryptoFormatterType.SM2_DECRYPT, params);
    }
}

11、创建响应内容重写类

package com.akim.cloud.gateway.common.crypto.rewrite;

import com.akim.cloud.gateway.common.crypto.adapter.CryptoFormatterAdapter;
import com.akim.cloud.gateway.common.crypto.enums.CryptoFormatterType;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author akim
 * @version 1.0
 * @date 2023/11/2 15:53
 * @desc 响应内容重写
 */
public class ResponseRewriter implements RewriteFunction<String, String> {
    /**
     * 加解密序列化适配器
     */
    private final CryptoFormatterAdapter cryptoFormatterAdapter;

    public ResponseRewriter(CryptoFormatterAdapter cryptoFormatterAdapter) {
        this.cryptoFormatterAdapter = cryptoFormatterAdapter;
    }

    @Override
    public Publisher<String> apply(ServerWebExchange exchange, String json) {
        return Mono.just(encrypt(exchange, json));
    }

    /**
     * 加密响应报文
     * @param exchange
     * @param json
     * @return
     */
    public String encrypt(ServerWebExchange exchange, String json) {
        return cryptoFormatterAdapter.format(exchange, CryptoFormatterType.SM2_ENCRYPT, json);
    }
}

12、创建加密工厂类

package com.akim.cloud.gateway.common.crypto;

import cn.hutool.core.util.StrUtil;
import com.akim.cloud.framework.common.util.crypto.SM2Key;
import com.akim.cloud.framework.common.util.crypto.SM2Util;
import com.akim.cloud.gateway.util.SecurityFrameworkUtils;
import lombok.SneakyThrows;
import org.springframework.web.server.ServerWebExchange;

/**
 * @author akim
 * @version 1.0
 * @date 2023/11/5 23:24
 * @desc 加密工厂
 */
public class CryptoFactory {
    /**
     * 单例模式
     */
    public static CryptoFactory instance;

    /**
     * 单例模式
     *
     * @return
     */
    public static CryptoFactory getInstance() {
        if (instance == null) return new CryptoFactory();
        return instance;
    }

    /**
     * sm2 解密
     *
     * @param exchange
     * @param data     待解密数据
     * @return 解密后数据
     */
    @SneakyThrows
    public String decryptBySm2(ServerWebExchange exchange, String data) {
        if (StrUtil.isEmpty(data)) return data;
        SM2Key sm2Key = SecurityFrameworkUtils.getLoginUserCryptoKey(exchange);
        return SM2Util.decrypt(sm2Key.getServerPrivateKey(), data);
    }

    /**
     * sm2 加密
     *
     * @param exchange
     * @param data     待加密数据
     * @return 加密后数据
     */
    public String encryptBySm2(ServerWebExchange exchange, String data) {
        if (StrUtil.isEmpty(data)) return data;
        SM2Key sm2Key = SecurityFrameworkUtils.getLoginUserCryptoKey(exchange);
        return SM2Util.encrypt(sm2Key.getServerPublicKey(), data);
    }
}

13、前端Vue2处理参考

import axios from 'axios'
import store from '@/store'
import { getAccessToken, getRefreshToken, setToken, setSM2Key, removeSM2Key, getPrivateKey, getPublicKey } from '@/utils/auth'
import { refreshToken } from "@/api/login"
import { sm2 } from 'sm-crypto'
// 请求队列 
let requestList = []
// 是否正在刷新中 
let isRefreshToken = false
// hex string 正则表达式 
const hexReg = new RegExp("^[a-fA-F0-9]+$");
// 刷新 token 后重发请求需要使用原始请求数据加密后再重发 
let retransConfig = {}
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例 
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分 
  baseURL: '/api/',
  // 超时 
  timeout: 30000,
  // 禁用 Cookie 等信息 
  withCredentials: false,
})
// request拦截器 
service.interceptors.request.use(config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  if (getAccessToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改 
  }
  // 非刷新 token 请求的,记录请求参数信息。刷新token后重发请求,需要新的sm2密钥对加密参数 
  if (config.url.indexOf('refresh-token') === -1) {
    retransConfig.url = config.url; retransConfig.params = config.params;
    retransConfig.data = config.data;
  }
  return encryptRequestParams(config);
}, error => {
  console.log(error)
  Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use(async res => {
  // 配置文件是否开启了sm2全包文加密且校验响应内容是否为hex字符串且私钥存在 
  if (process.env.VUE_APP_CRYPTO_ENABLE === "true" && hexReg.test(res.data) && getPrivateKey()) {
    // 解密出参 json 报文 
    const decryptJson = decryptData(res.data);
    // 转换为对象 
    res.data = JSON.parse(decryptJson);
  }
  // 未设置状态码则默认成功状态 
  const code = res.data.code || 200; if (code === 401) {
    // 如果未认证,并且未进行刷新令牌,说明访问令牌过期了
    if (!isRefreshToken) {
      isRefreshToken = true;
      // 如果获取不到刷新令牌,执行登出操作
      if (!getRefreshToken()) {
        return handleAuthorized();
      }
      // 进行刷新访问令牌
      try {
        // 移除sm2密钥对 
        removeSM2Key();
        const refreshTokenRes = await refreshToken();
        // 刷新成功,则回放队列的请求 + 当前请求 
        setToken(refreshTokenRes.data)
        // 刷新sm2密钥对 
        setSM2Key(refreshTokenRes.data)
        // 回放队列请求
        requestList.forEach(cb => cb())
        // 重发当前请求,使用新的sm2密钥对重新处理请求参数 
        res.config.url = retransConfig.url;
        res.config.params = retransConfig.params;
        res.config.data = retransConfig.data;
        return service(encryptRequestParams(res.config));
      } catch (e) {
        // 刷新失败,只回放队列的请求
        requestList.forEach(cb => cb())
        // 提示是否登出,不回放当前请求,不然会形成递归 
        return handleAuthorized();
      } finally {
        // 清空队列 
        requestList = []
        isRefreshToken = false
      }
    }
    else {
      // 添加到队列,等待刷新获取到新的令牌
      return new Promise(resolve => {
        requestList.push(() => {
          // 设置token 
          res.config.headers['Authorization'] = 'Bearer ' + getAccessToken()
          // 回放请求,使用新的sm2密钥对重新处理请求参数 
          res.config.url = retransConfig.url;
          res.config.params = retransConfig.params;
          res.config.data = retransConfig.data;
          resolve(service(encryptRequestParams(res.config)));
        })
      })
    }
  }
  else {
    return res.data
  }
}, error => {
  // 提示错误信息
  let { message } = error;
  return Promise.reject(error)
})

function handleAuthorized() {
  // 处理重新登录逻辑
}

/**
 * sm2 加密请求参数
 * @param {*} config 
 * @returns config
 */
function encryptRequestParams(config) {
  // get请求映射params参数
  if (config.method === 'get' && config.params) {
    let url = config.url + '?';
    let encryptParams = '';
    for (const propName of Object.keys(config.params)) {
      const value = config.params[propName];
      const part = encodeURIComponent(propName) + '='
      if (value !== null && typeof (value) !== "undefined") {
        if (typeof value === 'object') {
          for (const key of Object.keys(value)) {
            let params = propName + '[' + key + ']';
            const subPart = encodeURIComponent(params) + '='
            url += subPart + encodeURIComponent(value[key]) + "&";
          }
        } else {
          encryptParams += part + encodeURIComponent(value) + "&";
          url += part + encodeURIComponent(value) + "&";
        }
      }
    }
    if (process.env.VUE_APP_CRYPTO_ENABLE === "true" && encryptParams) {
      // 入参加密
      url = config.url + '?p=' + encryptData(encryptParams.slice(0, -1));
    } else {
      url = url.slice(0, -1);
    }
    config.params = {};
    config.url = url;
  }
  else if (process.env.VUE_APP_CRYPTO_ENABLE === "true" && getPublicKey()) {
    // sm2 加密请求内容
    if ((config.method === 'post' || config.method === 'put') && config.data) {
      config.data = encryptData(JSON.stringify(config.data));
    }
    else if ((config.method === 'delete' && config.url.indexOf('?') !== -1)
      || (config.method === 'get'
        && !config.params
        && config.url.indexOf('?') !== -1
        && config.url.indexOf('refresh-token') === -1)) {
      // api 地址
      let url = config.url.slice(0, config.url.indexOf('?'));
      // 参数
      let params = config.url.slice(config.url.indexOf('?') + 1);
      config.url = url + '?p=' + encryptData(params);
    }
  }
  return config
}

/** 
 * 加密请求数据 
 * @param {*} data
 *  @returns 
 */
function encryptData(data) {
  return sm2.doEncrypt(data, getPublicKey());
}

/** 
 * 解密响应数据 
 *  @param {*} data 
 *  @returns 
 */
function decryptData(data) {
  return sm2.doDecrypt(data, getPrivateKey())
}
export default service

 

标签: gateway sm2 Spring Spring Cloud Spring Cloud Gateway 全报文加密
最后更新:2024年11月7日

Akim

犇 骉 Java、C#、Python、Go、Android、MiniProgram、Bootstrap、Vue2

点赞
< 上一篇
下一篇 >
文章目录
  • 1、引用工具包
  • 2、创建sm2密钥对对象
  • 3、创建sm2工具类
  • 4、创建加密配置类
  • 5、配置application.yaml
  • 6、创建请求解密入参过滤器
  • 7、创建响应加密出参过滤器
  • 8、创建加密类型枚举类
  • 9、创建加密适配器
  • 10、创建请求参数重写类
  • 11、创建响应内容重写类
  • 12、创建加密工厂类
  • 13、前端Vue2处理参考

Copyright © 2025 aianran.com All Rights Reserved.

免责申明 | 隐私政策 | 服务条款 | 关于我们

黔ICP备2023008200号-1

贵公网安备 52010202003594号