本文共 19747 字,大约阅读时间需要 65 分钟。
在本系列的中,我们使用基于JVM的框架开发并部署了三个微服务。在第二篇文章中,我们将为应用程序添加几个功能:分布式跟踪、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 \u0026quot;io.micronaut:micronaut-tracing\u0026quot;
将以下依赖项添加到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.ymltracing: 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 -parallel run
你可以通过cURL命令向三个微服务发起请求:
$ curl http://localhost:8080/api/books[{\u0026quot;isbn\u0026quot;:\u0026quot;1680502395\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;Release It!\u0026quot;,\u0026quot;stock\u0026quot;:3},{\u0026quot;isbn\u0026quot;:\u0026quot;1491950358\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;Building Microservices\u0026quot;,\u0026quot;stock\u0026quot;:2}]
然后,你可以通过 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 \u0026quot;io.micronaut:micronaut-security-jwt\u0026quot; annotationProcessor \u0026quot;io.micronaut:micronaut-security\u0026quot;
修改application.yml:
gateway/src/main/resources/application.ymlmicronaut: 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: \u0026quot;books|inventory\u0026quot;
我们做了几个重要的配置变更:
你可以使用@Secured注解来配置Controller或Controller Action级别的访问。
使用@Secured(“isAuthenticated()”)注解BookController.java,只允许经过身份验证的用户访问。同时记得使用@Secured(“isAuthenticated()”)注解inventory和books微服务的BookController类。
/login端点被调用时,会尝试通过任何可用的AuthenticationProvider对用户进行身份验证。为了简单起见,我们将允许两个用户访问,他们是福尔摩斯和华生。创建SampleAuthenticationProvider:
gateway/src/main/java/example/micronaut/SampleAuthenticationProvider.javapackage 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\u0026lt;AuthenticationResponse\u0026gt; authenticate(AuthenticationRequest authenticationRequest) { if (authenticationRequest.getIdentity() == null) { return Flowable.just(new AuthenticationFailed()); } if (authenticationRequest.getSecret() == null) { return Flowable.just(new AuthenticationFailed()); } if (Arrays.asList(\u0026quot;sherlock\u0026quot;, \u0026quot;watson\u0026quot;).contains(authenticationRequest.getIdentity().toString()) \u0026amp;\u0026amp; authenticationRequest.getSecret().equals(\u0026quot;elementary\u0026quot;)) { return Flowable.just(new UserDetails(authenticationRequest.getIdentity().toString(), new ArrayList\u0026lt;\u0026gt;())); } return Flowable.just(new AuthenticationFailed()); } }
对于inventory和books,除了添加micronaut-security-jwt依赖项并使用@Secured注解控制器之外,我们还需要修改application.yml,以便能够验证在gateway中生成和签名的JWT令牌。
修改application.yml:
inventory/src/main/resources/application.ymlmicronaut: 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 -parallel run
你可以运行cURL命令,然后会收到401错误,表示未授权!
$ curl -I http://localhost:8080/api/books HTTP/1.1 401 UnauthorizedDate: Mon, 1 Oct 2018 18:44:54 GMT transfer-encoding: chunked connection: close
我们需要先登录,并获得一个有效的JWT访问令牌:
$ curl -X \u0026quot;POST\u0026quot; \u0026quot;http://localhost:8080/login\u0026quot; \\-H 'Content-Type: application/json; charset=utf-8' \\-d $'{ \u0026quot;username\u0026quot;: \u0026quot;sherlock\u0026quot;, \u0026quot;password\u0026quot;: \u0026quot;password\u0026quot; }' {\u0026quot;username\u0026quot;:\u0026quot;sherlock\u0026quot;,\u0026quot;access_token\u0026quot;:\u0026quot;eyJhbGciOiJIUzI1NiJ9.eyJzdWI iOiJzaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOSwicm9sZXMiOltdLCJpc3MiOiJnYX Rld2F5IiwiZXhwIjoxNTM4NDE2MDA5LCJpYXQiOjE1Mzg0MTI0MDl9.1W4CXbN1bJgM CQlCDKJtm7zHWzyZeIr1rHpTuDy6h0\u0026quot;,\u0026quot;refresh_token\u0026quot;:\u0026quot;eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ zaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOSwicm9sZXMiOltdLCJpc3MiOiJnYXRld2 F5IiwiaWF0IjoxNTM4NDEyNDA5fQ.l72msZKwHmYeLs7T0vKtRxu7_DZr62rPCILNmC 7UEZ4\u0026quot;,\u0026quot;expires_in\u0026quot;:3600,\u0026quot;token_type\u0026quot;:\u0026quot;Bearer\u0026quot;}
Micronaut提供了开箱即用的RFC 6750 Bearer Token规范支持。我们可以使用从/login响应标头中获得的JWT来调用/api/books端点。
curl \u0026quot;http://localhost:8080/api/books\u0026quot; \\ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOS wicm9sZXMiOltdLCJpc3MiOiJnYXRld2F5IiwiZXhwIjoxNTM4NDE2MDA5LCJpYXQiO jE1Mzg0MTI0MDl9.1W4CXbN1bJgMCQlCDKJtm7zHWz-yZeIr1rHpTuDy6h0'[{\u0026quot;isbn\u0026quot;:\u0026quot;1680502395\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;Release It!\u0026quot;,\u0026quot;stock\u0026quot;:3}, {\u0026quot;isbn\u0026quot;:\u0026quot;1491950358\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;Building Microservices\u0026quot;,\u0026quot;stock\u0026quot;:2}]
我们将添加一个部署到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 = \u0026quot;\\\\d{10}\u0026quot;) String isbn);}
Micronaut的验证基于标准框架,也称为Bean Validation 2.0。
是这个标准的参考实现。
将以下代码段添加到build.gradle中:
isbn-validator/build.gradle compile \u0026quot;io.micronaut.configuration:micronaut-hibernatevalidator\u0026quot;
创建一个实现了IsbnValidator的单例。
isbn-validator/src/main/java/example/micronaut/DefaultIsbnValidator.javapackage 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 = \u0026quot;\\\\d{10}\u0026quot;) String isbn) { char[] digits = isbn.toCharArray(); int accumulator = 0; int multiplier = 10; for (int i = 0; i \u0026lt; 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.javapackage 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(\u0026quot;01234567891\u0026quot;); } @Test public void testControlDigitValidationWorks() { IsbnValidator isbnValidator = applicationContext.getBean(IsbnValidator.class); assertTrue(isbnValidator.isValid(\u0026quot;1491950358\u0026quot;)); assertTrue(isbnValidator.isValid(\u0026quot;1680502395\u0026quot;)); assertFalse(isbnValidator.isValid(\u0026quot;0000502395\u0026quot;)); }}
如果我们尝试使用十一位数字字符串调用该方法,就会抛出javax.validation.ConstraintViolationException。
这个函数将接受单个参数(ValidationRequest,它是一个封装了ISBN的POJO)。
isbn-validator/src/main/java/example/micronaut/IsbnValidationRequest.javapackage 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.javapackage 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.javapackage example.micronaut;import io.micronaut.function.FunctionBean;import java.util.function.Function; import javax.validation.ConstraintViolationException;@FunctionBean(\u0026quot;isbn-validator\u0026quot;) public class IsbnValidatorFunction implements Function\u0026lt;IsbnValidationRequest, IsbnValidationResponse\u0026gt; { 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 \u0026quot;io.micronaut:micronaut-http-server-netty\u0026quot; testRuntime \u0026quot;io.micronaut:micronaut-function-web\u0026quot;
要在测试中调用函数,需要修改IsbnValidatorClient.java
isbn-validator/src/test/java/example/micronaut/IsbnValidatorClient.javapackage 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(\u0026quot;isbn-validator\u0026quot;) Single\u0026lt;IsbnValidationResponse\u0026gt; isValid(@Body IsbnValidationRequest isbn);}
同时修改IsbnValidatorFunctionTest.java。我们需要测试不同的场景(有效的ISBN、无效的ISBN、超过10位的ISBN和少于10位的ISBN)。
isbn-validator/src/test/java/example/micronaut/IsbnValidatorFunctionTest.javapackage 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(\u0026quot;1491950358\u0026quot;)).blockingGet().getValid()); assertTrue(client.isValid(new IsbnValidationRequest(\u0026quot;1680502395\u0026quot;)).blockingGet().getValid()); assertFalse(client.isValid(new IsbnValidationRequest(\u0026quot;0000502395\u0026quot;)).blockingGet().getValid()); assertFalse(client.isValid(new IsbnValidationRequest(\u0026quot;01234567891\u0026quot;)).blockingGet().getValid()); assertFalse(client.isValid(new IsbnValidationRequest(\u0026quot;012345678\u0026quot;)).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.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 \u0026quot;io.micronaut:micronaut-function-client\u0026quot; runtime 'com.amazonaws:aws-java-sdk-lambda:1.11.285'
修改src/main/resources/application.yml:
src/main/resources/application.ymlaws: lambda: functions: vat: functionName: isbn-validator qualifer: isbn region: eu-west-3 # Paris Region
创建一个接口:
src/main/java/example/micronaut/IsbnValidator.javapackage example.micronaut;import io.micronaut.http.annotation.Body;import io.reactivex.Single;public interface IsbnValidator { Single\u0026lt;IsbnValidationResponse\u0026gt; validateIsbn(@Body IsbnValidationRequest req); }
创建一个@FunctionClient:
src/main/java/example/micronaut/FunctionIsbnValidator.javapackage 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(\u0026quot;isbn-validator\u0026quot;) Single\u0026lt;IsbnValidationResponse\u0026gt; validateIsbn(@Body IsbnValidationRequest req);}
关于上面这些代码有几点值得注意:
最后一步是修改gateway的BookController,让它调用函数。
src/main/java/example/micronaut/BooksController.javapackage 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(\u0026quot;isAuthenticated()\u0026quot;)@Controller(\u0026quot;/api\u0026quot;) 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(\u0026quot;/books\u0026quot;) Flowable\u0026lt;Book\u0026gt; findAll() { return booksFetcher.fetchBooks() .flatMapMaybe(b -\u0026gt; isbnValidator.validateIsbn(new IsbnValidationRequest(b.getIsbn())) .filter(IsbnValidationResponse::getValid) .map(isbnValidationResponse -\u0026gt; b) ) .flatMapMaybe(b -\u0026gt; inventoryFetcher.inventory(b.getIsbn()) .filter(stock -\u0026gt; stock \u0026gt; 0) .map(stock -\u0026gt; { b.setStock(stock); return b; }) ); }}
我们通过构造函数注入了IsbnValidator。调用远程函数对程序员来说是透明的。
下面的图片说明了我们在这一系列文章中开发的应用程序:
关于Micronaut的更多内容,请访问。
Sergio del AmoCaballero是一名手机应用程序(iOS、Android,后端由Grails/Micronaut驱动)开发者。自2015年起,Sergio del Amo为Groovy生态系统和微服务维护着一个新闻源。
查看英文原文:
转载地址:http://xoxnx.baihongyu.com/