Java应用程序的简单令牌认证

“我喜欢编写身份验证和授权代码。” 〜从来没有Web开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。

JSON Web令牌已Swift成为保护Web应用程序安全的标准,并取代了Cookie和会话等旧技术。 正确使用它们可以解决一系列安全问题,包括跨站点脚本攻击(XSS),中间人攻击(MITM)和跨站点请求伪造(CSRF)。 它们还为我们提供了可检查的元数据和强大的密码签名的好处。 在本文中,我将深入探讨JWT。 首先,我将介绍一些理论基础,解释它们如何工作。 之后,我将向您展示如何使用Okta配置Spring Boot应用程序以使用JWT身份验证。

JSON Web令牌是一个开放标准,并且有许多可用的库允许创建,验证和检查JWT。 您将使用Java JWT (又名JJWT),这是一个Java库,提供端到端JWT的创建和验证。 JJWT由Les Hazlewood创建, Les Hazlewood是Stormpath的前联合创始人兼CTO Apache Shiro的主要撰稿人,目前是Okta自己的高级架构师。 根据Apache 2.0许可,它是开源的。

了解JWT及其在身份验证中的作用

首先让我们研究一下authenticationtoken在这种情况下的含义。

身份验证证明用户就是他们所说的真实身份

令牌是一个独立的信息块。 它可能具有内在价值,也可能没有。 我将向您展示一种特定类型的令牌, 它确实具有内在价值,并通过会话ID解决了许多问题。

什么是JSON Web令牌? JWT是一种开放标准( RFC 7519 ),用于使用JSON在各方之间作为数字签名的字符串令牌传输信息。 可以使用HMAC算法或使用RSAECDSA的公用/专用密钥对对它们进行签名

换句话说:JWT是JSON令牌,是URL安全,紧凑且自包含的字符串。 通常,它们携带有关用户的已验证身份的信息。 它们通常经过编码和加密。 它们Swift成为网络上令牌实现的事实上的标准。 URL安全意味着令牌字符串可以在URL中使用,因为所有特殊字符都已被编码为简单的字母数字字符。 JWT也被认为是不透明的,因为字符串本身没有解码或解密就不会提供任何信息。

令牌通常被认为是一种授权机制,但是它们也可以用作在Web应用程序和服务器之间安全存储和传输信息的一种方式,与使用会话ID的方式非常相似。

将JWT与OAuth 2.0结合使用

许多OAuth 2.0实现都将JWT用于其访问令牌。 应该指出,OAuth 2.0和JWT规范是彼此完全独立的,彼此之间没有任何依赖关系。 将JWT用作OAuth 2.0的令牌机制会带来很多好处,如下所示。

无论使用哪种JWT实施,都必须将漂亮的Web令牌存储在某个地方。 Cookie和HTML5 Web存储是两个流行的选项。 两种选择都有好处和潜在的风险。 对此的讨论不在本文讨论范围之内,但是值得阅读上面提到的典型攻击:跨站点脚本攻击(XSS),中间人攻击(MITM)和跨站点请求伪造(CSRF)。 Okta使用HTML5网络存储。

无论将JWT存储在何处,都可以用JWT完全替换会话ID,并获得直接从会话ID / JWT访问元信息的额外好处。

看一下JWT的结构

那是很多首字母缩写词。 让我们看一下JWT。 在野外,JWT很害羞。 有些人甚至可能说他们太丑陋或无聊了(取决于您对206个字符串的感觉)。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vdHJ1c3R5YXBwLmNvbS8iLCJleHAiOjEzMDA4MTkzODAsInN1YiI6InVzZXJzLzg5ODM0NjIiLCJzY29wZSI6InNlbGYgYXBpL2J1eSJ9.43DXvhrwMGeLLlP4P4izjgsBB2yrpo82oiUPhADakLs

就像生活中的大多数事情一样,请不要凭空判断JWT(请记住,它们是不透明的)。 如果仔细看,您会发现字符串中有两个句点。 这些界定了JWT的不同部分:

  1. 标头
  2. 有效载荷/主体(或要求)
  3. 密码签名。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.
eyJpc3MiOiJodHRwOi8vdHJ1c3R5YXBwLmNvbS8iLCJleHAiOjEzMDA4MTkzODAsInN1YiI6InVzZXJzLzg5ODM0NjIiLCJzY29wZSI6InNlbGYgYXBpL2J1eSJ9
.
43DXvhrwMGeLLlP4P4izjgsBB2yrpo82oiUPhADakLs

窥视令牌头

我知道我说过有些人认为JWT很无聊。 甚至不透明。 但是,如果您知道如何与他们交谈,JWT非常有趣。 让我们解码示例​​JWT并查看其中的内容。

标头只是通过Base64Url编码的。 它告诉我们令牌的类型和使用的哈希算法,通常是HMAC SHA256或RSA。

{
  "typ": "JWT",
  "alg": "HS256"
}

顺便说一下, jsonwebtoken.io是用于编码和解码JWT的出色在线工具。

查看有效载荷

令牌的第二部分是有效负载声明

{
  "iss": "http://trustyapp.com/",
  "exp": 1300819380,
  "sub": "users/8983462",
  "scope": "self api/buy"
}

有效负载包含声明 。 声明是有关实体(通常是用户)和任何其他数据的声明。 索赔有以下三种:

  1. 已注册的声明: RFC 7519规范中定义的一组建议的声明。 一些示例是issexpaud
  2. 公共声明:可由令牌用户定义但应遵循命名约定以避免冲突的用户定义声明(应在IANA JSON Web令牌注册表中定义,或定义为包含抗冲突名称空间的URI),因为它们在公共命名空间中。
  3. 私人声明:用于在达成协议的各方之间共享信息的任意自定义声明(不必担心名称冲突,因为它们是私有的)。

在上面的示例中:

  • iss是发行令牌的人。 这是已注册的索赔。
  • exp是令牌过期的时间。 也是已注册的索赔。
  • 是主题。 通常是一个用户标识符。 也是已注册的索赔。
  • scope是OAuth 2.0常用的自定义私人声明。

范围声明通常用于提供授权信息。 例如,让应用程序知道用户有权访问应用程序的哪一部分。 当然,这并不能免除服务器执行自己的授权检查的责任。 Web应用程序安全性的一般原则是冗余。 客户端应用程序提供一个检查点,服务器提供另一个检查点。

JSON数据经过Base64URL编码以创建编码的有效负载。 编码的标头和有效负载用于创建签名。

检查令牌签名

签名是JWT结构的最后一部分。 它获取标头,然后有效负载向哈希算法添加一个秘密,并吐出与JWT其余部分中未更改的数据相对应的哈希。 使用签名,客户端应用程序和服务器可以验证它们接收的令牌是原始的,未更改的令牌。

HMACSHA256( 
    base64UrlEncode(header) + "." + 
    base64UrlEncode(payload), 
    secret
)

了解此签名不提供机密性是非常重要的。 此信息是公开可见的。 签名保证令牌未被篡改,但不会隐藏数据(小孩可以在叔叔的iPhone 4上解码Base64)。 如果要发送敏感信息,必须对JWT进行加密。

通常以从属权利要求的形式存储用户标识符的做法。 当JWT签名时,它称为JWS。 加密后,称为JWE。

无国籍是JWT的一大好处。 服务器不需要存储任何会话数据。 它可以全部存储在令牌中,并在应用程序和服务器之间来回传递。 这看似是一个奇怪的电子飞盘游戏,但此模型的伸缩性很好(只要会话状态相对较小,它应该确实如此)。 从JWT解码会话状态要比在每个请求上访问数据库仅从会话存储中检索一些基本用户状态信息快得多,并且性能更高。

使用Java创建和验证JWT

我已经提到过我们的JJWT项目吗? 查看其GitHub页面 。 这是针对Java的完全开源的JWT解决方案。

让我们看一个使用JJWT创建JWT的示例。

import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.SignatureAlgorithm;  
  
byte[] key = getSignatureKey();  
  
String jwt = Jwts.builder().setIssuer("http://trustyapp.com/")  
    .setSubject("users/1300819380")  
    .setExpiration(expirationDate)  
    .put("scope", "self api/buy")  
    .signWith(SignatureAlgorithm.HS256,key)  
    .compact();

该库使用了流畅的Builder API。 请注意用于设置声明的设置器。 标准声明具有预定义的设置器,例如setSubject("users/1300819380") ,而自定义声明使用键值put()方法。 例如范围声明, put("scope", "self api/buy")

验证JWT一样容易。

String subject = "HACKER";  
  
try {  
    Jws jwtClaims = Jwts.parser().setSigningKey(key).parseClaimsJws(jwt);  
    subject = claims.getBody().getSubject();  
    // OK, you can trust this JWT  
} 
catch (SignatureException e) {  
    // don't trust this JWT!  
}

如果对JWT进行了任何篡改,则解析声明将引发SignatureException并且subject变量的值将保持HACKER 。 如果它是有效的JWT,则将从中提取subjectclaims.getBody().getSubject()

了解Java中用于令牌认证的OAuth 2.0

稍后,您将使用Okta的OAuth 2.0实现来创建Spring Boot应用程序。 但是首先,您应确保了解什么是OAuth,什么不是OAuth。

简而言之,OAuth 2.0是“授权的行业标准协议”(来自OAuth.net网站 )。 授权意味着它为应用程序提供了一种方法,以确保用户有权执行操作或访问资源。 OAuth 2.0 不提供用于验证用户身份的工具。

那是身份验证。

还有另一个称为OpenID Connect或OIDC的协议层,通常与提供身份验证的OAuth 2.0配对。 OIDC建立在OAuth 2.0的基础上,并提供一种验证用户身份的方法,通常是让用户使用用户名和密码登录,或者使用许多社交登录选项之一。 因为OIDC 确实与OAuth 2.0一起验证用户的身份,所以它们一起为Web应用程序和服务器提供了完整的身份验证和授权协议。

记得:

  • OIDC是身份验证,还是我是谁?
  • OAuth是授权,还是该怎么办?

在Java中配置Okta OIDC应用程序以进行令牌认证

很快,您将像专家一样生成和验证JWT。 但是首先,您需要转到developer.okta.com并创建一个OpenID Connect(OIDC)应用程序。 如果尚未注册免费的开​​发者帐户。 我是否提到过它完全免费?

登录到Okta开发人员仪表板。 转到应用程序 ,然后单击添加应用程序按钮。

选择Web应用程序类型,然后单击“ 下一步”

令牌认证

在下一页上,您需要给新应用程序起一个易记的名称。 您可以随意命名。 您还需要选中“ 客户端凭据”复选框。 这将激活您将要使用的client_credentials授予类型。

令牌认证

单击完成

请注意下一页底部的客户端ID客户端密钥 。 稍后您将需要这些。

令牌认证

安装HTTPie

我们将使用一个很棒的命令行实用工具来运行一些示例:HTTPie。 如果尚未安装,请访问其网站并安装。

申请JWT

时机已到。 我知道你很兴奋 您终于要见识JWT。

我知道您很想离开,但是您还需要执行一个初步的步骤。 您需要对上述Okta OIDC应用程序中的客户端ID和客户端密钥进行编码,以便在HTTP基本授权标头中使用。

通用格式为:

Authorization: Basic Base64Encode(< your client id >:< your client secret >)

注意中间的: 。 获取您的客户ID,并使用冒号将其加入到您的客户秘密中。 Base64对结果字符串进行编码(提示:使用base64encode.org )。 然后将其包含在您的请求中。 它看起来像这样:

Authorization: Basic ABChZzU4NDg5YW1aTDCBCB4waDc6TUp3YWN4RU5WNzQ1bEdQNWJPdlFETV9iaDE5NGp1eHQ3SXJfdWEzQQ==

互联网上有各种实用程序可以为您完成此任务。 尽管这些对于教程和从头开始开发可能会有所帮助,但出于显而易见的原因,但使用它们来编码生产凭证并不是一个好主意。

接下来,您需要的是Okta OIDC应用程序的令牌请求URL。 这将是您的Okta授权服务器基本URL加/v1/token 。 从顶部菜单转到API ,然后选择“ 授权服务器” 。 查看默认服务器的Issuer URI 。 添加/v1/token

例如,类似于: https://{yourOktaDomain}/oauth2/default/v1/token

使用HTTPie,您的终端命令将如下所示:

http -f POST https://{yourOktaDomain}/oauth2/default/v1/token \
'Authorization: Basic MG9hZzU4NDg5YW1aTDBNRU4wa...' \
grant_type=client_credentials

这里发生了什么?

-f是表单标志。 Content-Type设置为application/x-www-form-urlencoded; charset=utf-8 application/x-www-form-urlencoded; charset=utf-8并使用key=value表示法分析命令行以获取表单数据。

POST是HTTP请求类型。

https://{yourOktaDomain}/oauth2/default/v1/token是令牌请求URL。

'Authorization: Basic ...'设置基本auth标头。

grant_type=client_credentials是一个表单值,它告诉Okta我们正在请求的授予类型。 一秒钟内将对此进行更多介绍。

如果运行此命令,请继续! –您将收到invalid scope错误。

HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store
Connection: close
Content-Type: application/json;charset=UTF-8
...

{
    "error": "invalid_scope",
    "error_description": "The authorization server resource does not have any configured default scopes, 'scope' must be provided."
}

使用client_credentials授予类型实际上只是一种方便。 这是一种授予类型,它使我们无需遵循浏览器重定向即可请求JWT。 client_credentials通常用于API交互。

根据OAuth规范 ,“客户端使用客户端凭据授予类型来获取用户上下文之外的访问令牌。 客户端通常使用它来访问有关其自身的资源,而不是访问用户的资源。”

将Okta用作单一登录提供程序(一种更常见的用例)时,可以使用“ 授权代码授予” 。 这种授予类型(其中应用程序允许用户登录并交换访问令牌的授权代码),但是需要一系列重定向,而这些重定向将很难从命令行客户端(例如HTTPie)进行管理。 因此,本教程使用“客户端证书”授予类型来方便地演示一些基本的OAuth / JWT功能。

您希望能想到:“好极了,但是范围误差呢?”

很高兴你问!

范围提供了一种定义和限制令牌授予的访问权限的方法。 在对用户进行授权和身份验证时,通常会使用一系列范围,例如openid email profile 。 这些作用域告诉服务器,该应用程序希望访问用户的个人资料,电子邮件地址,并希望发出OpenID身份验证请求。

客户证书授予类型要求我们定义一个自定义范围。 这就是invalid scope错误所抱怨的。

让我们修复它!

添加自定义范围

返回至developer.okta.com 。 在顶部菜单中,转到API授权服务器

从服务器列表中单击默认服务器。

单击“ 作用域”选项卡,然后单击“ 添加作用域”按钮。

将范围命名为“ customScope”,对其进行描述,然后单击“ 创建” 。 您需要匹配的名称,但是描述是任意的。

令牌认证

再次运行令牌请求

重新运行请求,这次使用自定义范围。

http -f POST https://{yourOktaDomain}/oauth2/default/v1/token \
'Authorization: Basic MG9hZzU4NDg5YW1aTDBN...' \
grant_type=client_credentials \
scope=customScope

这次您将获得令牌!

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store
Connection: Keep-Alive
Content-Type: application/json;charset=UTF-8
Pragma: no-cache
...
{
    "access_token": "eyJraWQiOiJldjFpay1DS3UzYjJXS3QzSVl1MlJZc3...",
    "expires_in": 3600,
    "scope": "customScope",
    "token_type": "Bearer"
}

注意Cache-ControlPragma标头。 您不希望此响应被缓存在任何地方。 access_token是浏览器在后续请求中将使用的内容。 请记住,不需要将JWT用作OAuth 2.0访问令牌。 它们非常方便,因为您可以在其中编码防篡改(并且可能是机密的)元数据。

使用您的访问令牌

要在请求中包含访问令牌,请使用类型为BearerAuthorization标头。 下面是一个示例GET请求。

GET /admin HTTP/1.1
Authorization: Bearer eyJraWQiOiJldjFpay1DS3UzYjJXS3QzSVl1MlJZc3...

输入Spring Boot以使用Java进行令牌认证

您准备好使用Java吗? 我已经准备好使用Java。

您要做的第一件事是从GitHub存储库克隆我们的示例应用程序

现在,使用以下终端命令检出start分支:

git clone -b start https://github.com/oktadeveloper/okta-spring-boot-token-auth-example.git

此应用程序非常简单。 至少,它看起来超级简单。 实际上,在后台,Spring Boot和Okta做出了相当大的努力,以为您提供功能齐全的REST资源服务器,该服务器具有使用OAuth 2.0和Okta OIDC应用程序进行的JWT令牌身份验证的功能。 AM!

唯一需要进行的配置是在src/main/resources/application.yml文件中。 您需要从上面创建的几个步骤的Okta OIDC应用程序中填写您的客户端ID客户端密钥 。 您使用这些来生成令牌。 它们大致(如果是斜视,则有点类似)对应于OIDC应用程序的用户名和密码。

server:  
  port: 8080  
                                         
okta:  
  oauth2: 
    issuer: https://{yourOktaDomain}/oauth2/default  
    clientId: {yourClientId}
    clientSecret: {yourClientSecret}

这就是您需要做的! 至此,您已经拥有了一个功能齐全(如果不是很令人兴奋的话)的Spring Boot资源服务器。

该应用程序的入口点位于Application.java文件中:

@EnableResourceServer  
@SpringBootApplication  
public class Application {  
    public static void main(String[] args) {  
        SpringApplication.run(Application.class, args);  
    }
}

您会注意到这是老式的main()方法所在的地方。 这些天感觉有点像遗物,但这就是所有魔力的开始。 在每个愚蠢的猫录像和Facebook激烈的政治辩论背后,都有一个main()方法开始了这一切。

您正在使用SpringApplication.run()方法来引导Spring框架,该框架将加载Application类。 这将拾取@EnableResourceServer@SpringBootApplication批注。 @SpringBootApplication告诉Spring加载Spring Boot。 @EnableResourceServer将Spring Boot应用程序配置为通过OAuth令牌(可能与OAuth 2.0单一登录相对)对请求进行身份验证。

@EnableResourceServer有几个含义,值得指出。 如果您看一下有关注释文档 ,则会看到几个要点:如果要配置资源服务器,则需要定义ResourceServerConfigurerAdapter bean; 并以3的硬编码顺序添加了WebSecurityConfigurerAdapter bean。

你为什么要在乎呢? 因为在更复杂的Web应用程序中,您将要同时使用ResourceServerConfigurerAdapterWebSecurityConfigurerAdapter来配置权限。 与使用@EnableOAuth2Sso批注时所做的WebSecurityConfigurerAdapter ,这与简单地使用WebSecurityConfigurerAdapter @EnableOAuth2Sso ,所以我想警告您。 通常,资源服务器端点将以/api或其他内容开头,并由ResourceServerConfigurerAdapter进行配置和保护,而其他任何纯HTML端点将由WebSecurityConfigurerAdapter进行配置。 但是,您需要将@Order(Ordered.HIGHEST_PRECEDENCE)添加到WebSecurityConfigurerAdapter以使其优先于具有硬编码顺序的默认顺序。

如果要查看如何完成此操作的更具体示例,请查看full-config分支 。 您也可以在最后的链接中浏览Siva Tech文章。

除了这些,让我们看一下HelloController类。

@RestController
public class HelloController {  
  
    @RequestMapping("/")  
    public String home(java.security.Principal user) {  
        return "Hello " + user.getName();  
    }
}

@RestController批注是@Controller@ResponseBody批注的组合。

@Controller告诉Spring Boot此类是控制器类,并查找URL请求到内部方法的映射。

@ResponseBody指示控制器方法返回原始响应数据,并且不映射到模板名称。 这就是它返回简单字符串的方式,而不是使用Spring的模型和视图模板系统。

@RequestMapping("/")表示将home()方法映射到基本URL。

注意使用了依赖注入来获取java.security.Principal 。 这项工作需要大量自动调整。 Spring和Okta会根据OAuth 2.0和OpenID Connect规范共同验证令牌并进行来回通信,对用户进行身份验证,并提供JWT授权令牌,该令牌包含具有用户“名称”的元数据。

运行Spring Boot App

好的。 一起让它成为现实!

使用以下命令运行Spring Boot应用程序: ./gradlew bootRun

您应该看到很多文本以如下形式结尾:

...
[main] INFO org.springframework.jmx.export.annotation.AnnotationMBeanExporter - Registering beans for JMX exposure on startup
[main] INFO org.springframework.boot.web.embedded.tomcat.TomcatWebServer - Tomcat started on port(s): 8080 (http) with context path ''
[main] INFO com.okta.springboottokenauth.Application - Started Application in 21.209 seconds (JVM running for 21.709)
[2018-10-11 10:31:58.646] - 22235 INFO [http-nio-8080-exec-1] --- org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]: Initializing Spring FrameworkServlet 'dispatcherServlet'
[http-nio-8080-exec-1] INFO org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'dispatcherServlet': initialization started
[http-nio-8080-exec-1] INFO org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'dispatcherServlet': initialization completed in 13 ms

使用JWT进行身份验证

现在运行Spring Boot应用程序,使用HTTPie运行不带令牌的GET请求:

http GET http://localhost:8080

您将获得401 /未经授权:

HTTP/1.1 401
Cache-Control: no-store
Content-Type: application/json;charset=UTF-8
...
{
    "error": "unauthorized",
    "error_description": "Full authentication is required to access this resource"
}

重新运行它,这次包括您的令牌(取决于经过了多少时间,您可能需要请求一个新令牌):

http GET :8080 'Authorization: Bearer eyJraWQiOiJldjFpay1DS3UzYjJXS3QzSVl1...'

提示:如果收到显示“ Invalid JOSE Header kid”的invalid_token错误,则很有可能用不正确的值更新了application.yml

假设您的令牌仍然有效,您将获得一个HTTP 200:

HTTP/1.1 200
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 26
Content-Type: text/plain;charset=UTF-8
...

Hello 0oag58489amZL0MEN0h7

0oag58489amZL0MEN0h7将是您的客户ID。

请记住,您使用的是client_credentials授予类型,并且已将客户端ID客户端密钥作为凭据发送给服务器。 因此,授权服务器将您的客户ID作为您的“名称”发送回去。 在不同的情况下,例如使用授权码授予,这将是用户的名称(或者可能是他们的电子邮件地址或用户名)。

了解有关使用Java进行令牌身份验证和构建安全应用程序的更多信息

了解令牌身份验证对于构建现代Web应用程序至关重要。 有两种用于签名和加密令牌的主要方法:哈希和公钥/私钥。 两种方法都是互联网安全的基础。 在HMAC上查看Wikipedia页面 ,以继续学习JWT中使用的基于哈希的消息认证代码(HMAC)。 要了解公共/专用密钥加密, Red Hat进行了出色的介绍

一旦您对这两种技术真正感到满意,就可以看一下关于Medium的有关JWT的出色教程

您可能还需要查看RFC规范

在Spring结束时,我发现Siva Tech Lab的本教程对说明如何使用OAuth 2.0实施Spring Boot应用程序很有帮助。

以下是Okta博客的其他一些链接,可帮助您继续前进:

如果您对此帖子有任何疑问,请在下面添加评论。 有关更多精彩内容, 在Twitter上关注@oktadev在Facebook上关注我们,或订阅我们的YouTube频道

Java Apps的简单令牌身份验证最初于2018年10月16日发布在Okta开发人员博客上。

“我喜欢编写身份验证和授权代码。” 〜从来没有Web开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。

翻译自: https://www.javacodegeeks.com/2018/10/simple-token-authentication-java-apps.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值