在本系列的第一篇文章中,我们使用基于 JVM 的 Micronaut 框架开发并部署了三个微服务。在第二篇文章中,我们将为应用程序添加几个功能:分布式跟踪、JWT 安全性和无服务器功能。此外,我们也将介绍 Micronaut 提供的用户输入验证功能。
将系统分解为更小、更细粒度的微服务可以带来多种好处,但也会给生产环境的监控系统增加复杂性。
你应该假设你的网络将会受到恶意实体的骚扰,它们时刻准备着随心所欲地释放它们的愤怒。
——Sam Newman,《构建微服务》
Micronaut 与 Jaeger 和 Zipkin 原生集成——它们都是顶级的开源分布式跟踪解决方案。
Zipkin 是一种分布式跟踪系统,用于收集时序数据,这些数据可用于解决微服务架构中的延迟问题。它负责收集和查找这些数据。
启动 Zipkin 的简单方法是通过 Docker:
复制代码
$ docker run -d -p9411:9411openzipkin/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)用来启动微服务:
复制代码
./gradlew -parallelrun
你可以通过 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。
Micronaut 提供了多种开箱即用的安全选项,你可以使用基本的身份验证、基于会话的身份验证、JWT 身份验证、Ldap 身份验证,等等。JSON Web Token(JWT)是一种开放的行业标准(RFC 7519)用于在参与方之间声明安全。
Micronaut 提供了开箱即用的用于生成、签名、加密和验证 JWT 令牌的功能。
我们将把 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"
我们做了几个重要的配置变更:
你可以使用 @Secured 注解来配置 Controller 或 Controller Action 级别的访问。
使用 @Secured(“isAuthenticated()”) 注解 BookController.java,只允许经过身份验证的用户访问。同时记得使用 @Secured(“isAuthenticated()”) 注解 inventory 和 books 微服务的 BookController 类。
/login 端点被调用时,会尝试通过任何可用的 AuthenticationProvider 对用户进行身份验证。为了简单起见,我们将允许两个用户访问,他们是福尔摩斯和华生。创建 SampleAuthenticationProvider:
复制代码
gateway/src/main/java/example/micronaut/SampleAuthenticationProvider.java packageexample.micronaut; importio.micronaut.context.annotation.Requires; importio.micronaut.context.env.Environment; importio.micronaut.security.authentication.AuthenticationFailed; importio.micronaut.security.authentication.AuthenticationProvider; importio.micronaut.security.authentication.AuthenticationRequest; importio.micronaut.security.authentication.AuthenticationResponse; importio.micronaut.security.authentication.UserDetails; importio.reactivex.Flowable; importorg.reactivestreams.Publisher; importjavax.inject.Singleton; importjava.util.ArrayList; importjava.util.Arrays; @Requires(notEnv = Environment.TEST) @Singleton publicclassSampleAuthenticationProviderimplementsAuthenticationProvider{ @Override publicPublisher<AuthenticationResponse> authenticate(AuthenticationRequest authenticationRequest) { if(authenticationRequest.getIdentity() ==null) { returnFlowable.just(newAuthenticationFailed()); } if(authenticationRequest.getSecret() ==null) { returnFlowable.just(newAuthenticationFailed()); } if(Arrays.asList("sherlock","watson").contains(authenticationRequest.getIdentity().toString()) && authenticationRequest.getSecret().equals("elementary")) { returnFlowable.just(newUserDetails(authenticationRequest.getIdentity().toString(),newArrayList<>())); } returnFlowable.just(newAuthenticationFailed()); } }
对于 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):
复制代码
./gradlew -parallelrun
你可以运行 cURL 命令,然后会收到 401 错误,表示未授权!
复制代码
$ curl -I http://localhost:8080/api/books HTTP/1.1 401 Unauthorized Date: Mon,1Oct201818:44:54GMT 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}]
我们将添加一个部署到 AWS Lambda 的功能来验证 books 的 ISBN。
复制代码
mncreate-functionexample.micronaut.isbn-validator
注意:我们使用了 Micronaut CLI 提供的 create-function 命令。
我们将创建一个单例来处理 ISBN 10 验证。
创建一个封装操作的接口:
复制代码
packageexample.micronaut; importjavax.validation.constraints.Pattern; publicinterfaceIsbnValidator{ booleanisValid(@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 packageexample.micronaut; importio.micronaut.validation.Validated; importjavax.inject.Singleton; importjavax.validation.constraints.Pattern; @Singleton @Validated publicclassDefaultIsbnValidatorimplementsIsbnValidator{ /** * 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. *@paramisbn 10 Digit ISBN *@returnwhether the ISBN is valid or not. */ @Override publicbooleanisValid(@Pattern(regexp ="//d{10}") String isbn) { char[] digits = isbn.toCharArray(); intaccumulator =0; intmultiplier =10; for(inti =0; i < digits.length; i++) { charc = digits[i]; accumulator += Character.getNumericValue(c) * multiplier; multiplier--; } return(accumulator %11==0); } }
与之前的代码清单一样,你要为需要验证的类添加 @Validated 注解。
创建单元测试:
复制代码
isbn-validator/src/test/java/example/micronaut/IsbnValidatorTest.java packageexample.micronaut; importio.micronaut.context.ApplicationContext; importio.micronaut.context.DefaultApplicationContext; importio.micronaut.context.env.Environment; importorg.junit.AfterClass; importorg.junit.BeforeClass; importorg.junit.Rule; importorg.junit.Test; importorg.junit.rules.ExpectedException; importjavax.validation.ConstraintViolationException; importstaticorg.junit.Assert.assertFalse; importstaticorg.junit.Assert.assertTrue; publicclassIsbnValidatorTest{ privatestaticApplicationContext applicationContext; @BeforeClass publicstaticvoidsetupContext() { applicationContext =newDefaultApplicationContext(Environment.TEST).start(); } @AfterClass publicstaticvoidstopContext() { if(applicationContext!=null) { applicationContext.stop(); } } @RulepublicExpectedException thrown = ExpectedException.none(); @TestpublicvoidtestTenDigitValidation() { thrown.expect(ConstraintViolationException.class); IsbnValidator isbnValidator = applicationContext.getBean(IsbnValidator.class); isbnValidator.isValid("01234567891"); } @Test publicvoidtestControlDigitValidationWorks() { 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 packageexample.micronaut; publicclassIsbnValidationRequest{ privateString isbn; publicIsbnValidationRequest(){ } publicIsbnValidationRequest(String isbn){ this.isbn = isbn; } publicStringgetIsbn(){returnisbn; } publicvoidsetIsbn(String isbn){this.isbn = isbn; } }
并返回单个结果(ValidationResponse,一个封装了 ISBN 和一个指示 ISBN 是否有效的布尔值的 POJO)。
复制代码
isbn-validator/src/main/java/example/micronaut/IsbnValidationResponse.java packageexample.micronaut; publicclassIsbnValidationResponse{ privateString isbn; privateBoolean valid; publicIsbnValidationResponse(){ } publicIsbnValidationResponse(String isbn,booleanvalid){ this.isbn = isbn; this.valid = valid; } publicStringgetIsbn(){ returnisbn; } publicvoidsetIsbn(String isbn){ this.isbn = isbn; } publicBooleangetValid(){ returnvalid; } publicvoidsetValid(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 packageexample.micronaut; importio.micronaut.function.FunctionBean; importjava.util.function.Function; importjavax.validation.ConstraintViolationException; @FunctionBean("isbn-validator") publicclassIsbnValidatorFunctionimplementsFunction<IsbnValidationRequest,IsbnValidationResponse> { privatefinalIsbnValidator isbnValidator; publicIsbnValidatorFunction(IsbnValidator isbnValidator) { this.isbnValidator = isbnValidator; } @Override publicIsbnValidationResponse apply(IsbnValidationRequest req) { try{ returnnewIsbnValidationResponse(req.getIsbn(), isbnValidator.isValid(req.getIsbn())); }catch(ConstraintViolationException e) { returnnewIsbnValidationResponse(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 packageexample.micronaut; importio.micronaut.function.client.FunctionClient; importio.micronaut.http.annotation.Body; importio.reactivex.Single; importjavax.inject.Named; @FunctionClient publicinterfaceIsbnValidatorClient{ @Named("isbn-validator") Single<IsbnValidationResponse> isValid(@BodyIsbnValidationRequest 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; publicclassIsbnValidatorFunctionTest { @Test public void testFunction(){ EmbeddedServer server =ApplicationContext.run(EmbeddedServer.class); IsbnValidatorClient client = server.getApplicationContext().getBean(IsbnValidatorClient.class); assertTrue(client.isValid(newIsbnValidationRequest("1491950358")).blockingGet().getValid()); assertTrue(client.isValid(new IsbnValidationRequest("1680502395")).blockingGet().getValid()); assertFalse(client.isValid(newIsbnValidationRequest("0000502395")).blockingGet().getValid()); assertFalse(client.isValid(newIsbnValidationRequest("01234567891")).blockingGet().getValid()); assertFalse(client.isValid(newIsbnValidationRequest("012345678")).blockingGet().getValid()); server.close(); } }
假设你拥有 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.jar11M 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; publicinterfaceIsbnValidator { Single<IsbnValidationResponse> validateIsbn(@Body IsbnValidationRequest req); }
创建一个 @FunctionClient:
复制代码
src/main/java/example/micronaut/FunctionIsbnValidator.java packageexample.micronaut; importio.micronaut.context.annotation.Requires; importio.micronaut.context.env.Environment; importio.micronaut.function.client.FunctionClient; importio.micronaut.http.annotation.Body; importio.reactivex.Single; importjavax.inject.Named; @FunctionClient @Requires(notEnv = Environment.TEST) publicinterfaceFunctionIsbnValidatorextendsIsbnValidator{ @Override @Named("isbn-validator") Single<IsbnValidationResponse> validateIsbn(@BodyIsbnValidationRequest req); }
关于上面这些代码有几点值得注意:
最后一步是修改 gateway 的 BookController,让它调用函数。
复制代码
src/main/java/example/micronaut/BooksController.java packageexample.micronaut; importio.micronaut.http.annotation.Controller; importio.micronaut.http.annotation.Get; importio.micronaut.security.annotation.Secured; importio.reactivex.Flowable; importjava.util.List; @Secured("isAuthenticated()") @Controller("/api") publicclassBooksController{ privatefinalBooksFetcher booksFetcher; privatefinalInventoryFetcher inventoryFetcher; privatefinalIsbnValidator isbnValidator; publicBooksController(BooksFetcher booksFetcher, InventoryFetcher inventoryFetcher, IsbnValidator isbnValidator) { this.booksFetcher = booksFetcher; this.inventoryFetcher = inventoryFetcher; this.isbnValidator = isbnValidator; } @Get("/books") Flowable<Book> findAll() { returnbooksFetcher.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); returnb; }) ); } }
我们通过构造函数注入了 IsbnValidator。调用远程函数对程序员来说是透明的。
下面的图片说明了我们在这一系列文章中开发的应用程序:
关于 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