如何保护Service-to-Service的微服务
原创 Yunooa 锅外的大佬 2019-08-01
点击左上角蓝字,关注“锅外的大佬”
专注分享国外最新技术内容
已构建的微服务架构,如何保证了服务与服务之间(service-to-service)通信的安全性?
你可以通过不在 docker-compose.yml文件中暴露端口,使得服务之间的通信更加安全。但是,如果微服务应用程序的端口意外暴露又会发生什么呢?任何人都可以访问这些数据吗?
未雨绸缪,我将告诉你,如何使用HTTPS和OAuth 2.0保护服务与服务通信。
1.搭建微服务
我将使用Spring Boot,Spring Cloud和Spring Cloud Config快速构建完整的微服务架构。我朋友 Raphael 撰写了一篇关于 如何构建Spring微服务并将它们Docker化(Dockerize)用于生产 的文章。您可以使用他的示例应用程序作为起点。克隆 okta-spring-microservices-docker-example 项目:
``
git clone https://github.com/oktadeveloper/okta-spring-microservices-docker-example.git spring-microservices-security
cd spring-microservices-security
该项目需要Okta上的两个OpenID连接应用程序,一个用于开发(开发App),一个用于生产(生产App)。如果您没有完成上述教程,则需要在Okta上创建每个应用程序。
# 2. 在Okta上创建OpenID Connect应用程序
您可以注册一个 免费的开发者帐户 ,该帐户可以免费拥有1000个月活跃用户。对此例来说,应该已经足够了。
为什么使用Okta?因为编写身份验证并不有趣。Okta具有身份验证和用户管理API,可让您更快地开发应用程序。Okta的API和SDK使您可以在几分钟内轻松地对用户进行身份验证,管理和保护。
创建帐户后,在Okta的仪表板(应用程序 > 添加应用程序)中创建一个新的Web应用程序。为应用程序指定名称,复制现有的登录重定向URI,并使其使用HTTPS。单击 “完成”。
结果应类似于下面的屏幕截图。
![](https://s4.51cto.com/images/blog/202008/25/3046f84351c46e32692f710524201387.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
为生产App创建另一个应用。我将其命名为:Prod Microservices。
在克隆的项目中,修改config / school-ui.properties,获取开发App的设置。
- okta.oauth2.issuer=https://okta.okta.com/oauth2/default
- okta.oauth2.clientId={devClientId}
- okta.oauth2.clientSecret={devClientId}
用 Maven 单独运行应用程序时将使用这些设置。使用 Docker容器运行时将使用生产环境配置。修改 config-data/school-ui-production.properties 以从生产App中获取设置。
* okta.oauth2.clientId={prodClientId}
* okta.oauth2.clientSecret={prodClientId}
在docker-compose.yml 使用 spring.profiles.active 启用生产(prodcution)配置文件 :
* school-ui:
* image: developer.okta.com/microservice-docker-school-ui:0.0.1-SNAPSHOT
* environment:
* - JAVA_OPTS=
* -DEUREKA_SERVER=http://discovery:8761/eureka
* -Dspring.profiles.active=production
* restart: on-failure
* depends_on:
* - discovery
* - config
* ports:
* - 8080:8080
Docker Compose从应用程序上方的目录运行,并从config-data 目录中读取其数据 。因此,您需要将这些属性文件复制到此目录中。在该项目的根目录运行以下命令。
cp config/*.properties config-data/.
3.使用Docker Compose启动微服务
该项目在其根目录下有一个聚合 pom.xml ,允许您使用一个命令构建所有项目。运行以下 Maven 命令为每个项目构建,测试以及构建Docker镜像。
mvn clean install
如果您没有安装 Maven ,可以使用SDKMAN安装它:sdk install maven
完成此过程后,使用Docker Compose启动所有应用程序{config,discovery,school-service和school-ui}。如果没有安装Docker Compose,请参阅 安装Docker Compose。
docker-compose up -d
您可以使用 Kitematic 在启动时查看每个应用的日志。
浏览器访问:http://localhost:8080 。在这样做之后,您应该能够登录并查看学校课程列表.
4.Spring Security和OAuth 2.0
这个例子使用了 Okta的Spring Boot Starter,它是Spring Security上的一个轻量级层。Okta启动器简化了配置并在访问令牌中进行了访问验证。它还允许您指定将用于创建Spring Security权限的声明。
docker-compose.yml 文件不会暴露 school-service 给外界,通过不指定 ports 来做到这一点。
school-ui 项目中的 SchoolController 类告知与school-service 使用Spring的 RestTemplate。
@GetMapping("/classes")
@PreAuthorize("hasAuthority('SCOPE_profile')")
public ResponseEntity> listClasses() {
return restTemplate
.exchange("http://school-service/class", HttpMethod.GET, null,
new ParameterizedTypeReference>() {});
}
您会注意到此类的端点上存在安全性,但服务之间不存在安全性。我将在下面的步骤中向您展示如何解决这个问题。
首先,暴露 school-service 的端口以模拟某人错误植入配置。更改 school-service的 docker-compose.yml 配置使其暴露端口。
school-service:
image: developer.okta.com/microservice-docker-school-service:0.0.1-SNAPSHOT
environment:
- JAVA_OPTS=
-DEUREKA_SERVER=http://discovery:8761/eureka
depends_on:
- discovery
- config
ports:
- 8081:8081
使用Docker Compose重启所有内容:
docker-compose down
docker-compose up -d
访问:http://localhost:8081, 你将无需进行身份验证即可查看数据 !可怕!
在继续下一部分之前,请确保关闭所有Docker容器。
docker-compose down
5.无处不在的HTTPS!
HTTPS代表 “安全” HTTP。HTTPS连接是加密的,其内容比HTTP连接更难以阅读。近年来,即使在开发过程中,也无处不在地使用HTTPS。使用HTTPS运行时可能会遇到一些问题,最好及早发现它们。
Let's Encrypt 是一个提供免费HTTPS证书的证书颁发机构。它还具有自动续订的API。简而言之,它使HTTPS变得如此简单,没有理由不使用它!有关 如何使用 Let's Encrypt生成证书的说明, 请参阅 添加社交登录到您的JHipster应用程序,以获取有关如何使用certbot和Let's Encrypt 生成证书的说明。
我也鼓励您查看 Spring Boot Starter ACME。这是一个Spring Boot模块,可以使用Let’s Encrypt 和自动证书管理环境(ACME)协议简化生成证书。
6.使用mkcert轻松实现本地TLS
我最近发现了一个名为mkcert的工具 ,它允许创建 localhost 证书。您可以在macOS上使用Homebrew安装它:
* brew install mkcert
* brew install nss # Needed for Firefox
如果您使用的是Linux,则需要先安装 certutil :
* sudo apt install libnss3-tools
然后使用Linuxbrew 执行brew install mkcert 。Windows用户可以 使用Chocolately或Scoop。
执行以下 mkcert 命令为localhost生成一个证, 127.0.0.1您的计算机的名称,以及discovery 主机(如在 docker-compose.yml中引用)。
* mkcert -install
* mkcert localhost 127.0.0.1 ::1 `hostname` discovery
如果生成带有数字的文件,则重命名文件,使其没有数字。
* mv localhost+2.pem localhost.pem
* mv localhost+2-key.pem localhost-key.pe
7.Spring Boot HTTPS
Spring Boot不支持带有 PEM 扩展的证书 ,但您可以将其转换为Spring Boot支持的 PKCS12 。您可以使用OpenSSL将证书和私钥转换为 PKCS12 。这也是Let's Encrypt生成的证书所必需的。
运行 openssl 以转换证书:
* openssl pkcs12 -export -in localhost.pem -inkey \
* localhost-key.pem -out keystore.p12 -name bootifulsecurity
出现提示时指定密码。
在项目的根目录下创建 https.env 文件,并指定以下属性以启用HTTPS。
* export SERVER_SSL_ENABLED = true
* export SERVER_SSL_KEY_STORE = ../keystore.p12
* export SERVER_SSL_KEY_STORE_PASSWORD = {yourPassword}
* export SERVER_SSL_KEY_ALIAS = bootifulsecurity
* export SERVER_SSL_KEY_STORE_TYPE = PKCS12
更新 .gitignore 文件,删除 .env 文件,使密钥库密码不会在源代码管理中终止。
*.env
运行 source https.env 设置环境变量。或者,更好的是,将它添加到您的 .bashrc 或 .zshrc文件中,以便为每个新shell设置这些变量。当然,您也可以将它们包含在每个应用程序的 application.properties中。但之后您将密钥存储在源代码管理中。如果您没有将此示例检查到源代码管理中,则可以使用以下设置进行复制/粘贴。
* server.ssl.enabled=true
* server.ssl.key-store=../keystore.p12
* server.ssl.key-store-password: {yourPassword}
* server.ssl.key-store-type: PKCS12
* server.ssl.key-alias: bootifulsecurity
启动 discovery 应用:
* cd discovery
* source ../https.env
* mvn spring-boot:run
然后确认您可以访问: https://localhost:8761。
打开 docker-compose.yml 并将所有实例的 http 更改为 https。编辑 school-ui/src/main/java/ui/controller/SchoolController.java 将 school-service 的调用改为使用HTTPS。
* return restTemplate
* .exchange("https://school-service/class", HttpMethod.GET, null,
* new ParameterizedTypeReference>() {});
更新 {config,school-service,school-ui}/src/main/resources/application.properties 中使每个实例注册为安全的应用程序。
* eureka.instance.secure-port-enabled=true
* eureka.instance.secure-port=${server.port}
* eureka.instance.status-page-url=https://${eureka.hostname}:${server.port}/actuator/info
* eureka.instance.health-check-url=https://${eureka.hostname}:${server.port}/actuator/health
* eureka.instance.home-page-url=https://${eureka.hostname}${server.port}/
此外,更改每个application.properties (和 bootstrap.yml)中的Eureka地址为:https://localhost:8761/eureka。
school-ui工程中的端口号尚未指定,因此需要添加:server.port=8080.
此时,您应该能够通过在每个项目中运行以下内容(在单独的终端窗口中)来启动所有应用程序。
source ../https.env
./mvnw spring-boot:start
确认一切正常 https://localhost:8080。然后杀掉所有进程: killall java。
8.在Docker Compose中使用HTTPS
Docker不会读取环境变量,它不清楚本地CA(证书颁发机构),也无法将父目录中的文件添加到映像。
要解决此问题,需要复制 keystore.p12 和 localhost.pem 到每个项目的目录。第一个将用于Spring Boot,第二个将添加到每个映像的Java Keystore。
* cp localhost.pem keystore.p12 config/.
* cp localhost.pem keystore.p12 discovery/.
* cp localhost.pem keystore.p12 school-service/.
* cp localhost.pem keystore.p12 school-ui/.
然后修改每个项目 Dockerfile ,来复制证书并将其添加到其信任库。
* FROM openjdk:8-jdk-alpine
* VOLUME /tmp
* ADD target/*.jar app.jar
* ADD keystore.p12 keystore.p12
* USER root
* COPY localhost.pem $JAVA_HOME/jre/lib/security
* RUN \
* cd $JAVA_HOME/jre/lib/security \
* && keytool -keystore cacerts -storepass changeit -noprompt \
* -trustcacerts -importcert -alias bootifulsecurity -file localhost.pem
* ENV JAVA_OPTS=""
* ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]
然后Spring Boot和HTTPS的环境变量创建 .env 文件。
SERVER_SSL_ENABLED=true
SERVER_SSL_KEY_STORE=keystore.p12
SERVER_SSL_KEY_STORE_PASSWORD={yourPassword}
SERVER_SSL_KEY_ALIAS=bootifulsecurity
SERVER_SSL_KEY_STORE_TYPE=PKCS12
EUREKA_INSTANCE_HOSTNAME={yourHostname}
您可以{yourHostname} 通过运行中的 hostname。
Docker Compose有一个“env_file”配置选项,允许您读取此文件以获取环境变量。更新 docker-compose.yml 为每个应用程序指定 env_file 。
version: '3'
services:
discovery:
env_file:
- .env
...
config:
env_file:
- .env
...
school-service:
env_file:
- .env
...
school-ui:
env_file:
- .env
...
可以通过在根目录运行:docker-compose config来确保它正常工作 。
运行 mvn clean install 重新构建所有Docker镜像,并启用HTTPS以进行Eureka注册:
docker-compose up -d
现在,所有应用都使用HTTPS在Docker中运行!可访问 https://localhost:8080来验证。
If your apps do not start up or can’t talk to each other, make sure your hostname matches what you have in .env.
还可以进一步提高安全性:使用OAuth 2.0来保护school-serviceAPI。
9.OAuth 2.0的API安全性
将Okta Spring Boot Starter和Spring Cloud Config添加到 school-service/pom.xml:
com.okta.spring
okta-spring-boot-starter
1.1.0
org.springframework.cloud
spring-cloud-starter-config
然后在school-service/src/main/java/…/service/configuration创建一个 SecurityConfiguration.java 类
package com.okta.developer.docker_microservices.service.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
}
}
创建 school-service/src/test/resources/test.properties 文件并添加属性,使Okta的配置通过,并且在测试时不使用服务发现或配置中心服务。
okta.oauth2.issuer=https://okta.okta.com/oauth2/default
okta.oauth2.clientId=TEST
spring.cloud.discovery.enabled=false
spring.cloud.config.discovery.enabled=false
spring.cloud.config.enabled=false
然后修改ServiceApplicationTests.java加载test properties:
import org.springframework.test.context.TestPropertySource;
...
@TestPropertySource(locations="classpath:test.properties")
public class ServiceApplicationTests {
...
}
添加school-service/src/main/resources/bootstrap.yml 允许此实例从Spring Cloud Config读取其配置。
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_SERVER:https://localhost:8761/eureka}
spring:
application:
name: school-service
cloud:
config:
discovery:
enabled: true
serviceId: CONFIGSERVER
failFast: true
然后复制 将 config/school-ui.properties 等价复制到 school-service 。
cp config/school-ui.properties config/school-service.properties
对于Docker Compose,您还需要config-data/school-service.properties ,并添加以下配置:
okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/default
okta.oauth2.clientId={prodClientId}
okta.oauth2.clientSecret={prodClientId}
您还需要修改 docker-compose.yml 以便 school-service 在失败时重新启动。
school-service:
...
restart: on-failure
需要做的最后一步是修改 SchoolController (在 school-ui 项目中),以向其发出的请求添加OAuth 2.0访问令牌。
示例1.将AccessToken添加到RestTemplate
package com.okta.developer.docker_microservices.ui.controller;
import com.okta.developer.docker_microservices.ui.dto.TeachingClassDto;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;
import java.io.IOException;
import java.util.List;
@Controller
@RequestMapping("/")
public class SchoolController {
private final OAuth2AuthorizedClientService authorizedClientService;
private final RestTemplate restTemplate;
public SchoolController(OAuth2AuthorizedClientService clientService,
RestTemplate restTemplate) {
this.authorizedClientService = clientService;
this.restTemplate = restTemplate;
}
@RequestMapping("")
public ModelAndView index() {
return new ModelAndView("index");
}
@GetMapping("/classes")
@PreAuthorize("hasAuthority('SCOPE_profile')")
public ResponseEntity> listClasses(
@AuthenticationPrincipal OAuth2AuthenticationToken authentication) {
OAuth2AuthorizedClient authorizedClient =
this.authorizedClientService.loadAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(),
authentication.getName());
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
restTemplate.getInterceptors().add(getBearerTokenInterceptor(accessToken.getTokenValue()));
return restTemplate
.exchange("https://school-service/class", HttpMethod.GET, null,
new ParameterizedTypeReference>() {});
}
private ClientHttpRequestInterceptor getBearerTokenInterceptor(String accessToken) {
return (request, bytes, execution) -> {
request.getHeaders().add("Authorization", "Bearer " + accessToken);
return execution.execute(request, bytes);
};
}
}
由于 school-ui 和 school-service 使用相同的OIDC应用程序设置,服务器将识别并验证访问令牌(也是JWT),并允许访问。
此时,您可以选择使用./mvnw spring-boot:run 或Docker Compose 单独运行所有应用程序 。后一种方法只需要几个命令.
mvn clean install
docker-compose down
docker-compose up -d
10.HTTP Basic Auth安全通信
为了进一步提高微服务,Eureka Server和Spring Cloud Config之间的安全性,您可以添加HTTP基本身份验证。为此,您需要在config 和 discovery 项目中添加 spring-boot-starter-security 依赖 。然后指定spring.security.user.password 并加密它。您可以在Spring Cloud Config的安全文档中了解有关如何执行此操作的更多信息 。
在两个项目中配置Spring Security后,您可以调整URL以在其中包含用户名和密码。例如,school-ui 项目中的 bootstrap.yml:
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_SERVER:https://username:password@localhost:8761/eureka}
还需要对docker-compose.yml中的URL进行类似的调整。
本教程展示了如何确保您的服务到服务通信在微服务架构中是安全的。了解如何在任何地方使用HTTPS并使用OAuth 2.0和JWT保护API。
您可以在GitHub上的oktadeveloper/okta-spring-microservices-https-example找到此示例的源代码。
出处spring for all 翻译组
8月福利,准时来袭!关注公众号
后台回复:003 ,领取7月翻译集锦~
往期福利:回复 001, 002即可领取