背景
为了增强产品安全性,计划对应用网关进行改造,主要是出入参经过网关时需要进行加解密操作,保证请求数据在网络传输过程中不会泄露或篡改。
考虑到密钥的安全性,每个用户登录都会签发独立的密钥对。同时摒弃了对称加密算法,使用国密非对称的SM2算法进行参数加解密。
网关加解密全流程时序图
难点
先说下开发过程中遇到的一些困难,后面再看代码就知道为什么这么写。
1、网上有价值可供参考的代码不多,这也是为什么要写这边博客的原因,网上现有代码大部分都是互相照搬的,实测过程会发现有很多问题,比如ServerHttpRequestDecorator要重复new很多遍。
2、由于Gateway是基于WebFlux的非阻塞线程模型开发的,在读取RequestBody时可能会出现读取不完整的问题,而且是偶发现象,同样的问题在重写ResponseBody时也会遇到。
3、性能问题,SM2算法是基于bcprov-jdk15on开源库,加解密过程需要对密钥对进行缓存,如果通过16进制字符串进行序列化耗时过长,会造成网关性能瓶颈。
SM2
先说SM2加解密算法这块。
用的是全球最大同性交友网站开源的一个项目,对SM2加解密操作进行了一些封装,项目地址:https://github.com/ZZMarquis/gmhelper
因为每个用户登录时都会签发密钥对,所以每次加解密需要获取用户对应的密钥对后再进行参数加解密操作。
为了避免每次通过密钥对字符串创建密钥对象增加代码执行耗时,用户密钥对使用protostuff序列化为字符串后en缓存在Redis中,需要使用的时候直接从Redis中读取出来反序列化为密钥对象即可,这一步大大提升了代码性能。
该部分代码如下:
pom.xml- <dependency>
- <groupId>org.zz</groupId>
- <artifactId>gmhelper</artifactId>
- <version>1.0.0</version>
- </dependency>
- <dependency>
- <groupId>io.protostuff</groupId>
- <artifactId>protostuff-core</artifactId>
- <version>1.8.0</version>
- </dependency>
- <dependency>
- <groupId>io.protostuff</groupId>
- <artifactId>protostuff-runtime</artifactId>
- <version>1.8.0</version>
- </dependency>
复制代码 密钥对PO对象- public class SM2Key implements Serializable {
- private static final long serialVersionUID = 8273826788748051389L;
- /**
- * 后端加密公钥,对应私钥由前端持有(webPrivateKey)
- */<br><br> private ECPublicKeyParameters serverPublicKey;
- /**
- * 后端解密私钥,对应公钥由前端持有(webPublicKey)
- */
- private ECPrivateKeyParameters serverPrivateKey;
- /**
- * 前端加密公钥,对应私钥由后端持有(serverPrivateKey)
- */
- private String webPublicKey;
- /**
- * 前端解密私钥,对应公钥由后端持有(serverPublicKey)
- */
- private String webPrivateKey;
- public static SM2Key build() {
- return new SM2Key();
- }
- public static SM2Key build(String protostuffHex) {
- final byte[] protostuffBytes = ByteUtils.fromHexString(protostuffHex);
- Schema schema = RuntimeSchema.getSchema(SM2Key.class);
- SM2Key key = RuntimeSchema.getSchema(SM2Key.class).newMessage();
- GraphIOUtil.mergeFrom(protostuffBytes, key, schema);
- return key;
- }
- public ECPublicKeyParameters getServerPublicKey() {
- return serverPublicKey;
- }
- public void setServerPublicKey(ECPublicKeyParameters serverPublicKey) {
- this.serverPublicKey = serverPublicKey;
- }
- public ECPrivateKeyParameters getServerPrivateKey() {
- return serverPrivateKey;
- }
- public void setServerPrivateKey(ECPrivateKeyParameters serverPrivateKey) {
- this.serverPrivateKey = serverPrivateKey;
- }
- public String getWebPublicKey() {
- return webPublicKey;
- }
- public void setWebPublicKey(String webPublicKey) {
- this.webPublicKey = webPublicKey;
- }
- public String getWebPrivateKey() {
- return webPrivateKey;
- }
- public void setWebPrivateKey(String webPrivateKey) {
- this.webPrivateKey = webPrivateKey;
- }
- /**
- * 对象序列化
- * @return
- */
- public String toProtostuffString() {
- LinkedBuffer buffer = LinkedBuffer.allocate();
- try {
- Schema<SM2Key> schema = RuntimeSchema.getSchema(SM2Key.class);
- final byte[] protostuff = GraphIOUtil.toByteArray(this, schema, buffer);
- return ByteUtils.toHexString(protostuff);
- } finally {
- buffer.clear();
- }
- }
- }
复制代码 密钥对签发工具类- public class SM2KeyUtil {
- /**
- * 生成前后端加解密密钥对
- * @return
- */
- public static SM2Key generate() {
- // 构建前后端密钥对
- SM2Key key = SM2Key.build();
- AsymmetricCipherKeyPair keyPair;
- ECPrivateKeyParameters privateKey;
- ECPublicKeyParameters publicKey;
- keyPair = SM2Util.generateKeyPairParameter();
- privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
- publicKey = (ECPublicKeyParameters) keyPair.getPublic();
- // 后端加密所需公钥
- key.setServerPublicKey(publicKey);
- // 前端解密所需私钥
- key.setWebPrivateKey(ByteUtils.toHexString(privateKey.getD().toByteArray()));
- keyPair = SM2Util.generateKeyPairParameter();
- privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
- publicKey = (ECPublicKeyParameters) keyPair.getPublic();
- // 后端解密所需私钥
- key.setServerPrivateKey(privateKey);
- // 前端加密所需公钥
- key.setWebPublicKey(ByteUtils.toHexString(publicKey.getQ().getEncoded(false)));
- return key;
- }
- }
复制代码
通过SM2Key.toProtostuffString()方法获得序列化字符串并写入Redis中
全局拦截器
全局拦截器配置类- /**
- * 网关加解密配置类
- * @author changxy
- */
- @Configuration
- @ConditionalOnProperty(value = "secret.enabled", havingValue = "true", matchIfMissing = true)
- public class SecretConfiguration {
- private static final Logger log = LoggerFactory.getLogger(SecretConfiguration.class);
- /**
- * 免加密接口配置
- */
- public static final String EXCLUDE_PATH_CONFIG_KEY = "#{'${secret.excluded.paths}'.split(',')}";
- /**
- * 注册入参解密全局拦截器
- * @param secretFormatterAdapter 加解密格式化适配器
- * @param decryptRequestBodyFilterFactory 入参解密拦截器工厂,主要为了读取Body
- * @param requestBodyDecryptRewriter RequestBody参数解密RewriteFunction
- * @return
- */
- @Bean
- public DecryptParameterFilter decryptParameterFilter(
- @Autowired SecretFormatterAdapter secretFormatterAdapter,
- @Autowired ModifyRequestBodyGatewayFilterFactory decryptRequestBodyFilterFactory,
- @Autowired RequestBodyDecryptRewriter requestBodyDecryptRewriter
- ) {
- log.info("初始化入参解密全局拦截器");
- return new DecryptParameterFilter(secretFormatterAdapter, decryptRequestBodyFilterFactory, requestBodyDecryptRewriter);
- }
- /**
- * 注册出参加密拦截器
- * !!!!免加密配置项中的接口出参不进行加密处理!!!!
- * @param secretFormatterAdapter 加解密格式化适配器
- * @param encryptFilterFactory 出参加密拦截器工厂,对Content-Type为JSON的响应内容加密处理
- * @param jsonEncryptRewriter ResponseBody参数加密RewriteFunction
- * @param excludedPaths 免加密接口配置
- * @return
- */
- @Bean
- public EncryptResponseFilter encryptResponseFilter(
- @Autowired SecretFormatterAdapter secretFormatterAdapter,
- @Autowired ModifyResponseBodyGatewayFilterFactory encryptFilterFactory,
- @Autowired ResponseJSONEncryptRewriter jsonEncryptRewriter,
- @Value(EXCLUDE_PATH_CONFIG_KEY) List<String> excludedPaths
- ) {
- log.info("初始化出参加密全局拦截器");
- return new EncryptResponseFilter(secretFormatterAdapter, encryptFilterFactory, jsonEncryptRewriter, excludedPaths);
- }
- /**
- * 入参解密拦截器工厂,主要为了读取Body
- * @param secretFormatterAdapter 加解密格式化适配器
- * @return
- */
- @Bean
- RequestBodyDecryptRewriter requestBodyDecryptRewrite(@Autowired SecretFormatterAdapter secretFormatterAdapter) {
- return new RequestBodyDecryptRewriter(secretFormatterAdapter);
- }
- /**
- * 出参加密拦截器工厂,对Content-Type为JSON的响应内容加密处理
- * @param secretFormatterAdapter 加解密格式化适配器
- * @return
- */
- @Bean
- ResponseJSONEncryptRewriter responseJSONEncryptRewriter(@Autowired SecretFormatterAdapter secretFormatterAdapter) {
- return new ResponseJSONEncryptRewriter(secretFormatterAdapter);
- }
- }
复制代码
全局入参解密拦截器,为了安全起见,保留关键代码,部分常量被移除。
DecryptedServerHttpRequestDecorator通过重写URI实现querystring部分参数的解密处理,同时在路由转发前增加secret请求头。
RequestBodyDecryptRewriter是RewriteFunction的实现类,主要读取RequestBody内容进行解密、重写操作,这里使用RewriteFunction可以获取完整的Body内容。- public class DecryptParameterFilter implements GlobalFilter, Ordered {
- private final static Logger log = LoggerFactory.getLogger(DecryptParameterFilter.class);
- /**
- * 加解密序列化适配器
- */
- private final SecretFormatterAdapter secretFormatterAdapter;
- private final ModifyRequestBodyGatewayFilterFactory decryptRequestBodyFilterFactory;
- private final RequestBodyDecryptRewriter requestBodyDecryptRewriter;
- public DecryptParameterFilter(
- SecretFormatterAdapter secretFormatterAdapter,
- ModifyRequestBodyGatewayFilterFactory decryptRequestBodyFilterFactory,
- RequestBodyDecryptRewriter requestBodyDecryptRewriter) {
- this.secretFormatterAdapter = secretFormatterAdapter;
- this.decryptRequestBodyFilterFactory = decryptRequestBodyFilterFactory;
- this.requestBodyDecryptRewriter = requestBodyDecryptRewriter;
- }
- @Override
- public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
- // 读取token
- String token = exchange.getRequest().getHeaders().getFirst(SystemSsoLoginStore.SSO_TOKEN);
- // 通过token在redis读取用户信息
- SystemSsoUser user = SystemSsoLoginHelper.loginCheck(token);
- // 设置用户信息上下文
- exchange.getAttributes().put(SystemSsoTokenFilter.GATEWAY_SSO_USER_ATTR, user);
- // 设置用户上下文对象,用户加解密读取公私钥
- exchange.getAttributes().put(GATEWAY_SSO_USER_KEYS_ATTR, SM2Key.build(user.getSecretKey()));
- return decryptRequestBodyFilterFactory
- .apply(new ModifyRequestBodyGatewayFilterFactory.Config().setRewriteFunction(String.class, String.class, requestBodyDecryptRewriter))
- .filter(new DecryptedServerWebExchangeDecorator(exchange, secretFormatterAdapter), chain);
- }
- @Override
- public int getOrder() {
- // 需要对exchange和request对象进行封装,所以优先级放到最高
- // 优先级过低可能会造成拦截器不生效
- return Ordered.HIGHEST_PRECEDENCE;
- }
- /**
- * ServerHttpRequest解密封装类
- * 1、处理queryString参数解密
- * 2、处理body参数解密
- * @author changxy
- */
- static class DecryptedServerHttpRequestDecorator extends ServerHttpRequestDecorator {
- private ServerWebExchange originExchange;
- private ServerHttpRequest originRequest;
- public DecryptedServerHttpRequestDecorator(ServerWebExchange originExchange, ServerHttpRequest originRequest, SecretFormatterAdapter secretFormatterAdapter) {
- super(originRequest);
- this.originExchange = originExchange;
- this.originRequest = originRequest;
- this.secretFormatterAdapter = secretFormatterAdapter;
- }
- @Override
- public URI getURI() {
- // 获取原始请求链接
- URI uri = super.getURI();
- // 获取原始QueryString请求参数
- MultiValueMap<String, String> originQueryParams = originRequest.getQueryParams();
- // 处理QueryString请求参数解密
- if (Objects.nonNull(originQueryParams) && originQueryParams.containsKey(ENCRYPT_QUERY_STRING_KEY)) {
- // 获取密文
- List<String> encrypted = originQueryParams.get(ENCRYPT_QUERY_STRING_KEY);
- // 非空校验
- if (Objects.nonNull(encrypted) && !encrypted.isEmpty()) {
- UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uri);
- // 清空原有queryString
- uriComponentsBuilder.query(null);
- for (String encrypt : encrypted) {
- // 解密并放入queryString中
- uriComponentsBuilder.query(Sm2Factory.getInstance().decrypt(originExchange, encrypt));
- }
- // build(true) 不会再次进行URL编码
- uri = uriComponentsBuilder.build(true).toUri();
- return uri;
- }
- }
- return super.getURI();
- }
- @Override
- public HttpHeaders getHeaders() {
- HttpHeaders headers = new HttpHeaders();
- headers.putAll(super.getHeaders());
- // 请求微服务应用时添加加解密请求头,门户根据请求头签发证书,前端进行入参加密
- headers.put("secret", Collections.singletonList(Boolean.TRUE.toString()));
- return headers;
- }
- }
- /**
- * ServerWebExchange包装类,这里主要为了包装ServerHttpRequest
- */
- static class DecryptedServerWebExchangeDecorator extends ServerWebExchangeDecorator {
- private final ServerHttpRequestDecorator requestDecorator;
- protected DecryptedServerWebExchangeDecorator(ServerWebExchange delegate, SecretFormatterAdapter secretFormatterAdapter) {
- super(delegate);
- this.requestDecorator = new DecryptedServerHttpRequestDecorator(delegate, delegate.getRequest(), secretFormatterAdapter);
- }
- @Override
- public ServerHttpRequest getRequest() {
- return requestDecorator;
- }
- }
- }
复制代码
RequestBodyDecryptRewriter- public class RequestBodyDecryptRewriter implements RewriteFunction<String, String> {
- protected static final List<MediaType> ENCRYPT_MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_JSON,
- MediaType.APPLICATION_JSON_UTF8,
- MediaType.APPLICATION_FORM_URLENCODED,
- MediaType.valueOf("application/x-www-form-urlencoded;charset=UTF-8"));
- /**
- * 加解密序列化适配器
- */
- private final SecretFormatterAdapter secretFormatterAdapter;
- public RequestBodyDecryptRewriter(SecretFormatterAdapter secretFormatterAdapter) {
- this.secretFormatterAdapter = secretFormatterAdapter;
- }
- @Override
- public Publisher<String> apply(ServerWebExchange exchange, String body) {
- return Mono.just(decryptBody(exchange, body));
- }
- /**
- * 判断是否需要解密处理
- * @param exchange
- * @return
- */
- protected Boolean isEncryptBody(ServerWebExchange exchange) {
- MediaType contentType = exchange.getRequest().getHeaders().getContentType();
- return ENCRYPT_MEDIA_TYPES.contains(contentType);
- }
- protected String decryptBody(ServerWebExchange exchange, String body) {
- if (isEncryptBody(exchange) && StringUtils.hasText(body)) {
- return secretFormatterAdapter.format(exchange, SecretFormatter.SecretFormatterType.DECRYPT, body);
- }
- return body;
- }
- }
复制代码
全局加密拦截器
实现原理和全局解密拦截器类似,这里不再赘述。
EncryptResponseFilter
[code]public class EncryptResponseFilter implements GlobalFilter, Ordered { protected static final List MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8); /** * 加解密序列化适配器 */ private final SecretFormatterAdapter secretFormatterAdapter; private final ModifyResponseBodyGatewayFilterFactory encryptFilterFactory; private final ResponseJSONEncryptRewriter jsonEncryptRewriter; private final List excludedPaths; public EncryptResponseFilter( SecretFormatterAdapter secretFormatterAdapter, ModifyResponseBodyGatewayFilterFactory encryptFilterFactory, ResponseJSONEncryptRewriter jsonEncryptRewriter, List excludedPaths ) { this.secretFormatterAdapter = secretFormatterAdapter; this.encryptFilterFactory = encryptFilterFactory; this.jsonEncryptRewriter = jsonEncryptRewriter; this.excludedPaths = excludedPaths; } @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 不处理免加密接口 if (PathMatcherFactoryInstance.match(excludedPaths, exchange.getRequest().getURI().getPath())) { return chain.filter(exchange); } return chain.filter(exchange.mutate().response(new EncryptServerHttpResponseDecorator(exchange, encryptFilterFactory, jsonEncryptRewriter, chain)).build()); } @Override public int getOrder() { return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1; } /** * ServerHttpResponse封装类 */ static class EncryptServerHttpResponseDecorator extends ServerHttpResponseDecorator { private final ServerHttpResponse serverHttpResponse; private final ModifyResponseBodyGatewayFilterFactory encryptFilterFactory; private final ResponseJSONEncryptRewriter jsonEncryptRewriter; private final ServerWebExchange serverWebExchange; private final GatewayFilterChain chain; public EncryptServerHttpResponseDecorator( ServerWebExchange serverWebExchange, ModifyResponseBodyGatewayFilterFactory encryptFilterFactory, ResponseJSONEncryptRewriter jsonEncryptRewriter, GatewayFilterChain chain ) { super(serverWebExchange.getResponse()); this.serverHttpResponse = serverWebExchange.getResponse(); this.serverWebExchange = serverWebExchange; this.encryptFilterFactory = encryptFilterFactory; this.jsonEncryptRewriter = jsonEncryptRewriter; this.chain = chain; } @Override public Mono writeWith(Publisher |