关键要点
Micronaut 提供了与 Zipkin 和 Jaeger 等多种分布式跟踪解决方案的无缝集成。
框架提供了几种“开箱即用”的安全解决方案,例如基于 JWT 的认证。
Micronaut 提供了“令牌传播”之类的功能,用以简化微服务之间的安全通信。
因为内存占用少,Micronaut 能够运行在功能即服务(FaaS)无服务器环境中。
在本系列的第一篇文章中,我们使用基于 JVM 的Micronaut框架开发并部署了三个微服务。在第二篇文章中,我们将为应用程序添加几个功能:分布式跟踪、JWT 安全性和无服务器功能。此外,我们也将介绍 Micronaut 提供的用户输入验证功能。
分布式跟踪
将系统分解为更小、更细粒度的微服务可以带来多种好处,但也会给生产环境的监控系统增加复杂性。
你应该假设你的网络将会受到恶意实体的骚扰,它们时刻准备着随心所欲地释放它们的愤怒。
——Sam Newman,《构建微服务》
Micronaut 与 Jaeger 和 Zipkin 原生集成——它们都是顶级的开源分布式跟踪解决方案。
Zipkin 是一种分布式跟踪系统,用于收集时序数据,这些数据可用于解决微服务架构中的延迟问题。它负责收集和查找这些数据。
启动 Zipkin 的简单方法是通过 Docker:
$ docker run -d -p 9411:9411 openzipkin/zipkin
复制代码
这个应用程序由三个微服务组成,也就是我们在第一篇文章中开发的三个微服务(gateway、inventory、books)。
我们需要对这三个微服务做出修改。
修改 build.gradle,加入跟踪依赖项:
build.gradle
compile "io.micronaut:micronaut-tracing"
复制代码
将以下依赖项添加到 build.gradle 中,这样就可以将跟踪数据发送到 Zipkin。
build.gradle
runtime 'io.zipkin.brave:brave-instrumentation-http'
runtime 'io.zipkin.reporter2:zipkin-reporter'
compile 'io.opentracing.brave:brave-opentracing'
复制代码
配置跟踪选项:
src/main/resources/application.yml
tracing:
zipkin:
http:
url: http://localhost:9411
enabled: true
sampler:
probability: 1
复制代码
设置 tracing.zipkin.sample.probability = 1,意思是我们要跟踪所有的请求。在生产环境中,你可能希望设置较低的百分比。
在测试时禁用跟踪:
src/test/resources/application-test.yml
tracing:
zipkin:
enabled: false
复制代码
只需要很少的配置更改,就可以将分布式跟踪集成到 Micronaut 中。
运行应用程序
现在让我们运行应用程序,看看分布式跟踪集成是否能够正常运行。在第一篇文章中,我们集成了 Consul,用于实现服务发现。因此,在启动微服务之前需要先启动 Zipkin 和 Consul。在微服务启动好以后,它们将在 Consul 服务发现中进行注册。当我们发出请求时,它们会向 Zipkin 发送数据。
Gradle 提供了一个 flag(-parallel)用来启动微服务:
你可以通过 cURL 命令向三个微服务发起请求:
$ curl http://localhost:8080/api/books
[{"isbn":"1680502395","name":"Release It!","stock":3},
{"isbn":"1491950358","name":"Building Microservices","stock":2}]
复制代码
然后,你可以通过http://localhost:9411来访问Zipkin UI。
JWT 安全性
Micronaut 提供了多种开箱即用的安全选项,你可以使用基本的身份验证、基于会话的身份验证、JWT 身份验证、Ldap 身份验证,等等。JSON Web Token(JWT)是一种开放的行业标准(RFC 7519)用于在参与方之间声明安全。
Micronaut 提供了开箱即用的用于生成、签名、加密和验证 JWT 令牌的功能。
我们将把 JWT 身份验证集成到我们的应用程序中。
修改 gateway 微服务,让它支持 JWT
gateway 微服务将负责生成和传播 JWT 令牌。
修改 build.gradle,为每个微服务(gateway、inventory 和 books)添加 micronaut-security-jwt 依赖项:
gateway/build.gradle
compile "io.micronaut:micronaut-security-jwt"
annotationProcessor "io.micronaut:micronaut-security"
复制代码
修改 application.yml:
gateway/src/main/resources/application.yml
micronaut:
application:
name: gateway
server:
port: 8080
security:
enabled: true
endpoints:
login:
enabled: true
oauth:
enabled: true
token:
jwt:
enabled: true
signatures:
secret:
generator:
secret: pleaseChangeThisSecretForANewOne
writer:
header:
enabled: true
propagation:
enabled: true
service-id-regex: "books|inventory"
复制代码
我们做了几个重要的配置变更:
micronaut.security.enable = true 启用了安全,并默认为每个端点提供安全保护。
micronaut.security.endpoints.login.enable = true 启用了/login 端点,我们将用它进行身份验证。
micronaut.security.endpoints.oauth.enable = true 启用了/oauth/access_tokenendpoint 端点,在令牌过期时,我们可以使用它来获取新的 JWT 访问令牌。
micronaut.security.jwt.enable = true 启用了 JWT 功能。
我们让应用程序启用签名的 JWT。更多的签名和加密选项,请参阅 JWT 令牌生成文档。
micronaut.security.token.propagation.enabled = true 表示启用了令牌传播。这是一种在微服务架构中简化 JWT 或其他令牌安全机制的功能。
micronaut.security.writer.header.enabled = ture 启用了一个令牌写入器,它将为开发人员在 HTTP 标头中写入 JWT 令牌。
micronaut.security.token.propagation.service-id-regex 设置了一个正则表达式,用于匹配需要进行令牌传播的服务。我们匹配了应用程序中的其他两个服务。
你可以使用 @Secured 注解来配置 Controller 或 Controller Action 级别的访问。
使用 @Secured(“isAuthenticated()”)注解 BookController.java,只允许经过身份验证的用户访问。同时记得使用 @Secured(“isAuthenticated()”)注解 inventory 和 books 微服务的 BookController 类。
/login 端点被调用时,会尝试通过任何可用的 AuthenticationProvider 对用户进行身份验证。为了简单起见,我们将允许两个用户访问,他们是福尔摩斯和华生。创建 SampleAuthenticationProvider:
gateway/src/main/java/example/micronaut/SampleAuthenticationProvider.java
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.security.authentication.AuthenticationFailed;
import io.micronaut.security.authentication.AuthenticationProvider;
import io.micronaut.security.authentication.AuthenticationRequest;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.authentication.UserDetails;
import io.reactivex.Flowable;
import org.reactivestreams.Publisher;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.Arrays;
@Requires(notEnv = Environment.TEST)
@Singleton
public class SampleAuthenticationProvider implements AuthenticationProvider {
@Override
public Publisher<AuthenticationResponse> authenticate(AuthenticationRequest authenticationRequest) {
if (authenticationRequest.getIdentity() == null) {
return Flowable.just(new AuthenticationFailed());
}
if (authenticationRequest.getSecret() == null) {
return Flowable.just(new AuthenticationFailed());
}
if (Arrays.asList("sherlock", "watson").contains(authenticationRequest.getIdentity().toString()) && authenticationRequest.getSecret().equals("elementary")) {
return Flowable.just(new UserDetails(authenticationRequest.getIdentity().toString(), new ArrayList<>()));
}
return Flowable.just(new AuthenticationFailed());
}
}
复制代码
修改 inventory 和 books,让它们支持 JWT
对于 inventory 和 books,除了添加 micronaut-security-jwt 依赖项并使用 @Secured 注解控制器之外,我们还需要修改 application.yml,以便能够验证在 gateway 中生成和签名的 JWT 令牌。
修改 application.yml:
inventory/src/main/resources/application.yml
micronaut:
application:
name: inventory
server:
port: 8081
security:
enabled: true
token:
jwt:
enabled: true
signatures:
secret:
validation:
secret: pleaseChangeThisSecretForANewOne
复制代码
请注意,我们使用与 gateway 配置中相同的秘钥,这样就可以验证由 gateway 微服务签名的 JWT 令牌。
运行安全的应用程序
在启动了 Zipkin 和 Consul 之后,你就可以同时启动这三个微服务。Gradle 提供了一个方便的 flag(-parallel):
你可以运行 cURL 命令,然后会收到 401 错误,表示未授权!
$ curl -I http://localhost:8080/api/books HTTP/1.1 401 Unauthorized
Date: Mon, 1 Oct 2018 18:44:54 GMT transfer-encoding: chunked connection: close
复制代码
我们需要先登录,并获得一个有效的 JWT 访问令牌:
$ curl -X "POST" "http://localhost:8080/login" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{ "username": "sherlock", "password": "password" }'
{"username":"sherlock","access_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWI iOiJzaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOSwicm9sZXMiOltdLCJpc3MiOiJnYX Rld2F5IiwiZXhwIjoxNTM4NDE2MDA5LCJpYXQiOjE1Mzg0MTI0MDl9.1W4CXbN1bJgM CQlCDKJtm7zHWzyZeIr1rHpTuDy6h0","refresh_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ zaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOSwicm9sZXMiOltdLCJpc3MiOiJnYXRld2 F5IiwiaWF0IjoxNTM4NDEyNDA5fQ.l72msZKwHmYeLs7T0vKtRxu7_DZr62rPCILNmC 7UEZ4","expires_in":3600,"token_type":"Bearer"}
复制代码
Micronaut 提供了开箱即用的 RFC 6750 Bearer Token 规范支持。我们可以使用从/login 响应标头中获得的 JWT 来调用/api/books 端点。
curl "http://localhost:8080/api/books" \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOS wicm9sZXMiOltdLCJpc3MiOiJnYXRld2F5IiwiZXhwIjoxNTM4NDE2MDA5LCJpYXQiO jE1Mzg0MTI0MDl9.1W4CXbN1bJgMCQlCDKJtm7zHWz-yZeIr1rHpTuDy6h0'
[{"isbn":"1680502395","name":"Release It!","stock":3}, {"isbn":"1491950358","name":"Building Microservices","stock":2}]
复制代码
Serverless
我们将添加一个部署到 AWS Lambda 的功能来验证 books 的 ISBN。
mn create-function example.micronaut.isbn-validator
复制代码
注意:我们使用了 Micronaut CLI 提供的 create-function 命令。
验证
我们将创建一个单例来处理 ISBN 10 验证。
创建一个封装操作的接口:
package example.micronaut;
import javax.validation.constraints.Pattern;
public interface IsbnValidator {
boolean isValid(@Pattern(regexp = "\\d{10}") String isbn);
}
复制代码
Micronaut 的验证基于标准框架JSR 380,也称为 Bean Validation 2.0。
Hibernate Validator是这个标准的参考实现。
将以下代码段添加到 build.gradle 中:
isbn-validator/build.gradle
compile "io.micronaut.configuration:micronaut-hibernatevalidator"
复制代码
创建一个实现了 IsbnValidator 的单例。
isbn-validator/src/main/java/example/micronaut/DefaultIsbnValidator.java
package example.micronaut;
import io.micronaut.validation.Validated;
import javax.inject.Singleton;
import javax.validation.constraints.Pattern;
@Singleton
@Validated
public class DefaultIsbnValidator implements IsbnValidator {
/**
* must range from 0 to 10 (the symbol X is used for 10), and must be such that the sum of all the ten digits, each multiplied by its (integer) weight, descending from 10 to 1, is a multiple of 11.
* @param isbn 10 Digit ISBN
* @return whether the ISBN is valid or not.
*/
@Override
public boolean isValid(@Pattern(regexp = "\\d{10}") String isbn) {
char[] digits = isbn.toCharArray();
int accumulator = 0;
int multiplier = 10;
for (int i = 0; i < digits.length; i++) {
char c = digits[i];
accumulator += Character.getNumericValue(c) * multiplier;
multiplier--;
}
return (accumulator % 11 == 0);
}
}
复制代码
与之前的代码清单一样,你要为需要验证的类添加 @Validated 注解。
创建单元测试:
isbn-validator/src/test/java/example/micronaut/IsbnValidatorTest.java
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.DefaultApplicationContext;
import io.micronaut.context.env.Environment;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import javax.validation.ConstraintViolationException;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IsbnValidatorTest {
private static ApplicationContext applicationContext;
@BeforeClass
public static void setupContext() {
applicationContext = new DefaultApplicationContext(Environment.TEST).start();
}
@AfterClass
public static void stopContext() {
if (applicationContext!=null) {
applicationContext.stop();
}
}
@Rule public ExpectedException thrown = ExpectedException.none();
@Test public void testTenDigitValidation() {
thrown.expect(ConstraintViolationException.class);
IsbnValidator isbnValidator = applicationContext.getBean(IsbnValidator.class);
isbnValidator.isValid("01234567891");
}
@Test
public void testControlDigitValidationWorks() {
IsbnValidator isbnValidator = applicationContext.getBean(IsbnValidator.class);
assertTrue(isbnValidator.isValid("1491950358"));
assertTrue(isbnValidator.isValid("1680502395"));
assertFalse(isbnValidator.isValid("0000502395"));
}
}
复制代码
如果我们尝试使用十一位数字字符串调用该方法,就会抛出 javax.validation.ConstraintViolationException。
函数的输入和输出
这个函数将接受单个参数(ValidationRequest,它是一个封装了 ISBN 的 POJO)。
isbn-validator/src/main/java/example/micronaut/IsbnValidationRequest.java
package example.micronaut;
public class IsbnValidationRequest {
private String isbn;
public IsbnValidationRequest() {
}
public IsbnValidationRequest(String isbn) {
this.isbn = isbn;
}
public String getIsbn() { return isbn; }
public void setIsbn(String isbn) { this.isbn = isbn; }
}
复制代码
并返回单个结果(ValidationResponse,一个封装了 ISBN 和一个指示 ISBN 是否有效的布尔值的 POJO)。
isbn-validator/src/main/java/example/micronaut/IsbnValidationResponse.java
package example.micronaut;
public class IsbnValidationResponse {
private String isbn;
private Boolean valid;
public IsbnValidationResponse() {
}
public IsbnValidationResponse(String isbn, boolean valid) {
this.isbn = isbn;
this.valid = valid;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public Boolean getValid() {
return valid;
}
public void setValid(Boolean valid) {
this.valid = valid;
}
}
复制代码
函数测试
当我们运行 create-function 命令时,Micronaut 会在 src/main/java/example/micronaut 目录创建一个 IsbnValidatorFunction 类。修改它,让它实现 java.util.Function 接口。
isbn-validator/src/main/java/example/micronaut/IsbnValidatorFunction.java
package example.micronaut;
import io.micronaut.function.FunctionBean;
import java.util.function.Function;
import javax.validation.ConstraintViolationException;
@FunctionBean("isbn-validator")
public class IsbnValidatorFunction implements Function<IsbnValidationRequest, IsbnValidationResponse> {
private final IsbnValidator isbnValidator;
public IsbnValidatorFunction(IsbnValidator isbnValidator) {
this.isbnValidator = isbnValidator;
}
@Override
public IsbnValidationResponse apply(IsbnValidationRequest req) {
try {
return new IsbnValidationResponse(req.getIsbn(), isbnValidator.isValid(req.getIsbn()));
} catch(ConstraintViolationException e) {
return new IsbnValidationResponse(req.getIsbn(),false);
}
}
}
复制代码
上面的代码做了几件事:
函数也可以作为 Micronaut 应用程序上下文的一部分运行,这样方便进行测试。应用程序已经在类路径中包含了用于测试的 function-web 和 HTTP 服务器依赖项:
isbn-validator/build.gradle
testRuntime "io.micronaut:micronaut-http-server-netty"
testRuntime "io.micronaut:micronaut-function-web"
复制代码
要在测试中调用函数,需要修改 IsbnValidatorClient.java
isbn-validator/src/test/java/example/micronaut/IsbnValidatorClient.java
package example.micronaut;
import io.micronaut.function.client.FunctionClient;
import io.micronaut.http.annotation.Body;
import io.reactivex.Single;
import javax.inject.Named;
@FunctionClient
public interface IsbnValidatorClient {
@Named("isbn-validator")
Single<IsbnValidationResponse> isValid(@Body IsbnValidationRequest isbn);
}
复制代码
同时修改 IsbnValidatorFunctionTest.java。我们需要测试不同的场景(有效的 ISBN、无效的 ISBN、超过 10 位的 ISBN 和少于 10 位的 ISBN)。
isbn-validator/src/test/java/example/micronaut/IsbnValidatorFunctionTest.java
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IsbnValidatorFunctionTest {
@Test public void testFunction() {
EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class);
IsbnValidatorClient client = server.getApplicationContext().getBean(IsbnValidatorClient.class);
assertTrue(client.isValid(new IsbnValidationRequest("1491950358")).blockingGet().getValid());
assertTrue(client.isValid(new IsbnValidationRequest("1680502395")).blockingGet().getValid());
assertFalse(client.isValid(new IsbnValidationRequest("0000502395")).blockingGet().getValid());
assertFalse(client.isValid(new IsbnValidationRequest("01234567891")).blockingGet().getValid());
assertFalse(client.isValid(new IsbnValidationRequest("012345678")).blockingGet().getValid());
server.close();
}
}
复制代码
部署到 AWS Lambda
假设你拥有 Amazon Web Services(AWS)帐户,那么就可以转到 AWS Lambda 并创建一个新功能。
选择 Java 8 运行时。名称为 isbn-validator,并创建一个新的角色表单模板。角色名称为 lambda_basic_execution。
运行./gradlew shadowJar 生成一个 Jar 包。
shadowJar 是 Gradle ShadowJar 插件提供的一个任务。
$ du -h isbn-validator/build/libs/isbn-validator-0.1-all.jar 11M isbn-validator/build/libs/isbn-validator-0.1-all.jar
复制代码
上传 JAR,并指定 Handler。
io.micronaut.function.aws.MicronautRequestStreamHandler
复制代码
我只分配了 256Mb 内存,超时时间为 25 秒。
从另一个微服务中调用函数
我们将在 gateway 微服务中使用这个 lambda。修改 gateway 微服务中的 build.gradle,添加 micronaut-function-client:
com.amazonaws:aws-java-sdk-lambda dependencies:
build.gradle
compile "io.micronaut:micronaut-function-client"
runtime 'com.amazonaws:aws-java-sdk-lambda:1.11.285'
复制代码
修改 src/main/resources/application.yml:
src/main/resources/application.yml
aws:
lambda:
functions:
vat:
functionName: isbn-validator
qualifer: isbn
region: eu-west-3 # Paris Region
复制代码
创建一个接口:
src/main/java/example/micronaut/IsbnValidator.java
package example.micronaut;
import io.micronaut.http.annotation.Body;
import io.reactivex.Single;
public interface IsbnValidator {
Single<IsbnValidationResponse> validateIsbn(@Body IsbnValidationRequest req);
}
复制代码
创建一个 @FunctionClient:
src/main/java/example/micronaut/FunctionIsbnValidator.java
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.function.client.FunctionClient;
import io.micronaut.http.annotation.Body;
import io.reactivex.Single;
import javax.inject.Named;
@FunctionClient
@Requires(notEnv = Environment.TEST)
public interface FunctionIsbnValidator extends IsbnValidator {
@Override
@Named("isbn-validator")
Single<IsbnValidationResponse> validateIsbn(@Body IsbnValidationRequest req);
}
复制代码
关于上面这些代码有几点值得注意:
最后一步是修改 gateway 的 BookController,让它调用函数。
src/main/java/example/micronaut/BooksController.java
package example.micronaut;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.reactivex.Flowable;
import java.util.List;
@Secured("isAuthenticated()")
@Controller("/api")
public class BooksController {
private final BooksFetcher booksFetcher;
private final InventoryFetcher inventoryFetcher;
private final IsbnValidator isbnValidator;
public BooksController(BooksFetcher booksFetcher, InventoryFetcher inventoryFetcher, IsbnValidator isbnValidator) {
this.booksFetcher = booksFetcher;
this.inventoryFetcher = inventoryFetcher;
this.isbnValidator = isbnValidator;
}
@Get("/books")
Flowable<Book> findAll() {
return booksFetcher.fetchBooks()
.flatMapMaybe(b -> isbnValidator.validateIsbn(new IsbnValidationRequest(b.getIsbn()))
.filter(IsbnValidationResponse::getValid)
.map(isbnValidationResponse -> b)
)
.flatMapMaybe(b -> inventoryFetcher.inventory(b.getIsbn())
.filter(stock -> stock > 0)
.map(stock -> {
b.setStock(stock);
return b;
})
);
}
}
复制代码
我们通过构造函数注入了 IsbnValidator。调用远程函数对程序员来说是透明的。
结论
下面的图片说明了我们在这一系列文章中开发的应用程序:
我们有三个微服务(一个 Java 服务、一个 Groovy 服务和一个 Kotlin 服务)。
这些微服务使用 Consul 进行服务发现。
这些微服务使用 Zipkin 作为分布式跟踪服务。
我们添加了第四个微服务,一个部署到 AWS Lambda 的功能。
微服务之间的通信是安全的。每个请求在 Authorization Http 标头中包含一个 JWT 令牌就可以通过网络。JWT 令牌通过内部请求自动传播。
关于 Micronaut 的更多内容,请访问官方网站。
关于作者
Sergio del AmoCaballero 是一名手机应用程序(iOS、Android,后端由 Grails/Micronaut 驱动)开发者。自 2015 年起,Sergio del Amo 为 Groovy 生态系统和微服务维护着一个新闻源Groovy Calamari。
查看英文原文:Micronaut Tutorial: Part 2: Easy Distributed Tracing, JWT Security and AWS Lambda Deployment
评论 1 条评论