micronaut_Micronaut教程:第2部分:轻松分布式跟踪,JWT安全性和AWS Lambda部署

micronaut

重要要点

  • Micronaut提供了与多个分布式跟踪解决方案的无缝集成,例如Zipkin和Jaeger
  • 框架“开箱即用”地提供了几种安全解决方案,例如基于JWT的身份验证。
  • Micronaut提供了诸如“令牌传播”之类的功能,以简化微服务之间的安全通信。
  • 由于内存占用量少,Micronaut能够在无服务即服务(FaaS)的无服务器环境中运行。

本系列的第一篇文章中 ,我们使用基于JVM的Micronaut框架开发和部署了三个微服务。 在第二篇教程文章中,我们将为我们的应用程序添加一些功能:分布式跟踪,通过JWT的安全性和无服务器功能。 此外,我们将讨论Micronaut提供的用户输入验证功能。

分布式跟踪

将我们的系统分解为更细,更细粒度的微服务会带来多种好处,但是在监视生产中的系统时也会增加复杂性。

您应该假设您的网络受到恶意实体的困扰,这些实体随时准备释放他们的愤怒。 ― Sam Newman,建筑微服务

Micronaut与JaegerZipkin (顶级的开源分布式跟踪解决方案)本机集成。

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, inventorybooks ):

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()); 
    } 
}

更改inventorybooks以支持JWT

对于服务inventorybooks ,除了添加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等...

翻译自: https://www.infoq.com/articles/micronaut-tracing-security-serverless/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

micronaut

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值