micronaut
重要要点
- 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
意味着我们要跟踪100%的请求。 在生产中,您可能希望设置较低的百分比。
在测试中禁用跟踪:
src/test/resources/application-test.yml
tracing:
zipkin:
enabled: false
这就对了。 只需进行最少的配置更改,就可以将分布式跟踪集成到Micronaut中。
运行应用
让我们运行该应用程序,并查看分布式跟踪集成操作。 在第一篇文章中,我们将Consul服务发现集成到了我们的应用程序中。 因此,在启动微服务之前,您需要同时启动Zipkin和Consul。 当我们启动微服务时,它们将在Consul服务发现中注册自己。 当我们与他们联系时,他们会将跨度发送到Zipkin。
要启动微服务,Gradle为此提供了一个方便的标记(-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令牌(JWT)是一种开放的行业标准RFC 7519方法,用于安全地表示两方之间的声明。
Micronaut开箱即用,具有生成,签名和/或加密以及验证JWT令牌的功能。
我们将把JWT身份验证集成到我们的应用程序中。
更改gateway
以支持JWT
网关微服务将负责生成和传播JWT令牌。
修改build.gradle
以将micronaut-security-jwt
依赖性添加到每个微服务( gateway, inventory
和books
):
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
启用令牌编写器,该编写器将在HTTP标头中为开发人员透明地写入JWT令牌。 -
micronaut.security.token.propagation.service-id-regex
设置一个正则表达式,该正则表达式与用于令牌传播的服务相匹配。 我们正在匹配该应用程序中的其他两项服务。
使用Micronaut,您可以使用@Secured
注释在Controller或Controller的Action级别配置访问。
用@ Secured("isAuthenticated()"
)注释BookController.java
。 它仅允许经过身份验证的用户访问。 记得用注解
@Secured("isAuthenticated()")
也是库存和书籍微服务的BookController
类。
公开的/login
端点在被调用时,将尝试根据任何可用的AuthenticationProvider
bean对用户进行AuthenticationProvider
。 为简单起见,我们将允许访问两个用户sherlock和watson; 向亚瑟·柯南·道尔爵士的角色致敬。 创建一个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
注释控制器@Secured
,我们还需要修改application.yml
以创建配置,使我们能够验证在网关生成和签名的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为此提供了一个方便的标记(-平行):
./gradlew -parallel run
您可以运行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承载令牌规范。 我们可以通过HTTP授权标头调用/api/books
端点,以提供/ login响应中收到的JWT。
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的功能,以验证书籍的ISBN代码。
mn create-function example.micronaut.isbn-validator
注意:我们使用了Micronaut CLI中包含的create-function命令。
验证方式
我们将创建一个Singleton来处理ISBN 10验证。
创建一个接口来封装操作:
package example.micronaut;
import javax.validation.constraints.Pattern;
public interface IsbnValidator {
boolean isValid(@Pattern(regexp = "\\d{10}") String isbn);
}
前面的代码清单包含一个javax.validation.constraint
。
Micronaut的验证基于标准框架– JSR 380 ,也称为Bean验证2.0。
Hibernate Validator是验证API的参考实现。
将下一个代码段添加到build.gradle
isbn-validator/build.gradle
compile "io.micronaut.configuration:micronaut-hibernatevalidator"
创建一个实现IsbnValidator.java的Singleton
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);
}
}
与前面的代码清单一样,您将在Micronaut中将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"));
}
}
前面的代码清单证明,如果我们尝试使用11位数字的字符串调用该方法,则会引发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应用程序上下文的一部分运行,以简化测试。 该应用程序已经在类路径上包含了功能网络和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插件公开的gradle任务。
$ 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,超时时间为25s。
从另一个微服务使用功能/ AWS Lambda。
我们将在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
并定义一个与我们部署到AWS Lambda的函数同名的isbn-validator
函数:
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
批注允许将引入建议应用于接口,以使接口定义的方法成为应用程序配置的远程函数的调用者。 - 像在application.yml中一样使用函数名称isbn-validator。
最后一步是修改网关B ookController
以调用该函数。
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
的bean。 对程序员来说,使用远程功能是透明的 。
结论
下图显示了我们在阅读完这些文章后的应用程序:
- 我们有三种微服务(Java,Groovy和Kotlin微服务)。
- 这些微服务使用Consul进行服务发现。
- 这些微服务使用Zipkin作为分布式跟踪服务。
- 我们添加了第四个微服务,该功能已部署到AWS Lambda
- 微服务之间的通信是安全的。 通过网络允许的每个请求都在Authorization Http标头中包含JWT令牌。 JWT令牌通过内部请求自动传播。
请访问该网站以了解有关Micronaut的更多信息。
关于作者
是一家专门开发由Grails / Micronaut后端提供支持的手机应用程序(iOS,Android)的开发人员。 自2015年以来,Sergio del Amo围绕Groovy生态系统和微服务撰写新闻通讯Groovy Calamari 。 Groovy,Grails,Micronaut,Gradle等...
micronaut