Spring Security OAuth2.0(一):开发指南

官方原文:http://projects.spring.io/spring-security-oauth/docs/oauth2.html

更全译文:https://mp.weixin.qq.com/s/LwQxGzG17A3NYTDTMry9Kg

 

目录

1.OAuth2.0 提供者实现原理:

2.OAuth 2.0客户端


内容

1.OAuth2.0 提供者实现原理:


Spring OAuth2.0提供者实际上分为:

  • 授权服务 Authorization Service.
  • 资源服务 Resource Service.

虽然这两个提供者有时候可能存在同一个应用程序中,但在Spring Security OAuth中你可以把

他它们各自放在不同的应用上,而且你可以有多个资源服务,它们共享同一个中央授权服

务。

所有获取令牌的请求都将会在Spring MVC controller endpoints中进行处理,并且访问受保护

的资源服务的处理流程将会放在标准的Spring Security请求过滤器中(filters)。

下面是配置一个授权服务必须要实现的endpoints:

  • AuthorizationEndpoint:用来作为请求者获得授权的服务,默认的URL是/oauth/authorize.
  • TokenEndpoint:用来作为请求者获得令牌(Token)的服务,默认的URL是/oauth/token.

下面是配置一个资源服务必须要实现的过滤器:

  • OAuth2AuthenticationProcessingFilter:用来作为认证令牌(Token)的一个处理流程过滤器。只有当过滤器通过之后,请求者才能获得受保护的资源。

配置提供者(授权、资源)都可以通过简单的Java注解@Configuration来进行适配,你也可以使用基于XML的声明式语法来进行配置,如果你打算这样做的话,那么请使用http://www.springframework.org/schema/security/spring-security-oauth2.xsd来作为XML的schema(即XML概要定义)以及使用http://www.springframework.org/schema/security/oauth2来作为命名空间。

一、授权服务配置:


配置一个授权服务,你需要考虑几种授权类型(Grant Type),不同的授权类型为客户端(Client)提供了不同的获取令牌(Token)方式,为了实现并确定这几种授权,需要配置使用 ClientDetailsService 和 TokenService 来开启或者禁用这几种授权机制。到这里就请注意了,不管你使用什么样的授权类型(Grant Type),每一个客户端(Client)都能够通过明确的配置以及权限来实现不同的授权访问机制。这也就是说,假如你提供了一个支持"client_credentials"的授权方式,并不意味着客户端就需要使用这种方式来获得授权。下面是几种授权类型的列表,具体授权机制的含义可以参见RFC6749(中文版本):

  • authorization_code:授权码类型。
  • implicit:隐式授权类型。
  • password:资源所有者(即用户)密码类型。
  • client_credentials:客户端凭据(客户端ID以及Key)类型。
  • refresh_token:通过以上授权获得的刷新令牌来获取新的令牌。

可以用 @EnableAuthorizationServer 注解来配置OAuth2.0 授权服务机制,通过使用@Bean注解的几个方法一起来配置这个授权服务。下面咱们介绍几个配置类,这几个配置是由Spring创建的独立的配置对象,它们会被Spring传入AuthorizationServerConfigurer中:

  • ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
  • AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束.
  • AuthorizationServerEndpointsConfigurer:用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。

(译者注:以上的配置可以选择继承AuthorizationServerConfigurerAdapter并且覆写其中的三个configure方法来进行配置。)

配置授权服务一个比较重要的方面就是提供一个授权码给一个OAuth客户端(通过 authorization_code 授权类型),一个授权码的获取是OAuth客户端跳转到一个授权页面,然后通过验证授权之后服务器重定向到OAuth客户端,并且在重定向连接中附带返回一个授权码。

如果你是通过XML来进行配置的话,那么可以使用 <authorization-server/> 标签来进行配置。

(译者注:想想现在国内各大平台的社会化登陆服务,例如腾讯,用户要使用QQ登录到某个网站,这个网站是跳转到了腾讯的登陆授权页面,然后用户登录并且确定授权之后跳转回目标网站,这种授权方式规范在我上面提供的链接*RFC6749*的第4.1节有详细阐述。)

 

配置客户端详情信息(Client Details):

ClientDetailsServiceConfigurer (AuthorizationServerConfigurer 的一个回调配置项,见上的概述) 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),有几个重要的属性如下列表:

  • clientId:(必须的)用来标识客户的Id。
  • secret:(需要值得信任的客户端)客户端安全码,如果有的话。
  • scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
  • authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
  • authorities:此客户端可以使用的权限(基于Spring Security authorities)。

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过 ClientDetailsManager 接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。

(译者注:不过我并没有找到 ClientDetailsManager 这个接口文件,只找到了 ClientDetailsService)

 

管理令牌(Managing Token):

AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,在使用这些操作的时候请注意以下几点:

  • 当一个令牌被创建了,你必须对其进行保存,这样当一个客户端使用这个令牌对资源服务进行请求的时候才能够引用这个令牌。
  • 当一个令牌是有效的时候,它可以被用来加载身份信息,里面包含了这个令牌的相关权限。

当你自己创建 AuthorizationServerTokenServices 这个接口的实现时,你可能需要考虑一下使用 DefaultTokenServices 这个类,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都实现了TokenStore接口:

  • InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
  • JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的classpath当中。
  • JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。

 

JWT令牌(JWT Tokens):

使用JWT令牌你需要在授权服务中使用 JwtTokenStore,资源服务器也需要一个解码的Token令牌的类 JwtAccessTokenConverter,JwtTokenStore依赖这个类来进行编码以及解码,因此你的授权服务以及资源服务都需要使用这个转换类。Token令牌默认是有签名的,并且资源服务需要验证这个签名,因此呢,你需要使用一个对称的Key值,用来参与签名计算,这个Key值存在于授权服务以及资源服务之中。或者你可以使用非对称加密算法来对Token进行签名,Public Key公布在/oauth/token_key这个URL连接中,默认的访问安全规则是"denyAll()",即在默认的情况下它是关闭的,你可以注入一个标准的 SpEL 表达式到 AuthorizationServerSecurityConfigurer 这个配置中来将它开启(例如使用"permitAll()"来开启可能比较合适,因为它是一个公共密钥)。

如果你要使用 JwtTokenStore,请务必把"spring-security-jwt"这个依赖加入到你的classpath中。

 

配置授权类型(Grant Types):

授权是使用 AuthorizationEndpoint 这个端点来进行控制的,你能够使用 AuthorizationServerEndpointsConfigurer 这个对象的实例来进行配置(AuthorizationServerConfigurer 的一个回调配置项,见上的概述) ,如果你不进行设置的话,默认是除了资源所有者密码(password)授权类型以外,支持其余所有标准授权类型的(RFC6749),我们来看一下这个配置对象有哪些属性可以设置吧,如下列表:

  • authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。
  • userDetailsService:如果啊,你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现,或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对象),当你设置了这个之后,那么 "refresh_token" 即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。
  • authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对象),主要用于 "authorization_code" 授权码类型模式。
  • implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
  • tokenGranter:这个属性就很牛B了,当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。

在XML配置中呢,你可以使用 "authorization-server" 这个标签元素来进行设置。

 

配置授权端点的URL(Endpoint URLs):

AuthorizationServerEndpointsConfigurer 这个配置对象(AuthorizationServerConfigurer 的一个回调配置项,见上的概述) 有一个叫做 pathMapping() 的方法用来配置端点URL链接,它有两个参数:

  • 第一个参数:String 类型的,这个端点URL的默认链接。
  • 第二个参数:String 类型的,你要进行替代的URL链接。

以上的参数都将以 "/" 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的第一个参数:

  • /oauth/authorize:授权端点。
  • /oauth/token:令牌端点。
  • /oauth/confirm_access:用户确认授权提交端点。
  • /oauth/error:授权服务错误信息端点。
  • /oauth/check_token:用于资源服务访问的令牌解析端点。
  • /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。

需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问,我们来看看在标准的Spring Security中 WebSecurityConfigurer 是怎么用的。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http .authorizeRequests().antMatchers("/login").permitAll().and()
    // default protection for all resources (including /oauth/authorize)
    .authorizeRequests() .anyRequest().hasRole("USER")
    // ... more configuration, e.g. for form login
}

注意:如果你的应用程序中既包含授权服务又包含资源服务的话,那么这里实际上是另一个的低优先级的过滤器来控制资源接口的,这些接口是被保护在了一个访问令牌(access token)中,所以请挑选一个URL链接来确保你的资源接口中有一个不需要被保护的链接用来取得授权,就如上面示例中的 /login 链接,你需要在 WebSecurityConfigurer 配置对象中进行设置。

令牌端点默认也是受保护的,不过这里使用的是基于 HTTP Basic Authentication 标准的验证方式来验证客户端的,这在XML配置中是无法进行设置的(所以它应该被明确的保护)。

在XML配置中可以使用 <authorization-server/> 元素标签来改变默认的端点URLs,注意在配置 /check_token 这个链接端点的时候,使用 check-token-enabled 属性标记启用。

 

强制使用SSL(Enforcing SSL):

使用简单的HTTP请求来进行测试是可以的,但是如果你要部署到产品环境上的时候,你应该永远都使用SSL来保护授权服务器在与客户端进行通讯的时候进行加密。你可以把授权服务应用程序放到一个安全的运行容器中,或者你可以使用一个代理,如果你设置正确了的话它们应该工作的很好(这样的话你就不需要设置任何东西了)。

但是也许你可能希望使用 Spring Security 的 requiresChannel() 约束来保证安全,对于授权端点来说(还记得上面的列表吗,就是那个 /authorize 端点),它应该成为应用程序安全连接的一部分,而对于 /token 令牌端点来说的话,它应该有一个标记被配置在 AuthorizationServerEndpointsConfigurer 配置对象中,你可以使用 sslOnly() 方法来进行设置。当然了,这两个设置是可选的,不过在以上两种情况中,会导致Spring Security 会把不安全的请求通道重定向到一个安全通道中。(译者注:即将HTTP请求重定向到HTTPS请求上)。

 

自定义错误处理(Error Handling):

端点实际上就是一个特殊的Controller,它用于返回一些对象数据。

授权服务的错误信息是使用标准的Spring MVC来进行处理的,也就是 @ExceptionHandler 注解的端点方法,你也可以提供一个 WebResponseExceptionTranslator 对象。最好的方式是改变响应的内容而不是直接进行渲染。

假如说在呈现令牌端点的时候发生了异常,那么异常委托了 HttpMessageConverters 对象(它能够被添加到MVC配置中)来进行输出。假如说在呈现授权端点的时候未通过验证,则会被重定向到 /oauth/error 即错误信息端点中。whitelabel error (即Spring框架提供的一个默认错误页面)错误端点提供了HTML的响应,但是你大概可能需要实现一个自定义错误页面(例如只是简单的增加一个 @Controller 映射到请求路径上 @RequestMapping("/oauth/error"))。

 

映射用户角色到权限范围(Mapping User Roles to Scopes):

有时候限制令牌的权限范围是很有用的,这不仅仅是针对于客户端,你还可以根据用户的权限来进行限制。如果你使用 DefaultOAuth2RequestFactory 来配置 AuthorizationEndpoint 的话你可以设置一个flag即 checkUserScopes=true来限制权限范围,不过这只能匹配到用户的角色。你也可以注入一个 OAuth2RequestFactory 到 TokenEnpoint 中,不过这只能工作在 password 授权模式下。如果你安装一个 TokenEndpointAuthenticationFilter 的话,你只需要增加一个过滤器到 HTTP BasicAuthenticationFilter 后面即可。当然了,你也可以实现你自己的权限规则到 scopes 范围的映射和安装一个你自己版本的 OAuth2RequestFactory。AuthorizationServerEndpointConfigurer 配置对象允许你注入一个你自定义的 OAuth2RequestFactory,因此你可以使用这个特性来设置这个工厂对象,前提是你使用 @EnableAuthorizationServer 注解来进行配置(见上面介绍的授权服务配置)。

 

二、资源服务配置:


一个资源服务(可以和授权服务在同一个应用中,当然也可以分离开成为两个不同的应用程序)提供一些受token令牌保护的资源,Spring OAuth提供者是通过Spring Security authentication filter 即验证过滤器来实现的保护,你可以通过 @EnableResourceServer 注解到一个 @Configuration 配置类上,并且必须使用 ResourceServerConfigurer 这个配置对象来进行配置(可以选择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法,参数就是这个对象的实例),下面是一些可以配置的属性:

  • tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌服务。
  • resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证。
  • 其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。
  • 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是受保护资源服务的全部路径。
  • 受保护资源的访问规则,默认的规则是简单的身份验证(plain authenticated)。
  • 其他的自定义权限保护规则通过 HttpSecurity 来进行配置。

@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链,

在XML配置中,使用 <resource-server />标签元素并指定id为一个servlet过滤器就能够手动增加一个标准的过滤器链。

ResourceServerTokenServices 是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序上的话,你可以使用 DefaultTokenServices ,这样的话,你就不用考虑关于实现所有必要的接口的一致性问题,这通常是很困难的。如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices,它知道如何对令牌进行解码。

在授权服务器上,你通常可以使用 DefaultTokenServices 并且选择一些主要的表达式通过 TokenStore(后端存储或者本地编码)。

RemoteTokenServices 可以作为一个替代,它将允许资源服务器通过HTTP请求来解码令牌(也就是授权服务的 /oauth/check_token 端点)。如果你的资源服务没有太大的访问量的话,那么使用RemoteTokenServices 将会很方便(所有受保护的资源请求都将请求一次授权服务用以检验token值),或者你可以通过缓存来保存每一个token验证的结果。

使用授权服务的 /oauth/check_token 端点你需要将这个端点暴露出去,以便资源服务可以进行访问,这在咱们授权服务配置中已经提到了,下面是一个例子:

@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')")
        .checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}

在这个例子中,我们配置了 /oauth/check_token 和 /oauth/token_key 这两个端点(受信任的资源服务能够获取到公有密匙,这是为了验证JWT令牌)。这两个端点使用了HTTP Basic Authentication 即HTTP基本身份验证,使用 client_credentials 授权模式可以做到这一点。

您可能希望利用Spring Security 基于表达式的访问控制。默认情况下,将在@EnableResourceServer设置中注册表达式处理程序。表达式包括#oauth2.clientHasRole,#oauth2.clientHasAnyRole和#oath2.denyClient,它们可用于根据oauth客户端的角色提供访问权限(OAuth2SecurityExpressionMethods有关详细列表,请参阅参考资料)。在XML中,您可以使用expression-handler常规<http/>安全配置的元素注册oauth感知表达式处理程序。

 

2.OAuth 2.0客户端


OAuth 2.0 客户端机制作用是负责访问受 OAuth 2.0 保护的其他服务器的资源。配置涉及建立用户可能访问的相关受保护资源。客户端可能还需要提供用于存储授权代码和用户访问令牌的机制。

  • 受保护的资源配置

受保护的资源(或“远程资源”)可以使用 OAuth2ProtectedResourceDetails 类型的 bean 定义来定义。受保护的资源具有以下属性:

id:资源的ID。id仅供客户端用于查找资源; 它从未在OAuth协议中使用过。它也被用作bean的id。

clientId:OAuth客户端ID。这是OAuth提供商识别您的客户端的ID。

clientSecret:与资源相关的秘密。默认情况下,没有秘密是空的。

accessTokenUri:提供访问令牌的提供者OAuth端点的URI。

scope:逗号分隔的字符串列表,指定对资源的访问范围。默认情况下,不会指定范围。

clientAuthenticationScheme:客户端用于对访问令牌端点进行身份验证的方案。建议值:“http_basic”和“form”。默认值:“http_basic”。请参阅OAuth 2规范的第2.1节。

不同的授权类型具有不同的具体实现OAuth2ProtectedResourceDetails(例如,ClientCredentialsResource对于“client_credentials”授权类型)。对于需要用户授权的授权类型,还有一个属性:

userAuthorizationUri:如果用户需要授权访问资源,则将重定向用户的uri。请注意,这并不总是必需的,具体取决于支持的OAuth 2配置文件。

在XML中,有一个<resource/>元素可用于创建类型的bean OAuth2ProtectedResourceDetails。它具有匹配上述所有属性的属性。

 

  • 客户端配置

对于 OAuth 2.0 客户端,使用 @EnableOAuth2Client 进行配置是非常简单的。需要做两件事情:

创建一个过滤器 bean(ID 为 oauth2ClientContextFilter)来存储当前请求和上下文。在请求期间需要进行身份验证的情况下,它将管理 OAuth 身份验证 URI 中的重定向。

创建一个 AccessTokenRequest 型的 bean 的请求范围。这可以通过授权代码(或隐式)授权客户端使用,以保持与单个用户相关的状态不受冲突。

必须将过滤器连接到应用程序中(例如,使用Servlet初始化程序或 具有相同名称的web.xml配置DelegatingFilterProxy)。

本AccessTokenRequest可以在使用 OAuth2RestTemplate这样的:

@Autowired
private OAuth2ClientContext oauth2Context;

@Bean
public OAuth2RestTemplate sparklrRestTemplate() {
return new OAuth2RestTemplate(sparklr(), oauth2Context);
}

OAuth2ClientContext(会为你)被放置在会话范围内,以保持不同用户的状态分离。没有这个的话,你将不得不在服务器上自己管理与之对等的数据结构,将传入的请求映射到用户,并将每个用户与一个单独的 OAuth2ClientContext 实例相关联。

在 XML 中,有一个带有 id 属性的 <client/> 元素 - 这是 servlet Filter 的 bean id,必须像在 @Configurationcase 中一样映射到 DelegatingFilterProxy(具有相同的名称)。

 

  • 访问受保护的资源

一旦为资源提供了所有配置,您现在就可以访问这些资源。用于访问这些资源的建议的方法是通过使用所述RestTemplate在spring 3引入。Spring Security的OAuth提供了RestTemplate的扩展,只需要提供一个实例OAuth2ProtectedResourceDetails。要将其与用户令牌(授权代码授予)一起使用,您应该考虑使用@EnableOAuth2Client配置(或XML等价物<oauth:rest-template/>)来创建一些请求和会话范围的上下文对象,以便不同用户的请求不会在运行时发生冲突。

作为一般规则,Web 应用程序不应使用密码授权,因此如果你可以使用 AuthorizationCodeResourceDetails,请避免使用 ResourceOwnerPasswordResourceDetails。如果你确切需要从 Java 客户端获得密码授权,那么请使用相同的机制来配置 OAuth2RestTemplate,并将凭据添加到 AccessTokenRequest(它是一个 Map 并且是临时的),而不是 ResourceOwnerPasswordResourceDetails(它在所有访问令牌之间共享)。

 

  • 客户端的令牌持久化

客户端不需要持久化令牌,但每次重新启动客户端应用程序时,用户都不需要批准新的令牌授权。该ClientTokenServices接口定义了为特定用户持久保存OAuth 2.0令牌所必需的操作。提供了一个JDBC实现,但是如果您更喜欢实现自己的服务来在持久性数据库中存储访问令牌和关联的身份验证实例。如果您想使用此功能,您需要提供专门配置TokenProvider的OAuth2RestTemplate例如:

@Bean
@Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
public OAuth2RestOperations restTemplate() {
OAuth2RestTemplate template = new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(accessTokenRequest));
AccessTokenProviderChain provider = new AccessTokenProviderChain(Arrays.asList(new AuthorizationCodeAccessTokenProvider()));
provider.setClientTokenServices(clientTokenServices());
return template;
}

更多精彩内容,请关注博主公众号

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

StarskyBoy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值