我正在参加「启航计划」

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
    SpringCloud实践系列(三):OpenFeign服务调用

可以看到javax.servlet.http包下有个getHeader的办法,可以获得当前浏览器Header中的信息。

  • 如何将token放到跨域请求中

    SpringCloud实践系列(三):OpenFeign服务调用

    在fegin包中的请求拦截器RequestInterceptor有个apply办法,该办法的默许完成如下:

    SpringCloud实践系列(三):OpenFeign服务调用

    可以看到,默许的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

比方咱们在购物的时候,检查产品概况页面的时候,里面包括库存信息,产品概况信息,谈论信息,这个需求包括的微服务如下:

SpringCloud实践系列(三):OpenFeign服务调用
假定现在谈论服务宕机了,那就意味用户发出检查产品请求也无法正常显现了,产品都看不到了,那用户也无法进行下单的操作了。 可是关于用户来说,谈论看不到并不影响他购物,所以这时候咱们应该对谈论服务进行降级处理,返回一个兜底数据(空数据),这样用户的检查产品请求能正常显现,只是谈论数据看不到而已,这样的话,用户的下单请求也不会受到影响.

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;
          }
      }