Skip to content
Xiaoyu's Blog
Go back

魔改RestTemplate支持我司参数加密

😀
将请求参数加密下放到基础架构层面,让开发同学无需关心加密策略和算法,只需简单配置即可使用。思路就是对restTemplate能力进行增强,结合springboot提供的扩展点自动注入,开发无感知。

📝 需要支持特性

之前有看过SpringCloud的源码,Ribbon在实现负载均衡策略上的方式对我印象深刻,本次的实现思路和Ribbon在restTemplate上的扩展方式基本一致。

😅 思路

开发使用基础架构提供的RestTemplate → 发起请求 → RestTemplate自定义拦截器 → 加签逻辑 → 发起请求

如何让开发使用我们增强过的RestTemplate的实例呢?

🤪 使用的技术点

1、Spring提供的@Qualifier注解

1.1 @Qualifier注解

学习过Spring基础的同学都会很熟悉这个注解,使用这个注解的大多数的目的是为了区分开我们注入的具体bean。例如:

	@Data
	public class User{
		private String name;
		private String sex;
	}
	
	@Bean("user1")
	User user1() {
		return new User("1", "woman");
	}
	 
	@Bean("user2")
	User user2() {
		return new User("2", "man");
	}

容器里有两个user的bean,当我们想指定某一个bean注入到我们的服务里时,可以使用@Qualifier注解

	public class UserService{
		
		@Autowired
		@Qualifier(value = "user2")
		User user;
	
		public String getUserName(){
			return user.getName();
		}
	
	}

这个是我们常用的方式,其实@Qualifier注解还有一些比较特殊且好用的技巧

上面的两个User的bean实例对象还可以注入到map和list中

	@Autowired(required = false)
	private List<User> users = Collections.emptyList();
		 
    @Autowired(required = false)
    private Map<String,User> userMap = new HashMap<>();

写两个测试用例看下结果:

		@RequestMapping("/users")
    public String users(){
       return JSON.toJSONString(users);
    }


    @RequestMapping("/userMap")
    public String userMap(){
       return JSON.toJSONString(userMap);
    }

结果:

[
    {
        "name":"1",
        "sex":"woman"
    },
    {
        "name":"2",
        "sex":"man"
    }
]

{
    "user1":{
        "name":"1",
        "sex":"woman"
    },
    "user2":{
        "name":"2",
        "sex":"man"
    }
}
	@Bean("user1")
	@Qualifier("valid")
	User user1() {
		return new User("1", "woman");
	}
	
	@Qualifier("invalid")
	@Bean("user2")
	User user2() {
		return new User("2", "man");
	}

我们把测试的controller也加上这个修饰符,看下注入结果是否不一样

    @Autowired(required = false)
    @Qualifier("valid")
    private List<User> users = Collections.emptyList();
 
    @Autowired(required = false)
    @Qualifier("invalid")
    private Map<String,User> userMap = new HashMap<>();
 
    @RequestMapping("/list")
    public Object listUsers() {
       return users;
    }
    @RequestMapping("/map")
    public Object mapUsers() {
       return userMap;
    }

再分别请求,结果:

[
    {
        "name":"1",
        "sex":"woman"
    }
]

{
    "user2":{
        "name":"2",
        "sex":"man"
    }
}

这里可以发现@Qualifier修饰符还有过滤的作用,而SpringCloud中Ribbon获取RestTemplate就是采用了这个小技巧。

1.2 从Ribbon中学到的技巧

在使用springcloud ribbon客户端负载均衡的时候,可以给RestTemplate bean 加一个@LoadBalanced注解,就能让这个RestTemplate在请求时拥有客户端负载均衡的能力

  	@Bean
    @LoadBalanced
    RestTemplate restTemplate() {
       return new RestTemplate();
    }

看下LoadBalanced源码,发现其中并没有什么特殊的地方:

package org.springframework.cloud.client.loadbalancer;
 
import org.springframework.beans.factory.annotation.Qualifier;
 
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
 * @author Spencer Gibb
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

其实主要发挥作用的就是@Qualifier注解了,@Qualifier支持继承,LoadBalanced 有了@Qualifier能力。

搜索@LoadBalanced注解的使用地方,发现只有一处使用了,

在LoadBalancerAutoConfiguration这个自动装配类中:

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
 
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
      final List<RestTemplateCustomizer> customizers) {
   return new SmartInitializingSingleton() {
      @Override
      public void afterSingletonsInstantiated() {
         for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
            for (RestTemplateCustomizer customizer : customizers) {
               customizer.customize(restTemplate);
            }
         }
      }
   };
}
 
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
 
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
      LoadBalancerClient loadBalancerClient) {
   return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
}
 
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
   @Bean
   public LoadBalancerInterceptor ribbonInterceptor(
         LoadBalancerClient loadBalancerClient,
         LoadBalancerRequestFactory requestFactory) {
      return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
   }
 
   @Bean
   @ConditionalOnMissingBean
   public RestTemplateCustomizer restTemplateCustomizer(
         final LoadBalancerInterceptor loadBalancerInterceptor) {
      return new RestTemplateCustomizer() {
         @Override
         public void customize(RestTemplate restTemplate) {
            List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                  restTemplate.getInterceptors());
            list.add(loadBalancerInterceptor);
            restTemplate.setInterceptors(list);
         }
      };
   }
}

这段自动装配的代码的含义不难理解,就是利用了RestTemplate的拦截器,使用RestTemplateCustomizer对所有标注了@LoadBalanced的RestTemplate Bean添加了一个LoadBalancerInterceptor拦截器,而这个拦截器的作用就是对请求的URI进行转换获取到具体应该请求哪个服务实例ServiceInstance。

有以上的知识,我们可以知道:

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

restTemplates这个list能拿到所有加上@LoadBalanced注解的RestTemplate的bean。

1.3 实践

package com.envconfig.common.hps;

import java.lang.annotation.*;
import org.springframework.beans.factory.annotation.Qualifier;

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface HPS {
   
}

  	@Bean
    @HPS
    RestTemplate restTemplate() {
			//bean增强 ... 
       return new RestTemplate();
    }

@RestController
public class HpsTestController {
	  @Autowired
    @HPS
    private RestTemplate restTemplate;

		@RequestMapping("/test")
		public String test(){
				return restTemplate.getForObject("https://www.baidu.com", String.class);
		}
}

2、RestTemplate的扩展

通过使用@HPS注解,我们已经可以让开发同学使用增强后的restTemplate了,那如何对restTemplate进行增强呢?

其实从Ribbon的源码LoadBalancerAutoConfiguration中,我们可以学习到如何去做。

LoadBalancerAutoConfiguration中注入了RestTemplateCustomizer,将自定义的customize逻辑注入到restTemplate中。

RestTemplate的扩展点就是RestTemplateCustomizer,我们可以在其中魔改拿到的restTemplate。

@Bean
   @ConditionalOnMissingBean
   public RestTemplateCustomizer restTemplateCustomizer(
         final LoadBalancerInterceptor loadBalancerInterceptor) {
      return new RestTemplateCustomizer() {
         @Override
         public void customize(RestTemplate restTemplate) {
            List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                  restTemplate.getInterceptors());
            list.add(loadBalancerInterceptor);
            restTemplate.setInterceptors(list);
         }
      };
   }

在customize中,ribbon注入了自定义的拦截器负责负载均衡,那我们也在此自定义一个拦截器对请求参数进行加签即可。

public class HpsRestTemplateCustomizer implements RestTemplateCustomizer, ClientHttpRequestInterceptor {

    public static final Logger log = LoggerFactory.getLogger(HpsRestTemplateCustomizer.class);
    public static final String ALGORITHM__MD5 = "MD5"; // 签名算法, 默认MD5
   
    public HpsRestTemplateCustomizer() {
      
    }

    @Override
    public void customize(RestTemplate restTemplate) {
        List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
        List<ClientHttpRequestInterceptor> list = new ArrayList<>(interceptors);
        list.add(this);
        restTemplate.setInterceptors(list);
    }

    @Override
    public ClientHttpResponse intercept(
            HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution)
            throws IOException {
        Map<String, Object> reqParamsMap = new HashMap<>();
        reqParamsMap = getUrlQueryMap(httpRequest);
        Map<String, Object> postParamsMap = getQueryMap(httpRequest, bytes);
        reqParamsMap.putAll(postParamsMap);
        Map<String, Object> hpsSignedParamsMap = toHpsSignedParamsMap(config, reqParamsMap);
        if ("GET".equalsIgnoreCase(httpRequest.getMethodValue())){
            httpRequest = 加密GET请求参数;
        }else if("POST".equalsIgnoreCase(httpRequest.getMethodValue())) {
            bytes = 加密POST请求参数;
        }
        return clientHttpRequestExecution.execute(httpRequest, bytes);
    

    public static Map<String, Object> getUrlQueryMap(HttpRequest request) {
        String urlQueryString = request.getURI().getQuery();
        Map<String, Object> queryMap = new HashMap<>();
        String[] arrSplit;
        if (urlQueryString == null) {
            return queryMap;
        } else {
            //每个键值为一组
            arrSplit = urlQueryString.split("[&]");
            for (String strSplit : arrSplit) {
                String[] arrSplitEqual = strSplit.split("[=]");
                //解析出键值
                if (arrSplitEqual.length > 1) {
                    queryMap.put(arrSplitEqual[0], arrSplitEqual[1]);
                } else {
                    if (!"".equals(arrSplitEqual[0])) {
                        queryMap.put(arrSplitEqual[0], "");
                    }
                }
            }
        }
        return queryMap;
    }

    public static Map<String, Object> getQueryMap(HttpRequest request, byte[] body) {
        String reqBodyStr = "";
        if (body != null && request.getHeaders().getContentType() != null) {
            String contentType =
                    request.getHeaders().getContentType().toString().toLowerCase();
            if (contentType.contains("application/x-www-form-urlencoded")
                    || contentType.contains("application/json")
                    || contentType.contains("text/xml")) {
                String charset = !contentType.contains("gbk") && !contentType.contains("gb2312") ? "utf-8" : "gbk";
                reqBodyStr = bodyToString(body, charset);
            }
        }
        Map<String, Object> reqMap = new HashMap<>();
        if (StringUtils.isNotBlank(reqBodyStr)){
            reqMap = JsonUtil.toMap(reqBodyStr);
        }
        return reqMap;
    }

    private static String bodyToString(byte[] body, String charset) {
        try {
            return new String(body, charset);
        } catch (Exception e) {
            return new String(body);
        }
    }

}


@Configuration
@ConditionalOnClass(RestTemplate.class)
public class HpsAutoConfiguration {
    //获取被HPS注解修饰过的restTemplate
    @HPS
    @Autowired(required = false)
    private Map<String, RestTemplate> hpsRestTemplates = Collections.emptyMap();

    @Bean
    public SmartInitializingSingleton hpsRestTemplateInitializer(HpsRestTemplateCustomizer hpsRestTemplateCustomizer) {
        return new SmartInitializingSingleton() {
            @Override
            public void afterSingletonsInstantiated() {
                //增强被HPS注解修饰过的restTemplate
                Map<String, RestTemplate> merchantRestTemplates = hpsRestTemplates;
                merchantRestTemplates.forEach((merchantName, restTemplate) -> {
                    if ("hpsRestTemplate".equals(merchantName)) {
													//这里是为了避免将开发自定义的restTemplate也增强了
                        hpsRestTemplateCustomizer.customize(restTemplate);
                    }
                });
            }
        };
    }

    @Bean
    @ConditionalOnMissingBean(name = "hpsRestTemplateCustomizer")
    public HpsRestTemplateCustomizer hpsRestTemplateCustomizer(HpsMerchantConfig config) {
        return new HpsRestTemplateCustomizer(config);
    }
}

3、每次请求都支持单独的超时时间配置

支持加解密的RestTemplate只有一个,但是需要根据不用的请求url有不同的超时时间,那如何进行设置呢?

思路是:

1、在需要自定义 RequsetConfig 的场景,将 RequsetConfig 存储在 ThreadLocal 中

2、在请求时,将自定义的RequsetConfig从ThreadLocal取出来注入到restTemplate

那现在就是寻找RestTemplate对配置的扩展点了,走查了一下午代码发现了HttpComponentsClientHttpRequestFactory,支持我们设置超时时间。

而他默认的SimpleClientHttpRequestFactory则不支持。

HttpComponentsClientHttpRequestFactory#createRequest源码:

public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
        HttpClient client = this.getHttpClient();
        HttpUriRequest httpRequest = this.createHttpUriRequest(httpMethod, uri);
        this.postProcessHttpRequest(httpRequest);
        HttpContext context = this.createHttpContext(httpMethod, uri);
        if (context == null) {
            context = HttpClientContext.create();
        }

        if (((HttpContext)context).getAttribute("http.request-config") == null) {
            RequestConfig config = null;
            if (httpRequest instanceof Configurable) {
                config = ((Configurable)httpRequest).getConfig();
            }

            if (config == null) {
                config = this.createRequestConfig(client);
            }

            if (config != null) {
                ((HttpContext)context).setAttribute("http.request-config", config);
            }
        }

        return (ClientHttpRequest)(this.bufferRequestBody ? new HttpComponentsClientHttpRequest(client, httpRequest, (HttpContext)context) : new HttpComponentsStreamingClientHttpRequest(client, httpRequest, (HttpContext)context));
    }

其中createHttpContext的结果HttpContext就是配置超时时间的地方,但是他默认的createHttpContext是直接返回null的(版本是spring-web-5.2.1.RELEASE)

  	@Nullable
    protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
        return null;
    }

那没办法我们只能继承这个类,自定义一个出来 HpsRequestContextFactory


import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.protocol.HttpContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

import java.net.URI;

public class HpsRequestContextFactory extends HttpComponentsClientHttpRequestFactory {

    @Override
    protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
				//从ThreadLocal中获取到请求配置 requestConfig
        RequestConfig requestConfig = RequestTimeOutConfigHolder.get();
        if (requestConfig != null) {
            HttpContext context = HttpClientContext.create();
            context.setAttribute(HttpClientContext.REQUEST_CONFIG, requestConfig);
            return context;
        }
        return null;
    }
}
  	@Bean
    @ConditionalOnMissingBean(name = {"hpsRequestContextFactory"})
    HpsRequestContextFactory hpsRequestContextFactory() {
        return new HpsRequestContextFactory();
    }

		@Bean
    @HPS
    @ConditionalOnMissingBean(name = {"hpsRestTemplate"})
    RestTemplate hpsRestTemplate(HpsRequestContextFactory hpsRequestContextFactory) {
        return new RestTemplate(clientHttpRequestFactory);
    }
@RestController
public class HpsTestController {
	  @Autowired
    @HPS
    private RestTemplate restTemplate;

		@RequestMapping("/test2")
		public String test(){
				RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(timeout).build();
       RequestConfigHolder.bind(requestConfig);
				return restTemplate.getForObject("https://www.baidu.com", String.class);
		}
}

🤗 总结归纳

这次扩展主要使用了Spring提供的@Qualifier修饰符和RestTemplate的扩展点。其实接到这个需求的时候,我就基本有了思路,很庆幸之前看过SpringCloud的源码。

沉淀是一件非常重要的事情,书到用时方恨少,只有当我们有足够的知识储备后,我们才能写出更优秀的代码,思考到更好的解决方案。

📎 参考文章


Share this post:

Previous Post
一句鸡汤文
Next Post
折腾搭建AIRobot平台