上週因家裏突發急事,請假一週,故博客沒有正常更新java
RestTemplate是spring框架中自帶的rest客戶端工具類,具備豐富的API,而且在spring cloud中,標記@LoadBalanced註解,能夠實現客戶端負載均衡的rest調用.git
RestTemplate雖然提供了豐富的API,可是這些API過於底層,若是不稍加控制,讓開發人員隨意使用,那後續的代碼也將會變的五花八門,難以維護.spring
同時,當系統規模大了以後,將會有更多的服務,而且服務之間的調用關係也將更加複雜,若是不進行管控治理的話,一樣,項目同期也將愈來愈不可控,app
最後,服務間調用也須要有明確的權限認證機制,最好是能經過配置的方式來明確,哪些服務能夠調用那些服務.從而來把控項目的複雜度.負載均衡
本文將從如下幾點來提供一個解決問題的思路:框架
經過spring boot的@ConfigurationProperties機制來定義遠程服務的元數據,從而實現權限認證的配置化ide
使用HandlerInterceptor來進行攔截,實現權限的驗證spring-boot
定義通用Rms類,來規範RestTemplate的使用工具
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; }
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;
@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;
#定義了一個叫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=文件下載
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) { ....... } }
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();
//判斷環境(開發環境無需校驗) 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"); } } }
@Configuration @ConfigurationProperties(prefix = "org.itkk.rms.config") @Validated public class RmsConfig { //RMS掃描路徑 @NotNull private String rmsPathPatterns; ......... }
@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); } }; }
#攔截路徑 org.itkk.rms.config.rmsPathPatterns=/service/**
@Component public class Rms { //應用名稱 @Value("${spring.application.name}") private String springApplicationName; //restTemplate @Autowired private RestTemplate restTemplate; //配置 @Autowired private RmsProperties rmsProperties;
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>()); }
//構建請求頭 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()); }
//得到文件信息 ResponseEntity<RestResponse<FileInfo>> fileInfo = rms.call("FILE_4", fileParam, null, new ParameterizedTypeReference<RestResponse<FileInfo>>() { }, null);
這樣,規範了遠程服務的調用,只關心接口編號和接口的入參和出參,可以增長溝通效率,而且也有了輕量級的服務治理機制,服務間的調用更可控,到最後,配置文件一拉出來一清二楚.
想得到最快更新,請關注公衆號