spring boot / cloud (八) 使用RestTemplate來構建遠程調用服務 前言 上周因家裡突發急事,請假一周,故博客沒有正常更新 RestTemplate介紹: RestTemplate是spring框架中自帶的rest客戶端工具類,具有豐富的API,並且在spring clo ...
spring boot / cloud (八) 使用RestTemplate來構建遠程調用服務
前言
上周因家裡突發急事,請假一周,故博客沒有正常更新
RestTemplate介紹:
RestTemplate是spring框架中自帶的rest客戶端工具類,具有豐富的API,並且在spring cloud中,標記@LoadBalanced註解,可以實現客戶端負載均衡的rest調用.
思路
RestTemplate雖然提供了豐富的API,但是這些API過於底層,如果不稍加控制,讓開發人員隨意使用,那後續的代碼也將會變的五花八門,難以維護.
同時,當系統規模大了之後,將會有更多的服務,並且服務之間的調用關係也將更加複雜,如果不進行管控治理的話,同樣,項目同期也將越來越不可控,
最後,服務間調用也需要有明確的許可權認證機制,最好是能通過配置的方式來明確,哪些服務可以調用那些服務.從而來把控項目的複雜度.
本文將從以下幾點來提供一個解決問題的思路:
通過spring boot的@ConfigurationProperties機制來定義遠程服務的元數據,從而實現許可權認證的配置化
使用HandlerInterceptor來進行攔截,實現許可權的驗證
定義通用Rms類,來規範RestTemplate的使用
實現
1.實現許可權配置
1.定義Application元數據
public class ApplicationMeta implements Serializable {
//ID
private static final long serialVersionUID = 1L;
//服務ID
private String serviceId;
//私鑰
private String secret;
//許可權
private String purview;
//所有服務的調用許可權(優先判定)
private Boolean all = false;
//禁止服務調用
private Boolean disabled = false;
//描述
private String description;
}
2.定義Service元數據
public class ServiceMeta implements Serializable {
//ID
private static final long serialVersionUID = 1L;
//應用名稱
private String owner;
//地址
private String uri;
//服務方法
private String method;
//是否HTTPS
private Boolean isHttps = false;
//描述
private String description;
3.定義RmsProperties類
@Component
@ConfigurationProperties(prefix = "org.itkk.rms.properties")
public class RmsProperties implements Serializable {
//ID
private static final long serialVersionUID = 1L;
//應用清單(應用名稱 : 應用地址)
private Map<String, ApplicationMeta> application;
//服務路徑(服務編號 : 服務元數據)
private Map<String, ServiceMeta> service;
4.在properties文件中進行配置
#定義了一個叫udf-demo(跟spring boot的應用ID一致),設置了私鑰,以及可調用的服務
org.itkk.rms.properties.application.udf-demo.serviceId=127.0.0.1:8080
org.itkk.rms.properties.application.udf-demo.secret=ADSFHKW349546RFSGF
org.itkk.rms.properties.application.udf-demo.purview=FILE_3
org.itkk.rms.properties.application.udf-demo.all=false
org.itkk.rms.properties.application.udf-demo.disabled=false
org.itkk.rms.properties.application.udf-demo.description=sample application
#定義了一個叫FILE_3的服務,後續使用這個服務編號進行調用即可
org.itkk.rms.properties.service.FILE_3.owner=udf-demo
org.itkk.rms.properties.service.FILE_3.uri=/service/file/download
org.itkk.rms.properties.service.FILE_3.method=POST
org.itkk.rms.properties.service.FILE_3.isHttps=false
org.itkk.rms.properties.service.FILE_3.description=文件下載
2.實現許可權校驗
1.定義RmsAuthHandlerInterceptor攔截器
public class RmsAuthHandlerInterceptor implements HandlerInterceptor {
//環境標識
private static final String DEV_PROFILES = "dev";
//配置
@Autowired
private RmsProperties rmsProperties;
//環境變數
@Autowired
private Environment env;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
.......
}
}
2.完善preHandle方法-取出認證信息
String rmsApplicationName = request.getHeader(Constant.HEADER_RMS_APPLICATION_NAME_CODE);
if (StringUtils.isBlank(rmsApplicationName)) {
rmsApplicationName = request.getParameter(Constant.HEADER_RMS_APPLICATION_NAME_CODE);
}
//獲取認證信息(sign)
String rmsSign = request.getHeader(Constant.HEADER_RMS_SIGN_CODE);
if (StringUtils.isBlank(rmsSign)) {
rmsSign = request.getParameter(Constant.HEADER_RMS_SIGN_CODE);
}
//獲取認證信息(服務代碼)
String rmsServiceCode = request.getHeader(Constant.HEADER_SERVICE_CODE_CODE);
if (StringUtils.isBlank(rmsServiceCode)) {
rmsServiceCode = request.getParameter(Constant.HEADER_SERVICE_CODE_CODE);
}
//獲取請求地址
String url = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE).toString();
//獲取請求方法
String method = request.getMethod();
3.完善preHandle方法-校驗
//判斷環境(開發環境無需校驗)
if (!DEV_PROFILES.equals(env.getProperty("spring.profiles.active"))) {
//判斷是否缺少認證信息
if (StringUtils.isBlank(rmsApplicationName) || StringUtils.isBlank(rmsSign)
|| StringUtils.isBlank(rmsServiceCode)) {
throw new AuthException("missing required authentication parameters (rmsApplicationName , rmsSign)");
}
//判斷systemTag是否有效
if (!this.rmsProperties.getApplication().containsKey(rmsApplicationName)) {
throw new AuthException("unrecognized systemTag:" + rmsApplicationName);
}
//獲得應用元數據
ApplicationMeta applicationMeta = rmsProperties.getApplication().get(rmsApplicationName);
//獲得secret
String secret = applicationMeta.getSecret();
//計算sign
String sign = Constant.sign(rmsApplicationName, secret);
//比較sign
if (!rmsSign.equals(sign)) {
throw new AuthException("sign Validation failed");
}
//判斷是否有調用所有服務的許可權
if (!applicationMeta.getAll()) {
//判斷是否禁止調用所有服務許可權
if (applicationMeta.getDisabled()) {
throw new PermissionException(rmsApplicationName + " is disabled");
}
//判斷是否有調用該服務的許可權
if (applicationMeta.getPurview().indexOf(rmsServiceCode) == -1) {
throw new PermissionException("no access to this servoceCode : " + rmsServiceCode);
}
//判斷服務元數據是否存在
if (!rmsProperties.getService().containsKey(rmsServiceCode)) {
throw new PermissionException("service code not exist");
}
//獲得服務元數據
ServiceMeta serviceMeta = rmsProperties.getService().get(rmsServiceCode);
//比較url和method的有效性
if (!serviceMeta.getUri().equals(url) || !serviceMeta.getMethod().equals(method)) {
throw new PermissionException("url and method verification error");
}
}
}
4.定義RmsConfig類
@Configuration
@ConfigurationProperties(prefix = "org.itkk.rms.config")
@Validated
public class RmsConfig {
//RMS掃描路徑
@NotNull
private String rmsPathPatterns;
.........
}
5.定義RmsConfig類-註冊bean
@Bean
@LoadBalanced
RestTemplate restTemplate(ClientHttpRequestFactory requestFactory) {
return new RestTemplate(requestFactory);
}
@Bean
public RmsAuthHandlerInterceptor rmsAuthHandlerInterceptor() {
return new RmsAuthHandlerInterceptor();
}
@Bean
public WebMvcConfigurer rmsAuthConfigurer() { //NOSONAR
return new WebMvcConfigurerAdapter() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
String[] rmsPathPatternsArray = rmsPathPatterns.split(",");
registry.addInterceptor(rmsAuthHandlerInterceptor()).addPathPatterns(rmsPathPatternsArray);
super.addInterceptors(registry);
}
};
}
6.在properties文件中進行配置
#攔截路徑
org.itkk.rms.config.rmsPathPatterns=/service/**
3.實現Rms類
1.定義rms類
@Component
public class Rms {
//應用名稱
@Value("${spring.application.name}")
private String springApplicationName;
//restTemplate
@Autowired
private RestTemplate restTemplate;
//配置
@Autowired
private RmsProperties rmsProperties;
2.定義rms類-call方法
public <I, O> ResponseEntity<O> call(String serviceCode, I input, String uriParam,
ParameterizedTypeReference<O> responseType, Map<String, ?> uriVariables) {
//客戶端許可權驗證
verification(serviceCode);
//構建請求路徑
String path = getRmsUrl(serviceCode);
//獲得請求方法
String method = getRmsMethod(serviceCode);
//拼裝路徑參數
if (StringUtils.isNotBlank(uriParam)) {
path += uriParam;
}
//構建請求頭
HttpHeaders httpHeaders = buildSystemTagHeaders(serviceCode);
//構建請求消息體
HttpEntity<I> requestEntity = new HttpEntity<>(input, httpHeaders);
//請求並且返回
return restTemplate.exchange(path, HttpMethod.resolve(method), requestEntity, responseType,
uriVariables != null ? uriVariables : new HashMap<String, String>());
}
3.定義rms類-其他方法
//構建請求頭
private HttpHeaders buildSystemTagHeaders(String serviceCode) {
String secret = rmsProperties.getApplication().get(springApplicationName).getSecret();
HttpHeaders headers = new HttpHeaders();
headers.add(Constant.HEADER_RMS_APPLICATION_NAME_CODE, springApplicationName);
headers.add(Constant.HEADER_RMS_SIGN_CODE, Constant.sign(springApplicationName, secret));
headers.add(Constant.HEADER_SERVICE_CODE_CODE, serviceCode);
return headers;
}
//客戶端驗證
private void verification(String serviceCode) {
ApplicationMeta applicationMeta = rmsProperties.getApplication().get(springApplicationName);
if (!applicationMeta.getAll()) {
if (applicationMeta.getDisabled()) {
throw new PermissionException(springApplicationName + " is disabled");
}
if (applicationMeta.getPurview().indexOf(serviceCode) == -1) {
throw new PermissionException("no access to this servoceCode : " + serviceCode);
}
}
}
//獲得請求方法
private String getRmsMethod(String serviceCode) {
return rmsProperties.getService().get(serviceCode).getMethod();
}
//構造url
private String getRmsUrl(String serviceCode) {
//獲取服務元數據
ServiceMeta serviceMeta = rmsProperties.getService().get(serviceCode);
//構建請求路徑
StringBuilder url =
new StringBuilder(serviceMeta.getIsHttps() ? Constant.HTTPS : Constant.HTTP);
url.append(rmsProperties.getApplication().get(serviceMeta.getOwner()).getServiceId());
url.append(serviceMeta.getUri());
return url.toString();
}
//計算sign
public static String sign(String rmsApplicationName, String secret) {
final String split = "_";
StringBuilder sb = new StringBuilder();
sb.append(rmsApplicationName).append(split).append(secret).append(split)
.append(new SimpleDateFormat(DATA_FORMAT).format(new Date()));
return DigestUtils.md5Hex(sb.toString());
}
3.客戶端調用
//獲得文件信息
ResponseEntity<RestResponse<FileInfo>> fileInfo = rms.call("FILE_4", fileParam, null,
new ParameterizedTypeReference<RestResponse<FileInfo>>() {
}, null);
代碼倉庫 (博客配套代碼)
結束
這樣,規範了遠程服務的調用,只關心介面編號和介面的入參和出參,能夠增加溝通效率,並且也有了輕量級的服務治理機制,服務間的調用更可控,到最後,配置文件一拉出來一清二楚.
想獲得最快更新,請關註公眾號