關鍵要點
- 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.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):
./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 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