HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

Micronaut 教程(二):分布式跟踪、JWT 安全和 AWS Lambda 部署

  • 2018-12-15
  • 本文字数:10489 字

    阅读完需:约 34 分钟

Micronaut教程(二):分布式跟踪、JWT安全和AWS Lambda部署

关键要点

  • 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)用来启动微服务:


./gradlew -parallel run
复制代码


你可以通过 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.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: "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):


./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 "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); } }}
复制代码


上面的代码做了几件事:


  • 使用 @FunctionBean 注解了一个返回函数的方法。

  • 你可以在函数中使用 Micronaut 的编译时依赖注入。我们通过构造函数注入了 IsbnValidator。


函数也可以作为 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);}
复制代码


关于上面这些代码有几点值得注意:


  • FunctionClient 注解可以在接口上应用引入通知(introduction advice),这样接口定义的方法就会成为远程函数的调用者。

  • 使用函数名 isbn-validator,与 application.yml 定义的一样。


最后一步是修改 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


2018-12-15 09:002060
用户头像

发布了 731 篇内容, 共 449.1 次阅读, 收获喜欢 2002 次。

关注

评论 1 条评论

发布
暂无评论
发现更多内容
Micronaut教程(二):分布式跟踪、JWT安全和AWS Lambda部署_语言 & 开发_Sergio del Amo Caballero_InfoQ精选文章