SpringSecurity Oauth2 - SpringSecurity Oauth2 搭建授权服务器和资源服务器

1. SpringSecurity Oauth2 授权服务器搭建

导入 SpringSecurity Oauth2 依赖 :

<!--引入Springsecurity-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--引入SpringSecurity oauth2 授权服务器依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    <version>2.2.4.RELEASE</version>
</dependency>

1. 授权服务器 - 支持授权码模式

SpringSecurity 配置类 :

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password(passwordEncoder().encode("123")).roles("ADMIN").build());
         return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable();
    }
}

自定义授权服务器配置:

// 自定义授权服务器配置,指定当前应用为授权服务器
@Configuration
@EnableAuthorizationServer
public class AuthenticationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 用来配置授权服务器可以为哪些客户端授权,使用哪种授权模式
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client")
                .secret(passwordEncoder.encode("secret"))
                .redirectUris("http://www.baidu.com")
                // 授权模式支持授权码模式,同时支持令牌刷新模式
                .authorizedGrantTypes("authorization_code","refresh_token")
                // 令牌允许获取的资源权限
                .scopes("read:user");
    }
}

授权码模式:

  • 请求用户是否授权,获取授权码code:/oauth/authorize
  • 授权之后根据获取的授权码获取令牌access_token:/oauth/token ,授权类型:authorization_code

① 请求用户是否授权,获取授权码code,浏览器访问完整路径:http://localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com

在这里插入图片描述

点击Authorize按钮就会跳转到百度网站,并返回一个授权码 code

在这里插入图片描述

② 授权之后根据返回的授权码 code 获取访问令牌 access_token :

在这里插入图片描述

2. 授权服务器 - 支持授权码模式同时支持刷新令牌

在access_token即将过期的时候,我们希望支持刷新令牌

授权码模式:

  • 请求用户是否授权,获取授权码 code:/oauth/authorize
  • 授权之后根据获取的授权码 code 获取令牌 access_token 和 refresh_token:/oauth/token, 授权类型需要为authorization_code
  • 令牌刷新:/oauth/token, 授权类型需要为 refresh_token
/**
 * 自定义授权服务器配置,指定当前应用为授权服务器
 */
@Configuration
@EnableAuthorizationServer
public class AuthenticationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 用来配置授权服务器可以为哪些客户端授权,使用哪种授权模式
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client")
                .secret(passwordEncoder.encode("secret"))
                .redirectUris("http://www.baidu.com")
                // 授权模式支持授权码模式,同时支持令牌刷新模式
                .authorizedGrantTypes("authorization_code","refresh_token")
                // 令牌允许获取的资源权限
                .scopes("read:user");
    }

    /**
     * 刷新令牌必须配置userDetailsService,用来刷新令牌时的认证
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.userDetailsService(userDetailsService);
    }
}

① 请求用户是否授权,获取授权码code,浏览器访问完整路径:http://localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com

在这里插入图片描述

② 授权之后根据返回的授权码 code 获取访问令牌 access_token 和 刷新令牌 refresh_token

在这里插入图片描述

③ 根据 refresh_token 刷新令牌:

在这里插入图片描述

3. 授权服务器 - 支持密码模式

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password(passwordEncoder().encode("123")).roles("ADMIN").build());
         return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    // 暴露 authenticationManager
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable();
    }
}
/**
 * 自定义授权服务器配置,指定当前应用为授权服务器
 */
@Configuration
@EnableAuthorizationServer
public class AuthenticationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 用来配置授权服务器可以为哪些客户端授权,使用哪种授权模式
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client")
                .secret(passwordEncoder.encode("secret"))
                .redirectUris("http://www.baidu.com")
                // 授权模式支持密码模式,同时支持令牌刷新模式
                .authorizedGrantTypes("password","refresh_token")
                // 令牌允许获取的资源权限
                .scopes("read:user");
    }

    /**
     * 刷新令牌必须配置userDetailsService,用来刷新令牌时的认证
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.userDetailsService(userDetailsService);
        endpoints.authenticationManager(authenticationManager);
    }
}

http://client:secret@localhost:8080/oauth/token

在这里插入图片描述

刷新令牌:

在这里插入图片描述

4. 授权服务器 - 支持客户端模式

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password(passwordEncoder().encode("123")).roles("ADMIN").build());
         return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    // 暴露 authenticationManager
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable();
    }
}
/**
 * 自定义授权服务器配置,指定当前应用为授权服务器
 */
@Configuration
@EnableAuthorizationServer
public class AuthenticationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 用来配置授权服务器可以为哪些客户端授权,使用哪种授权模式
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client")
                .secret(passwordEncoder.encode("secret"))
                .redirectUris("http://www.baidu.com")
                // 授权模式支持授权码模式,同时支持令牌刷新模式
                .authorizedGrantTypes("client_credentials","refresh_token")
                // 令牌允许获取的资源权限
                .scopes("read:user");
    }

    /**
     * 刷新令牌必须配置userDetailsService,用来刷新令牌时的认证
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.userDetailsService(userDetailsService);
        endpoints.authenticationManager(authenticationManager);
    }
}

在这里插入图片描述

5. 授权服务器 - 基于数据库客户端和令牌存储

在上面的案例中,TokenStore 的默认实现为 InMemoryTokenStore 即内存存储,对于Client 信息,ClientDetailsService 接口负责从存储仓库中读取数据,在上面的案例中默认使用的也是 InMemoryClientDetailsService 实现类。如果要想使用数据库存储,只要提供这些接口的实现类即可,而框架已经为我们写好JdbcTokenStore 和 JdbcClientDetailsService

1. SpringBoot 依赖和配置
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>


<!-- 引入授权服务器依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>


<!--引入 jdbc 依赖-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
spring.application.name=spring-security-19authorization-server
server.port=8080
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=Hongkong
spring.datasource.username=root
spring.datasource.password=root
2. 建数据库表

https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

用 BLOB 替换语句中的 LONGVARBINARY 类型

-- used in tests that use HSQL
create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256),
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)
);

create table oauth_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);

create table oauth_code (
  code VARCHAR(256), 
  authentication BLOB
);

create table oauth_approvals (
	userId VARCHAR(256),
	clientId VARCHAR(256),
	scope VARCHAR(256),
	status VARCHAR(10),
	expiresAt TIMESTAMP,
	lastModifiedAt TIMESTAMP
);


-- customized oauth_client_details table
create table ClientDetails (
  appId VARCHAR(256) PRIMARY KEY,
  resourceIds VARCHAR(256),
  appSecret VARCHAR(256),
  scope VARCHAR(256),
  grantTypes VARCHAR(256),
  redirectUrl VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additionalInformation VARCHAR(4096),
  autoApproveScopes VARCHAR(256)
);

在这里插入图片描述

// 写入客户端信息
INSERT INTO `oauth_client_details` VALUES (
    'client', 
     NULL,
	'$2a$10$QCsINtuRfP8kM112xRVdvuI58MrefLlEP2mM0kzB5KZCPhnOf4392',
	'read', 
    'authorization_code,refresh_token',
	'http://www.baidu.com', 
    NULL, NULL, NULL, NULL, NULL);

在这里插入图片描述

3. SpringSecurity 配置类
//自定义 security 配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password(passwordEncoder().encode("123")).roles("ADMIN").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    @Bean  //将内部 authorizationManager 暴露
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable();
    }
}
4. 授权服务器配置
@Configuration
@EnableAuthorizationServer
public class JdbcAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final DataSource dataSource;
    private final PasswordEncoder passwordEncoder;
    private final AuthenticationManager authenticationManager;

    @Autowired
    public JdbcAuthorizationServerConfig(DataSource dataSource, PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager) {
        this.dataSource = dataSource;
        this.passwordEncoder = passwordEncoder;
        this.authenticationManager = authenticationManager;
    }

    /**
     * 配置数据源
     */
    @Bean
    public ClientDetailsService clientDetails(){
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
        return jdbcClientDetailsService;
    }

    /**
     * 配置客户端信息
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetails());
    }


    /**
     * 配置令牌存储
     */
    @Bean
    public TokenStore tokenStore(){
        return new JdbcTokenStore(dataSource);
    }


    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //认证管理器
        endpoints.authenticationManager(authenticationManager);
        //配置令牌存储为数据库存储
        endpoints.tokenStore(tokenStore());

        // 配置TokenServices参数
        // 修改默认令牌生成服务
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        // 基于数据库令牌生成
        tokenServices.setTokenStore(endpoints.getTokenStore());
        // 是否支持刷新令牌
        tokenServices.setSupportRefreshToken(true);
        // 是否重复使用刷新令牌(直到过期)
        tokenServices.setReuseRefreshToken(true);

        // 设置客户端信息
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        // 用来控制令牌存储增强策略
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        // 访问令牌的默认有效期(以秒为单位)。过期的令牌为零或负数。30天
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));
        // 刷新令牌的有效性(以秒为单位)。如果小于或等于零,则令牌将不会过期,3天
        tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(3));
        // 使用配置令牌服务
        endpoints.tokenServices(tokenServices);
    }
}

启动测试

① 获取授权码code:http://localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com

在这里插入图片描述

在这里插入图片描述

② 根据code获取访问令牌access_token和刷新令牌refresh_token:

在这里插入图片描述

令牌已经存储在数据库中:

在这里插入图片描述

③ 根据refresh_token刷新令牌:

在这里插入图片描述

在这里插入图片描述

2. SpringSecurity Oauth2 资源服务器搭建

1. SpringBoot 依赖和配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>


<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
spring.application.name=spring-security-20resource-server
server.port=8081
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=Hongkong
spring.datasource.username=root
spring.datasource.password=root

2. 请求资源

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        String hello = "hello resource service";
        System.out.println(hello);
        return hello;
    }
}

3. 资源服务器配置

//开启 oauth 资源服务器
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    private final DataSource dataSource;
    
    @Autowired
    public ResourceServerConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore());
    }
    
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }
}

启动测试,生成令牌之后带有令牌访问:

curl -H “Authorization:Bearer dffa62d2-1078-457e-8a2b-4bd46fae0f47” http://localhost:8081/hello

① 访问授权服务器,获取授权码code:http://localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com

在这里插入图片描述

② 通过code获取访问令牌access_token :

在这里插入图片描述

③ 拿着授权服务器获取的访问令牌,访问资源服务器:

在这里插入图片描述

3. 使用 JWT 令牌

1. 授权服务器颁发 JWT 令牌

@Configuration
@EnableAuthorizationServer
public class JdbcAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final DataSource dataSource;
    private final PasswordEncoder passwordEncoder;
    private final AuthenticationManager authenticationManager;

    @Autowired
    public JdbcAuthorizationServerConfig(DataSource dataSource, PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager) {
        this.dataSource = dataSource;
        this.passwordEncoder = passwordEncoder;
        this.authenticationManager = authenticationManager;
    }

    /**
     * 配置数据源
     */
    @Bean
    public ClientDetailsService clientDetails(){
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
        return jdbcClientDetailsService;
    }

    /**
     * 配置客户端信息
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetails());
    }

    /**
     * 使用同一个秘钥来编码 JWT 中的 oauth2 令牌
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123");
        return converter;
    }

    /**
     * 配置令牌存储
     */
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //认证管理器
        endpoints.authenticationManager(authenticationManager);
        //配置jwt令牌存储
        endpoints.tokenStore(tokenStore());
        endpoints.accessTokenConverter(jwtAccessTokenConverter());
    }
}

2. 资源服务器配置

//开启 oauth 资源服务器
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore());
    }
    
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("123");
        return jwtAccessTokenConverter;
    }
}
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        String hello = "hello resource service";
        System.out.println(hello);
        return hello;
    }
}

3. 启动测试

① 访问授权服务器,获取授权码code:http://localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com

在这里插入图片描述

在这里插入图片描述

② 通过code获取访问令牌access_token :

在这里插入图片描述

③ 拿着授权服务器获取的访问令牌access_token,访问资源服务器:

在这里插入图片描述

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我一直在流浪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值