过程备注:
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
文章评论