我正在参加「启航计划」
SpringCloud实践系列(一):Nacos注册中心
Nacos: 注册中心,处理服务注册与发现
SpringCloud实践系列(二):Ribbon负载均衡
Ribbon: 客户端的负载均衡器,处理服务集群的负载均衡
SpringCloud实践系列(三):OpenFeign服务调用
OpenFeign:声明式的HTTP客户端,服务远程调用
SpringCloud实践系列(四):Nacos装备中心
Nacos:装备中心,中心化管理装备文件
SpringCloud实践系列(五):Sentinel流控
Sentinel:微服务流量卫兵,以流量为进口,维护微服务,避免呈现服务雪崩
SpringCloud实践系列(六):Gateway网关(待更新)
Gateway: 微服务网关,服务集群的进口,路由转发以及负载均衡(结合Sentinel)
SpringCloud实践系列(七):Sleuth链路追寻(待更新)
Sleuth: 链路追寻,链路快速整理、故障定位等
SpringCloud实践系列(八):Seata分布式业务(待更新)
Seata: 分布式业务处理计划
在开发 Spring Cloud 微服务的时候,咱们知道,服务之间都是以 HTTP 接口的形式对外供给服务的,因而顾客在进行调用的时候,底层就是经过 HTTP Client 的这种办法进行拜访。当然咱们可以运用JDK原生的 URLConnection、Apache 的 HTTP Client、Netty 异步 Http Client,Spring 的 RestTemplate 去完成服务间的调用。可是最便利、最高雅的办法是经过 Spring Cloud Open Feign 进行服务间的调用 Spring Cloud 对 Feign 进行了增强,使 Feign 支持 Spring Mvc 的注解,并整合了 Ribbon 等,从而让 Feign 的运用更加便利。
接下来就简略讲述一下Feign的入门运用
一、引进依赖及装备编写
- 引进依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>10.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
- 装备文件
可以不配,有默许值
feign:
client:
config:
cloud-goods: # 指定项目
connect-timeout: 1000 # 设置cloud-goods服务的创建连接超时时间为1000ms
read-timeout: 10 # 设置cloud-goods服务的呼应超时时间为10ms
default: # 默许项目
connect-timeout: 1000
read-timeout: 10
- 编写装备类
:一旦编写了装备类,编写接口时,spring的注解就不起作用了(不能运用@GetMapping等注解了),要运用它自己的注解 如 RequestLine。
@Configuration
public class FeignConfiguration {
@Bean
public Contract feignContract() {
return new Contract.Default();
}
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
Decoder feignDecoder() {
return new GsonDecoder();
}
@Bean
Encoder feignEncoder() {
return new GsonEncoder();
}
}
- 在发动类上加上注解
// 敞开 Feign 扫描支持
@EnableFeignClients
二、编写Feign接口及运用
1、编写Feign接口
办法一:第一步编写了装备类的话,只能运用这种,因为编写了装备文件,spring的注解就不起作用了(不能运用@GetMapping等注解了),要运用它自己的注解。否则会报错:not annotated with HTTP method type (ex. GET, POST)
@FeignClient(name = "myApi", url = "http://localhost:8080")
public interface MyService {
// 调用别的一个服务的接口(get传参)
@RequestLine("GET /user/getUsers?searchString={searchString}")
@Headers("Content-Type: application/json")
List<User> getUsers(@Param("searchString") String searchString);
// Post body传参
@RequestLine("POST /user/updateUser")
@Headers("Content-Type: application/json")
@Body("user")
Object updateUser(User user);
}
办法二:在没有编写装备类的情况下运用这种
@FeignClient(name = "myApi", url = "http://localhost:8080")
@RequestMapping("/user")
public interface MyService {
// 调用别的一个服务的接口(get传参)
@GetMapping("/getUsers")
List<User> getUsers(@RequestParam("searchString") String searchString);
// Post body传参
@PostMapping("/updateUser")
Object updateUser(User user);
}
留意:
// 当与nacos结合运用时,这样写是不对的
@FeignClient(name = "myApi", url = "http://cloud-goods")
// 需求这样写
@FeignClient(name = "cloud-goods")
2、运用接口
Feign接口不需求完成类,可直接调用
private MyService myService;
@GetMapping("/userList")
public List<User> getUsers(@RequestParam String searchString){
List<User> userList = myService.getUsers(searchString);
return userList;
}
三、带着token请求
为了安全考虑要拜访的服务的接口需求token验证才能拜访,因而需求带着token才能拜访。
关于新的服务建立安全框架,运用与要拜访的渠道一致的token生成和验证机制,这儿就不赘述了。
1、计划一:直接在@Headers注解中加token
这种计划可以用来测试,因为,这种办法token是写死的,不能依据浏览器带着的token进行验证。
@FeignClient(name = "myApi", url = "http://localhost:8080")
public interface MyService {
@RequestLine("GET /getUsers?searchString={searchString}")
// 直接在@Headers注解中加token
@Headers({"Content-Type: application/json", "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdW..."})
List<User> getUsers(@Param("searchString") String searchString);
}
2、计划二:依据浏览器动态获取token
- 如何从浏览器中拿到token
可以看到javax.servlet.http包下有个getHeader的办法,可以获得当前浏览器Header中的信息。
-
如何将token放到跨域请求中
在fegin包中的请求拦截器RequestInterceptor有个apply办法,该办法的默许完成如下:
可以看到,默许的Authorization是经过用户名和暗码进行base64加密得到的,跟咱们的token生成办法不一样,所以直接运用默许的是无法验证经过的,因而,只需完成RequestInterceptor,重写apply办法即可
-
兼并 编写装备类,完成RequestInterceptor,重写apply办法,把浏览器header拿到的token放进去。
@Slf4j
@Configuration
@AllArgsConstructor
public class NimBusRequestInterceptor implements RequestInterceptor {
private HttpServletRequest req;
private static final String HEADER_STRING = "Authorization";
@Override
public void apply(RequestTemplate requestTemplate) {
// 如果header没有auth头,从cookie获取token
String token = req.getHeader(HEADER_STRING);
Cookie[] cookies = req.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (Objects.equals(cookie.getName(), "token")) {
try {
token = URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
log.error(LogUtil.getStack(e));
}
}
}
}
requestTemplate.header(HEADER_STRING, token);
}
}
OK,以上就完成了Feign基本运用与带着token请求
四、用户名暗码拜访及绕过ssl验证
当咱们拜访一个url时需求用户名暗码、ssl证书验证,那就不是简略的加个@FeignClient注解,填个url了,需求咱们编写装备文件完成
一、装备文件
@Configuration
public class FeignConfiguration {
@Bean
public Contract feignContract() {
return new Contract.Default();
}
// 装备暗码和用户名
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("用户名", "暗码");
}
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
Decoder feignDecoder() {
return new GsonDecoder();
}
@Bean
Encoder feignEncoder() {
return new GsonEncoder();
}
// 装备绕过ssl
@Bean
public Client client() throws NoSuchAlgorithmException,
KeyManagementException {
return new Client.Default(
new NaiveSSLSocketFactory("ip"),
new NaiveHostnameVerifier("ip"));
}
}
二、完成装备文件中的两个类
- NaiveSSLSocketFactory
package com.enmotech.emcs.search.search;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class NaiveSSLSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory sslSocketFactory =
(SSLSocketFactory) SSLSocketFactory.getDefault();
private final SSLContext alwaysAllowSslContext;
private final Set<String> naivelyTrustedHostnames;
public NaiveSSLSocketFactory(String ... naivelyTrustedHostnames)
throws NoSuchAlgorithmException, KeyManagementException {
this.naivelyTrustedHostnames =
Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(naivelyTrustedHostnames)));
alwaysAllowSslContext = SSLContext.getInstance("TLS");
TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
alwaysAllowSslContext.init(null, new TrustManager[] { tm }, null);
}
@Override
public String[] getDefaultCipherSuites() {
return sslSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return sslSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
return (naivelyTrustedHostnames.contains(host))
? alwaysAllowSslContext.getSocketFactory().createSocket(socket, host, port, autoClose)
: sslSocketFactory.createSocket(socket, host, port, autoClose);
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return (naivelyTrustedHostnames.contains(host))
? alwaysAllowSslContext.getSocketFactory().createSocket(host, port)
: sslSocketFactory.createSocket(host, port);
}
@Override
public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException, UnknownHostException {
return (naivelyTrustedHostnames.contains(host))
? alwaysAllowSslContext.getSocketFactory().createSocket(host, port, localAddress, localPort)
: sslSocketFactory.createSocket(host, port, localAddress, localPort);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return (naivelyTrustedHostnames.contains(host.getHostName()))
? alwaysAllowSslContext.getSocketFactory().createSocket(host, port)
: sslSocketFactory.createSocket(host, port);
}
@Override
public Socket createSocket(InetAddress host, int port, InetAddress localHost, int localPort) throws IOException {
return (naivelyTrustedHostnames.contains(host.getHostName()))
? alwaysAllowSslContext.getSocketFactory().createSocket(host, port, localHost, localPort)
: sslSocketFactory.createSocket(host, port, localHost, localPort);
}
}
- NaiveHostnameVerifier
package com.enmotech.emcs.search.search;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class NaiveHostnameVerifier implements HostnameVerifier {
private final Set<String> naivelyTrustedHostnames;
private final HostnameVerifier hostnameVerifier =
HttpsURLConnection.getDefaultHostnameVerifier();
public NaiveHostnameVerifier(String ... naivelyTrustedHostnames) {
this.naivelyTrustedHostnames =
Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(naivelyTrustedHostnames)));
}
@Override
public boolean verify(String hostname, SSLSession session) {
return naivelyTrustedHostnames.contains(hostname) ||
hostnameVerifier.verify(hostname, session);
}
}
五、整合Sentinel
【Sentinel的运用,参考:】《SpringCloud实践:Sentinel流控组件》
5.1、为何整合Sentinel
比方咱们在购物的时候,检查产品概况页面的时候,里面包括库存信息,产品概况信息,谈论信息,这个需求包括的微服务如下: 假定现在谈论服务宕机了,那就意味用户发出检查产品请求也无法正常显现了,产品都看不到了,那用户也无法进行下单的操作了。 可是关于用户来说,谈论看不到并不影响他购物,所以这时候咱们应该对谈论服务进行降级处理,返回一个兜底数据(空数据),这样用户的检查产品请求能正常显现,只是谈论数据看不到而已,这样的话,用户的下单请求也不会受到影响.
5.2、如何整合
-
引进依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2.2.1.RELEASE</version> </dependency>
-
敞开sentinel
# 激活Sentinel对feign的支持 feign: sentinel: enabled: true
-
发动类
// 主发动类添加该注解 @EnableFeignClients
-
运用
编写运用feignClient接口的完成类,并运用fallback参数指定
-
feignClient接口类
sentinel 整合openFeign无法区别降级与异常处理逻辑,都共用一个fallback降级处理办法。
// 指定fallback参数传入自定义的捕获异常的类 @FeignClient(name="Nacos-Server-Name", fallback = MyFeiginClientFallBack.class) public interface ProductFeignClient { @GetMapping(value="/payment/get/{id}") CommonResult getPayment(@PathVariable("id") Long id); @GetMapping("/payment/get/timeout") CommonResult getPaymentTimeout(); }
-
自定义捕获异常的类
编写运用feignClient接口的完成类
@Component public class MyFeignClientFallBack implements ProductFeignClient { @Override public CommonResult getPayment(Long id) { return new CommonResult(500, "OpenFeign--容错---处理",null); } @Override public CommonResult getPaymentTimeout() { return null; } }
-