Home | 简体中文 | 繁体中文 | 杂文 | 知乎专栏 | 51CTO学院 | CSDN程序员研修院 | Github | OSChina 博客 | 腾讯云社区 | 阿里云栖社区 | Facebook | Linkedin | Youtube | 打赏(Donations) | About
知乎专栏多维度架构

1.21. 多维度架构之微服务

1.21.1. 微服务安全吗?

微服务安全吗?其实存在很多隐患,常规的做法是将微服务置于私有局域网中,通过网关报漏服务。如果破坏者一旦进入了你的私有局域网中,微服务是及其危险的。

1.21.1.1. 配置中心的隐患

配置中心的安全隐患

配置中心有以下几种安全隐患

  1. 配置中心报漏在公网IP之下
  2. 配置中心没有做用户验证
  3. 配置文件中存在敏感信息
  4. 明文传输内容

配置有泄漏敏感信息的隐患,你的配置中心是不是也这样?

			
iMac:workspace neo$ curl http://localhost:8888/netkiller-dev-master.json
{"sms":{"gateway":{"url":"https://sms.netkiller.cn/v1","username":"netkiller","password":"123456"}}}
			
			

给配置中心增加SSL和HTTP认证,可以让配置中心更安全。

			
iMac:resources neo$ curl -i -k https://config:s3cr3t@localhost:8888/netkiller-dev.json
HTTP/2 200 
set-cookie: JSESSIONID=9E77660C8DC7669121C8D122A48D8737; Path=/; Secure; HttpOnly
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
strict-transport-security: max-age=31536000 ; includeSubDomains
x-frame-options: DENY
content-type: application/json
content-length: 100
date: Mon, 07 Sep 2020 08:24:39 GMT

{"sms":{"gateway":{"url":"https://sms.netkiller.cn/v1","username":"netkiller","password":"123456"}}}	
			
			

我们将 HTTP2 SSL 应用在配置中心后,就不担心配置文件被嗅探器抓到。

1.21.1.2. 注册中心的隐患

注册中心一不小心就被公网IP报漏出去,甚至有被恶意注册的风险。

注册中心有以下几种安全隐患

  1. 注册中心没有做用户验证,任何人都能访问
  2. 注册中心报漏在公网IP之下,被恶意注册的风险。
  3. 从openfeign 访问 euerka server 明文传输内容

你的注册中心是不是这样的?

			
iMac:workspace neo$ curl http://localhost:8761/eureka/apps
<applications>
  <versions__delta>1</versions__delta>
  <apps__hashcode>UP_1_</apps__hashcode>
  <application>
    <name>WEBFLUX</name>
    <instance>
      <instanceId>192.168.3.85:webflux</instanceId>
      <hostName>192.168.3.85</hostName>
      <app>WEBFLUX</app>
      <ipAddr>192.168.3.85</ipAddr>
      <status>UP</status>
      <overriddenstatus>UNKNOWN</overriddenstatus>
      <port enabled="true">8080</port>
      <securePort enabled="false">443</securePort>
      <countryId>1</countryId>
      <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
        <name>MyOwn</name>
      </dataCenterInfo>
      <leaseInfo>
        <renewalIntervalInSecs>30</renewalIntervalInSecs>
        <durationInSecs>90</durationInSecs>
        <registrationTimestamp>1599106511367</registrationTimestamp>
        <lastRenewalTimestamp>1599106931380</lastRenewalTimestamp>
        <evictionTimestamp>0</evictionTimestamp>
        <serviceUpTimestamp>1599106511367</serviceUpTimestamp>
      </leaseInfo>
      <metadata>
        <management.port>8080</management.port>
      </metadata>
      <homePageUrl>http://192.168.3.85:8080/</homePageUrl>
      <statusPageUrl>http://192.168.3.85:8080/actuator/info</statusPageUrl>
      <healthCheckUrl>http://192.168.3.85:8080/actuator/health</healthCheckUrl>
      <vipAddress>webflux</vipAddress>
      <secureVipAddress>webflux</secureVipAddress>
      <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
      <lastUpdatedTimestamp>1599106511368</lastUpdatedTimestamp>
      <lastDirtyTimestamp>1599106511299</lastDirtyTimestamp>
      <actionType>ADDED</actionType>
    </instance>
  </application>
</applications>			
			
			

经过安全加固后

Eureka Web 界面进入需要输入用户名和密码,HTTP2 SSL 加密传输页面内容。

https://localhost:8761

			
iMac:resources neo$ curl -k https://eureka:s3cr3t@localhost:8761/eureka/apps
<applications>
  <versions__delta>1</versions__delta>
  <apps__hashcode>UP_2_</apps__hashcode>
  <application>
    <name>MICROSERVICE-RESTFUL</name>
    <instance>
      <instanceId>192.168.3.85:microservice-restful:8081</instanceId>
      <hostName>192.168.3.85</hostName>
      <app>MICROSERVICE-RESTFUL</app>
      <ipAddr>192.168.3.85</ipAddr>
      <status>UP</status>
      <overriddenstatus>UNKNOWN</overriddenstatus>
      <port enabled="true">8081</port>
      <securePort enabled="false">443</securePort>
      <countryId>1</countryId>
      <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
        <name>MyOwn</name>
      </dataCenterInfo>
      <leaseInfo>
        <renewalIntervalInSecs>30</renewalIntervalInSecs>
        <durationInSecs>90</durationInSecs>
        <registrationTimestamp>1599532959290</registrationTimestamp>
        <lastRenewalTimestamp>1599533499404</lastRenewalTimestamp>
        <evictionTimestamp>0</evictionTimestamp>
        <serviceUpTimestamp>1599532959290</serviceUpTimestamp>
      </leaseInfo>
      <metadata>
        <management.port>8081</management.port>
      </metadata>
      <homePageUrl>http://192.168.3.85:8081/</homePageUrl>
      <statusPageUrl>http://192.168.3.85:8081/actuator/info</statusPageUrl>
      <healthCheckUrl>http://192.168.3.85:8081/actuator/health</healthCheckUrl>
      <vipAddress>microservice-restful</vipAddress>
      <secureVipAddress>microservice-restful</secureVipAddress>
      <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
      <lastUpdatedTimestamp>1599532959291</lastUpdatedTimestamp>
      <lastDirtyTimestamp>1599532959204</lastDirtyTimestamp>
      <actionType>ADDED</actionType>
    </instance>
  </application>
  <application>
    <name>OPENFEIGN</name>
    <instance>
      <instanceId>192.168.3.85:openfeign:8088</instanceId>
      <hostName>192.168.3.85</hostName>
      <app>OPENFEIGN</app>
      <ipAddr>192.168.3.85</ipAddr>
      <status>UP</status>
      <overriddenstatus>UNKNOWN</overriddenstatus>
      <port enabled="true">8088</port>
      <securePort enabled="false">443</securePort>
      <countryId>1</countryId>
      <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
        <name>MyOwn</name>
      </dataCenterInfo>
      <leaseInfo>
        <renewalIntervalInSecs>30</renewalIntervalInSecs>
        <durationInSecs>90</durationInSecs>
        <registrationTimestamp>1599533216972</registrationTimestamp>
        <lastRenewalTimestamp>1599533517001</lastRenewalTimestamp>
        <evictionTimestamp>0</evictionTimestamp>
        <serviceUpTimestamp>1599533216972</serviceUpTimestamp>
      </leaseInfo>
      <metadata>
        <management.port>8088</management.port>
      </metadata>
      <homePageUrl>http://192.168.3.85:8088/</homePageUrl>
      <statusPageUrl>http://192.168.3.85:8088/actuator/info</statusPageUrl>
      <healthCheckUrl>http://192.168.3.85:8088/actuator/health</healthCheckUrl>
      <vipAddress>openfeign</vipAddress>
      <secureVipAddress>openfeign</secureVipAddress>
      <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
      <lastUpdatedTimestamp>1599533216972</lastUpdatedTimestamp>
      <lastDirtyTimestamp>1599533216920</lastDirtyTimestamp>
      <actionType>ADDED</actionType>
    </instance>
  </application>
</applications>			
			
			

1.21.1.3. Eureka 客户端

Eureka Client 的安全配置与Eureka Server/Config Server 类似

Eureka 客户端有以下几种安全隐患

  1. 服务报漏在公网IP之下,任何人都不经过 Eureka Server 和 Openfeign 绕开后直接访问服务
  2. 明文传输内容

我们给 Eureka Client 增加 HTTP/2 SSL 然后再注册到 Eureka Server,我通常会关闭 Eureka Client 端口,只保留 SSL 端口。

			
iMac:Architect neo$ curl -k https://eureka:s3cr3t@localhost:8761/eureka/apps 
<applications>
  <versions__delta>1</versions__delta>
  <apps__hashcode>UP_2_</apps__hashcode>
  <application>
    <name>MICROSERVICE-RESTFUL</name>
    <instance>
      <instanceId>192.168.3.85:microservice-restful:8081</instanceId>
      <hostName>192.168.3.85</hostName>
      <app>MICROSERVICE-RESTFUL</app>
      <ipAddr>192.168.3.85</ipAddr>
      <status>UP</status>
      <overriddenstatus>UNKNOWN</overriddenstatus>
      <port enabled="false">8081</port>
      <securePort enabled="true">8081</securePort>
      <countryId>1</countryId>
      <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
        <name>MyOwn</name>
      </dataCenterInfo>
      <leaseInfo>
        <renewalIntervalInSecs>30</renewalIntervalInSecs>
        <durationInSecs>90</durationInSecs>
        <registrationTimestamp>1599547853553</registrationTimestamp>
        <lastRenewalTimestamp>1599548033559</lastRenewalTimestamp>
        <evictionTimestamp>0</evictionTimestamp>
        <serviceUpTimestamp>1599547853554</serviceUpTimestamp>
      </leaseInfo>
      <metadata>
        <management.port>8081</management.port>
      </metadata>
      <homePageUrl>http://192.168.3.85:8081/</homePageUrl>
      <statusPageUrl>http://192.168.3.85:8081/actuator/info</statusPageUrl>
      <healthCheckUrl>http://192.168.3.85:8081/actuator/health</healthCheckUrl>
      <secureHealthCheckUrl>https://192.168.3.85:8081/actuator/health</secureHealthCheckUrl>
      <vipAddress>microservice-restful</vipAddress>
      <secureVipAddress>microservice-restful</secureVipAddress>
      <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
      <lastUpdatedTimestamp>1599547853554</lastUpdatedTimestamp>
      <lastDirtyTimestamp>1599547853483</lastDirtyTimestamp>
      <actionType>ADDED</actionType>
    </instance>
  </application>
  <application>
    <name>OPENFEIGN</name>
    <instance>
      <instanceId>192.168.3.85:openfeign:8088</instanceId>
      <hostName>192.168.3.85</hostName>
      <app>OPENFEIGN</app>
      <ipAddr>192.168.3.85</ipAddr>
      <status>UP</status>
      <overriddenstatus>UNKNOWN</overriddenstatus>
      <port enabled="true">8088</port>
      <securePort enabled="true">8088</securePort>
      <countryId>1</countryId>
      <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
        <name>MyOwn</name>
      </dataCenterInfo>
      <leaseInfo>
        <renewalIntervalInSecs>30</renewalIntervalInSecs>
        <durationInSecs>90</durationInSecs>
        <registrationTimestamp>1599547953476</registrationTimestamp>
        <lastRenewalTimestamp>1599547953476</lastRenewalTimestamp>
        <evictionTimestamp>0</evictionTimestamp>
        <serviceUpTimestamp>1599547953476</serviceUpTimestamp>
      </leaseInfo>
      <metadata>
        <management.port>8088</management.port>
      </metadata>
      <homePageUrl>http://192.168.3.85:8088/</homePageUrl>
      <statusPageUrl>http://192.168.3.85:8088/actuator/info</statusPageUrl>
      <healthCheckUrl>http://192.168.3.85:8088/actuator/health</healthCheckUrl>
      <secureHealthCheckUrl>https://192.168.3.85:8088/actuator/health</secureHealthCheckUrl>
      <vipAddress>openfeign</vipAddress>
      <secureVipAddress>openfeign</secureVipAddress>
      <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
      <lastUpdatedTimestamp>1599547953476</lastUpdatedTimestamp>
      <lastDirtyTimestamp>1599547953435</lastDirtyTimestamp>
      <actionType>ADDED</actionType>
    </instance>
  </application>
</applications>			
			
			

从上面配置中可以看到 port 已经禁用,也就意味着无法再通过 http:// 访问,securePort 是启用状态,只接受 https:// 访问。

			
      <port enabled="false">8081</port>
      <securePort enabled="true">8081</securePort>			
			
			

最好还要设置防火墙,只允许 Eureka Server 才能访问 Eureka Client。防止通过其他服务做为跳板,进入局域网,直接访问 Eureka Client。

1.21.1.4. 最终总结

为了防止不小心 公网IP保留微服务,我们需要将实例与局域网IP地址绑定,这样服务只能从局域网IP访问,即使服务器映射了公网IP地址也不用担心。

禁用 HTTP 访问,全部改为 HTTPS 访问

			
H5 / App 
    ^
    |
 HTTP2 ssl
    |
    V		
Openfeign <--- HTTP2 ssl ---> Eureka Server
                                  ^
                                  |
                                  |
                              HTTP2 ssl
                                  |
                                  |
                                  V
                             Eureka Client  <--- HTTP2 ssl ---> Config Server
			
			

如果可以,尽量为节点增加用户认证。

1.21.2. 熔断器解决了什么问题?

1.21.3. 微服务的性能

1.21.3.1. 微服务的开销

1.21.4. 多维度架构之微服务拆分

最近在群里有人问关闭分布式事务的话题,详细听了他们需求后。我呵呵一笑,大约在15年前我就遇到过这种问题。

起因是这样的,这是一个电商系统,架构师给出的架构是这样的:

		
用户中心:负责用户注册,登录,用户信息,钱包管理……
商品中心:负责商品的管理,包括展示,价格和库存管理……
广告中心:负责商品推广,促销……
物流中心:负责订单的物流……
等等中心:负责等等……		
		
		

每个中心都有一个独立域名例如:

		
user.domain.com
product.domain.com
ad.domain.com
search.domain.com
m.domain.com
……		
		
		

这种架构设计会存在一个问题,用户每下一个订单,都需要连接多个中心,做一连串调用,最终完成下订单这个功能。因为用户可能操作过程中终止购物流程,或者不可抗因素导致流程无法继续。为此需要设计了一种分布式事务系统,用来解决事务回滚的问题。

所谓的分布式事务,是指跨服务器实现数据库生成与回滚操作,例如:用户购物,浏览商品,添加购物车,选择物流方式…… 这些数据产生在不同服务器上,如果用户取消订单,数据将依次反向回滚。

无独有偶,另一个跨境电商公司的同事也遇到了这种问题,苦苦找不到解决方案,想起了我,询问我的意见。

有时候你会发现,人们会陷入思维边界的陷阱,全力以赴在错误的方向上,无法自拔。

首先,划分中心的架构思维,之所以会出现这种划分方法,我认为跟我们的教育方式有关,导致了多数人都会沿用这种思维定式。

其次,分布式服务的确能解决他们遇到的问题,能想到分布式事务,证明他们智商没有问题。但分布式事务不是最优解,是最差解决方案。

最后,出现了南辕北辙,在错误方向的道路上越走越远。

1.21.4.1. 分布式事务之路

大约在15年前我们也遇到了这个问题,幸好我及时出手纠正了架构设计的误区,从而没有走上分布式事务之路。

那时还没有微服务的概念,也没有容器技术,我们主要使用物理服务器,在服务器上运行多个实例。从BAT高薪挖来的架构师的思路跟上面一样,将应用划分成各种中心,并且要求每个中心都部署在独立物理机上。划分中心这种方式也与当时的开发模式有关,采用敏捷开发,分成多个小组,每个小组负责一个中心,小组间定义好信通接口,然后所有小组马力全开,活就一起开干了。现在想想简单又粗暴,就如人体器官一样,五脏六腑的联系不是通过一条神经实现的,他们的联系十分复杂。所以我们不能单独思考每个中心,然后就认为把它们合起来就是一个整体。

如果再继续下去,我们一定会去研发分布式事务。

此时有一个更好的机会等着我,于是我提出了离职申请,反正是准备离开了,也不怕得罪人,我想我应该在离职之前把这些问题跟公司说一下。

1.21.4.2. 微服务拆分法则

我向公司反映了目前面临的所有问题,并且提出了两个概念:

  1. 基于工作流拆分服务:服务的拆分法则,基于工作流拆分服务,确保该工作流运行在一个实例中。
  2. 服务器即是服务池:所有物理机应该是一个服务池,根据我们的需求,可以将它部署成任何服务。
1.21.4.2.1. 基于工作流拆分服务

上面提出的两点,直到今天也仍然适用,例如在微服务的拆分中。在我的职业生涯中,这两个概念始终在指导我的团队。下面我详细说明两个概念怎样应用到实际的工作中。

我们还以电商系统举例,用户下单购物的工作流,如果是按照中心划分,流程可能是这样的:

				
用户 —> 商品中心(浏览) —>  搜索中心(过滤)—> 用户中心(添加购物车)—> 物流中心 (物流方式) —> 结算中心(支付结算/扣积分)—> 商品中心(扣库存)—> 用户中心 (完成)
				
				

数据流在,商品中心,搜索中心,用户中心…… 服务器中不断传递,网络延迟,网络超时,网络故障等等任何错误都可能影响用户体验。

如果是运行在一个实例中呢?确切的说,我们需要让工作流运行在一个服务器上,一个CPU、内存和硬盘上。这样就没有分布式事务的需求了,数据库的事务处理解决了所有的问题,就这么简单!!!

基于这种法则,我们将几套工作流归类,放在一个实例中,放在今天就是微服务。同样微服务的拆分也尽量满足一套工作流在一个微服务客户端上,避免请求过程出现,一个微服务调用另一个微服务的情况。

1.21.4.2.2. 服务池的概念

服务器即是服务池的意思是,做到服务于服务器无关,与IP地址无关,通过运维手段,可以将服务器部署成任何服务,这样可以最大化利用硬件资源,不至于一些服务器资源闲置,而另一些则满负荷工作。这样更容易调配服务器资源。

这种概念在今天的容器中得到了更好的实现。

1.21.4.3. 最后总结

还是那句话架构设计是做减法,不是堆技术。你需要从整体考虑,整体不等于个体的总和,这就是多维度思维。

分布式事务目前有成熟的解决方案,但是能不用在高并发,长工作流的需求上,这种方案增加了系统的复杂度。导致开发复杂,测试难度大,运维难,故障多。

1.21.5. 接口安全

1.21.5.1. Restful 安全问提

Restful 的通信安全有很多中解决方案,例如

  1. HTTP Basic Auth 认证

  2. Cooke / Session 认证

  3. Token 认证

  4. Oauth / OpenID

等等,每一种方案都很成熟,这里不依依解释,如果不了解,请去搜索引擎查找相关资料。这里我谈谈在实施微服务项目中的心得,首先项目采用 Spring cloud 方案,Spring cloud 有自己的RestController 控制器,我们需要遵循他的规范开发,这就限制了很多传统的认证加密方法不能应用到 Spring cloud中。

例如传统restful 使用 POST 方式提交,POST 数据格式如下:

		
name=Neo&age=23&md5=xxxxxxx
		
			

然后做 token 校验。

而 Spring cloud 使用 raw 格式的数据做POST提交,例如

				@RequestMapping(value = "/member/create", method =
				RequestMethod.POST)
				public void create(@RequestBody Member member)
			

我们不想在Spring框架上做额外的改动,又想解决信息的安全问题。

1.21.5.2. 第一个阶段采用 HTTP Basic Auth

这个方案简单,实施起来最为方便,因为项目比较紧急,所以就采用了这个方案,这个方案既可以在运维方处理,也可以在开发方处理,对于 Spring boot 只需引入Spring Security 简单配置,立即生效。

实现方式请参考: Spring boot with Spring security

1.21.5.3. 第二阶段 HTTP Basic Auth + SSL

上面的方案适合在防火墙内部的服务器间通信,如果跨机房或者在广域网上就不在安全了,通过嗅探器抓包,包括 http basic auth 的用户和密码,以及接口数据没有安全可言。 为Web 服务器增加 SSL 证书,可以解决信息安全问提。

证书可以使用CA机构颁发的证书,也可以自己生成证书。

证书可以配置在Web服务器上如Nginx, 实现方式请参考: http://www.netkiller.cn/www/nginx/conf.html#http2 《Netkiller Web 手札》

也可以配置在 Spring boot 中, 实现方式请参考: Spring boot with HTTPS SSL

这个方案可以满足绝大部分用户的需求。

1.21.5.4. 第三阶段 HTTP2 + HTTP Basic Auth + Oauth2

由于需要为手机端提供 restful 服务,之前的方式已经不能满足我们的需求,之前的方式更适合提供私有服务,不适合提供公共服务。所谓私有服务是指它的使用范围限制在企业内部,或者事业部间共享服务,总的来说可以通过防火墙控制服务区域。

对于公共服务 OpenID/Oauth 更适合,我们不关心用户地理位,终端设备的情况。实现方式请参考: Spring boot with Oauth2

1.21.5.5. 第三阶段,终极版诞生,HTTP2 + HTTP Basic Auth + Oauth2 + Jwt

  1. SSL 双向认证

  2. HTTP Basic Auth 认证

  3. Oauth2 认证

这是我们最终的方案,双向认证是服务器与客户端两端都需要证书才能通信。

		
App(IOS/Android) -->  SSL 双向认证 --> SLB/Proxy --> Feign Client 		
		
			

file:///Users/neo/tmp/spring/security/oauth2.html#oauth2.jwt