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.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。

image

運行./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秒。

image

從另一個微服務中調用函數

我們將在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的更多內容,請訪問官方網站

關於作者

image

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章