SpringBoot学习笔记(十四:Spring Security安全管理 )

在这里插入图片描述

2.4、注销登录配置

如果想要注销登录 也只需要提供简单的配置即可:

//注销登录

.logout()

//注销登录请求url

.logoutUrl(“/logout”)

//清除身份认证信息

.clearAuthentication(true)

//使 Session失效

.invalidateHttpSession(true)

//定义注销成功的业务逻辑,这里返回一段json

.logoutSuccessHandler(new LogoutSuccessHandler() {

@Override

public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {

resp.setContentType(“application/json;charset=utf-8”);

PrintWriter out = resp.getWriter();

out.write(“logout success”);

out.flush();

}

})

.permitAll()

.and()

2.5、方法安全

上面介绍的认证与授权都是基于 URL 的,也可以通过注解来灵活地配置方法安全,要使用相关注解,首先要通过@EnableGloba!MethodSecurity 注解开启基于注解的安全配置:

@Configuration

@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true)

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

}

  • prePostEnabled=true会解锁@PreAuthorize和@PostAuthorize两个注解,@PreAuthorize会在执行方法前验证,@PostAuthorize会在执行方法后验证。

  • securedEnabled=true会解锁@Secured 注解。

@Service

public class MethodService {

//示访问该方法需要 ADMIN 角色

@Secured(“ROLE ADMIN”)

public String admin () {

return "hello admin ";

}

//访问该方法既需要ADMIN角色又需要USER角色

@PreAuthorize(“hasRole (‘ADMIN’) and hasRole ('USER ')”)

public String user(){

return “Hello User”;

}

//访问该方法需要ADMIN 或 USER角色

@PreAuthorize(“hasAnyRole(‘ADMIN’,‘USER’)”)

public String any(){

return “Hello Every One”;

}

}

3、加密


3.1、加密方案

密码加密一般会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中创建数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,然后将数据打乱混合,重新创建一个散列值。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据会使得数据库记录更难找到。我们常用的散列函数有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm )。

123456 —MD5—> e10adc3949ba59abbe56e057f20f883e

实际上,上面的实例在现实使用中还存在着一个不小的问题。虽然 MD5 算法是不可逆的,但是因为它对同一个字符串计算的结果是唯一 的,所以一些人可能会使用“字典攻击”的方式来攻破 MD5 加密的系统。这虽然属于暴力解密,却十分有效,因为大多数系统的用户密码都不会很长。

为了解决这个问题,我们可以使用盐值加密“salt-source”,所谓加盐加密,是指在加密之前,为原文附上额外的随机值,再进行加密。具体实现方法并不固定。

3.2、 实践

Spring Security内置了密码加密机制,只需使用一个PasswordEncoder接口即可。

在这里插入图片描述

PasswordEncoder接口定义了encode和matches两个方法,当用数据库存储用户密码时,加密过程用 encode方法,matches方法用于判断用户登录时输入的密码是否正确。

Spring Security 还内置了几种常用的 PasswordEncoder 接口,例如, StandardPasswordEncoder中的常规摘要算法(SHA-256等)、BCryptPasswordEncoder加密,以及类似 BCrypt的慢散列加密Pbkdf2PasswordEncoder等,官方推荐使用BCryptPasswordEncoder。

配置密码加密非常简单:

@Bean

PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

同样可以自定义加密方式,例如不想使用推荐的BCryptPasswordEncoder,想使用其它的加密方法,例如MD5加密,很简单,我们自己实现一个PasswordEncoder,在配置中使用自定义的加密类即可。

public class Md5PasswordEncoder implements PasswordEncoder {

@Override

public String encode(CharSequence charSequence) {

//省略md5加密过程

return md5String;

}

@Override

public boolean matches(CharSequence charSequence, String s) {

//省略比对过程

return false;

}

}

@Bean

PasswordEncoder passwordEncoder() {

return new Md5PasswordEncoder();

}

4、基于数据库的认证


在真实项目中,用户的基本信息以及角色等都存储在数据库中,因此需要从数据库中获取数据进行认证。

4.1、数据库设计

一共三张表,分别是用户表、角色表、用户_角色关联表。

在这里插入图片描述

创建表并插入一些测试数据:

SET FOREIGN_KEY_CHECKS=0;


– Table structure for role


DROP TABLE IF EXISTS role;

CREATE TABLE role (

id int(11) NOT NULL AUTO_INCREMENT,

rolename varchar(50) NOT NULL COMMENT ‘角色名’,

note varchar(255) NOT NULL COMMENT ‘角色描述’,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT=‘角色表’;


– Records of role


INSERT INTO role VALUES (‘1’, ‘ROLE_ADMIN’, ‘管理员’);

INSERT INTO role VALUES (‘2’, ‘ROLE_DBA’, ‘数据库管理员’);

INSERT INTO role VALUES (‘3’, ‘ROLE_USER’, ‘用户’);


– Table structure for user


DROP TABLE IF EXISTS user;

CREATE TABLE user (

id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘主键’,

username varchar(50) NOT NULL COMMENT ‘用户名’,

password varchar(255) NOT NULL COMMENT ‘密码’,

enabled tinyint(1) NOT NULL COMMENT ‘是否可用,1表示可用,0表示不可用’,

locked tinyint(1) NOT NULL COMMENT ‘是否上锁,1表示上锁,0表示未上锁’,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT=‘关联表’;


– Records of user


INSERT INTO user VALUES (‘1’, ‘admin’, ‘$2a$10$7zE.mjxSWV6w6jyh8iL8Q.5ouGokPg1PuL531YKPmRWCysN30I.qO’, ‘1’, ‘0’);

INSERT INTO user VALUES (‘2’, ‘root’, ‘$2a$10$7zE.mjxSWV6w6jyh8iL8Q.5ouGokPg1PuL531YKPmRWCysN30I.qO’, ‘1’, ‘0’);

INSERT INTO user VALUES (‘3’, ‘laosan’, ‘$2a$10$7zE.mjxSWV6w6jyh8iL8Q.5ouGokPg1PuL531YKPmRWCysN30I.qO’, ‘1’, ‘0’);


– Table structure for user_role


DROP TABLE IF EXISTS user_role;

CREATE TABLE user_role (

id int(11) NOT NULL AUTO_INCREMENT,

uid int(11) NOT NULL COMMENT ‘用户id’,

rid int(11) NOT NULL COMMENT ‘角色id’,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT=‘用户-角色关联表’;


– Records of user_role


INSERT INTO user_role VALUES (‘1’, ‘1’, ‘1’);

INSERT INTO user_role VALUES (‘2’, ‘2’, ‘2’);

INSERT INTO user_role VALUES (‘3’, ‘3’, ‘3’);

INSERT INTO user_role VALUES (‘4’, ‘1’, ‘3’);

INSERT INTO user_role VALUES (‘5’, ‘1’, ‘2’);

  • 角色名有一个默认的前缀"ROLE_"

4.2、创建项目

这里选择MyBatis作为持久层框架,添加依赖:

org.springframework.boot

spring-boot-starter-security

org.springframework.boot

spring-boot-starter-web

mysql

mysql-connector-java

runtime

com.alibaba

druid

1.1.12

org.mybatis.spring.boot

mybatis-spring-boot-starter

1.3.2

4.3、application.properties

数据库连接和MyBatis相关配置:

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

spring.datasource.username=root

spring.datasource.password=root

spring.datasource.url=jdbc:mysql://localhost:3306/demo_security?serverTimezone=CTT&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true

mybatis.type-aliases-package=edu.hpu.pojo

mybatis.mapper-locations=classpath:mybatis/mapper/*.xml

4.4、实体类

用户实体类需要实现UserDetails接口:

public class User implements UserDetails {

private Integer id;

private String username;

private String password;

private Boolean enabled;

private Boolean locked;

private List roles;

/**

  • 获取当前用户对象所具有的角色信息

  • @return

*/

@Override

public Collection<? extends GrantedAuthority> getAuthorities() {

List authorities = new ArrayList<>() ;

for (Role role : roles){

authorities.add (new SimpleGrantedAuthority (role.getRolename()) ) ;

}

return authorities;

}

/**

  • 获取当前用户的密码

  • @return

*/

@Override

public String getPassword() {

return password;

}

/**

  • 获取d当前用户的用户名

  • @return

*/

@Override

public String getUsername() {

return username;

}

/**

  • 当前账户是否未过期

  • @return

*/

@Override

public boolean isAccountNonExpired() {

return true;

}

/**

  • 当前账户是否未锁定

  • @return

*/

@Override

public boolean isAccountNonLocked() {

return !locked;

}

/**

  • 当前账户密码是否未过期

  • @return

*/

@Override

public boolean isCredentialsNonExpired() {

return true;

}

/**

  • 当前账户是否可用

  • @return

*/

@Override

public boolean isEnabled() {

return enabled;

}

//省略getter、setter

}

  • 实现了UserDetails接口的七个方法,方法作用见注释

  • 用户根据实际情况设直这个方法的返回值 因为默认情况下不需要开发者自己进行密码角色等信息的比对,开发者只需要提供相关信息即可。例如 getPassword()方法返回的密码和用户输入的登录密码不匹配,会自动抛出BadCredentialsException 异常。

  • getAuthorities()方法用来获取当前用户所具有的角色信息。

角色实体类:

public class Role {

private Integer id;

private String rolename;

private String note;

//省略getter、setter

}

4.5、持久层

接口:

@Mapper

public interface UserMapper {

User loadUserByUsername(String username);

List getUserRolesByUid (Integer id) ;

}

对应的映射文件:

<?xml version="1.0" encoding="UTF-8"?>

select * from user where username=#{username}

select r.* from role r

join user_role ur on r.id=ur.rid

join user u on u.id=ur.uid

where u.id=#{id}

4.6、服务层

UserService需要实现UserDetailsService接口

@Service

public class UserService implements UserDetailsService {

@Autowired

UserMapper userMapper;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

User user = userMapper.loadUserByUsername(username);

if (user==null){

throw new UsernameNotFoundException(“账号不存在”);

}

user.setRoles(userMapper.getUserRolesByUid(user.getId()));

return user;

}

}

  • 实现 UserDetailsService 接口,并实现该接口中的 loadUserByUsername 方法,该方法的参数就是用户登录时输入的用户名,通过用户名去数据库中查找用户,如果没有查找到用户,就抛出 个账户不存在的异常,如果查找到了用户,就继续查找该用户所具有的角色信息,并将获取到的 user 对象返回,再由系统提供的 DaoAuthenticationProvider 类去比对密码是否正确。

  • loadUserByUsername 方法将在用户登录时自动调用。

4.7、配置

这里是一个比较精简的配置,用户名和密码从数据库中获取:

@Configuration

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired

UserService userService;

@Bean

PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.userDetailsService (userService);

}

@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests()

.antMatchers(“/admin/**”).hasRole(“ADMIN”)

.antMatchers(“/db/**”).hasRole(“DBA”)

.antMatchers(“/user/**”).hasRole(“USER”)

.and()

.formLogin()

.loginProcessingUrl(“/login”).permitAll()

.and()

.csrf().disable();

}

}

4.8、控制层

根据配置类编写不同权限的接口:

@RestController

public class HelloController {

@GetMapping(“/user/hello”)

public String userHello(){

return “你好,普通用户!”;

}

@GetMapping(“/admin/hello”)

public String adminHello(){

return “你好,管理员!”;

}

@GetMapping(“/db/hello”)

public String dbaHello(){

return “你好,数据库管理员!”;

}

}

启动项目,就可以访问不同权限的接口进行测试了。

5、高级配置


5.1、角色继承


在上面的实例中中定义了三种角色,但是这三种角色之间不具备任何关系,一 般来说角色之间是有关系的,例如 ROLE_ADMIN 一般既具有 ADMIN 的权限,又具有 USER 的权限。那么如何配置这种角色继承关系呢?在 pring Security 中只需要开发者提供一个 RoleHierarchy 即可。

假设 ROLE_DBA是终极大 Boss ,具有所有的权限, ROLE_ADMIN具有 ROLE_USER权限, ROLE_USER是一个公共角色,即 ROLE_ADMIN继承 ROLE_USER, ROLE_DBA继承ROLE_ADMIN ,要描述这种继承关系,只需要开发者在 Spring Security 的配置类中提供RoleHierarchy 即可,代码如下:

@Bean

RoleHierarchy roleHierarchy(){

RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();

String hireachy = “ROLE_DBA > ROLE_ADMIN> ROLE_USER”;

roleHierarchy.setHierarchy(hireachy);

return roleHierarchy;

}

配置完 RoleHierarchy 之后,具有 ROLE_DBA 角色的用户就可以访问所有资源了, 具有

ROLE_ADMIN 角色的用户也可以访问具有 ROLE_USER 角色才能访问的资源。

5.2、动态配置权限


使用 ttpSecurity 配置的认证授权规则还是不够灵活,无法实现资源和角色之间的动态调整,要实现动态配置 URL 权限,就需要我们自定义权限配置,在第4节的基础上进行改造。

5.2.1、数据库设计

这里的数据库在4数据库的基础上再增加一张资源表和资源角色关联表,资源表中定义了用户能够访问的 URL 模式,资源角色表则定义了访问该模式的 URL 需要什么样的角色。

添加两张表之后的数据库表结构如下:

在这里插入图片描述

创建资源表和资源角色关联表并插入一些测试数据:

DROP TABLE IF EXISTS menu;

CREATE TABLE menu (

id int(11) NOT NULL AUTO_INCREMENT,

pattern varchar(50) NOT NULL COMMENT ‘路径’,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT=‘资源表’;


– Records of menu


INSERT INTO menu VALUES (‘1’, ‘/db/**’);

INSERT INTO menu VALUES (‘2’, ‘/admin/**’);

INSERT INTO menu VALUES (‘3’, ‘/user/**’);


– Table structure for menu_role


DROP TABLE IF EXISTS menu_role;

CREATE TABLE menu_role (

id int(11) NOT NULL AUTO_INCREMENT,

mid int(11) NOT NULL COMMENT ‘资源id’,

rid int(11) NOT NULL COMMENT ‘角色id’,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT=‘资源_角色关联表’;


– Records of menu_role


INSERT INTO menu_role VALUES (‘1’, ‘1’, ‘2’);

INSERT INTO menu_role VALUES (‘2’, ‘2’, ‘1’);

INSERT INTO menu_role VALUES (‘3’, ‘3’, ‘3’);

5.2.3、实体类、持久层接口和映射文件

Menu.java:

public class Menu {

private Integer id;

private String pattern;

private List roles;

//省略getter、setter

}

MenuMapper.java:

@Mapper

public interface MenuMapper {

List

getAllMenus();

}

MenuMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>

SELECT

m.*,

r.id AS rid,

r.rolename AS rolename,

r.note AS note

FROM

menu m

LEFT JOIN menu_role mr ON m.id=mr.mid

LEFT JOIN role r ON mr.rid= r.id

5.2.3、自定义 FilterlnvocationSecurityMetadataSource

要实现动态配置权限,首先要自定义 FilterlnvocationSecurityMetadataSource,Spring Security中通过FilterlnvocationSecurityMetadataSource接口的getAttributes方法来确定一个请求需要哪些角色,接口的默认实现类是DefaultFilterlnvocationSecurityMetadataSource,参考DefaultFilterlnvocationSecurityMetadataSource,可以自定义FilterlnvocationSecurityMetadataSource接口实现类。

@Component

public class CustomFilterinvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

//创建一个AntPathMatcher实例,主要用来实现ant风格的URL匹配。

AntPathMatcher antPathMatcher =new AntPathMatcher() ;

@Autowired

MenuMapper menuMapper;

/**

  • @param o

  • 参数是一个FilterInvocation,可以从中去除请求的url

  • @return Collection:表示当前请求 URL 所需的角色。

  • @throws IllegalArgumentException

*/

@Override

public Collection getAttributes(Object o) throws IllegalArgumentException {

//从FilterInvocation 中提取出当前请求的url

String requestUtl=( (FilterInvocation) o).getRequestUrl();

//从数据库中取出资源信息

List

menus=menuMapper.getAllMenus();

for (Menu menu:menus) {

if (antPathMatcher.match(menu.getPattern(),requestUtl)){

//获取当前请求的 URL 所需要的角色信息

List roles=menu.getRoles();

String[] roleArr =new String[roles.size()];

for (int i=0;i<roleArr.length;i++){

roleArr[i] = roles.get(i).getRolename();

}

//返回角色信息

return SecurityConfig.createList(roleArr);

}

}

//如果不存在匹配的角色信息,返回ROLE_LOGIN,即登录就可访问

return SecurityConfig.createList(“ROLE_LOGIN”);

}

/**

  • @return 返回所有定义好的权限资源, Spring Security 在启动时会校验

  • 相关配置是否正确 ,如果不需要校验,那么该方法直接返回 null 即可

*/

@Override

public Collection getAllConfigAttributes() {

return null;

}

/**

  • @param aClass

  • @return 返回类对象是否支持校验

*/

@Override

public boolean supports(Class<?> aClass) {

return FilterInvocation.class.isAssignableFrom(aClass) ;

}

}

5.2.4、自定义 AccessDecisionManager

当一个请求走完FilterlnvocationSecurityMetadataSource的getAttributes方法之后,会

来到AccessDecisionManager类中进行角色信息的比对,自定义AccessDecisionManager类如下:

最后

手绘了下图所示的kafka知识大纲流程图(xmind文件不能上传,导出图片展现),但都可提供源文件给每位爱学习的朋友

image.png

public Collection getAttributes(Object o) throws IllegalArgumentException {

//从FilterInvocation 中提取出当前请求的url

String requestUtl=( (FilterInvocation) o).getRequestUrl();

//从数据库中取出资源信息

List

menus=menuMapper.getAllMenus();

for (Menu menu:menus) {

if (antPathMatcher.match(menu.getPattern(),requestUtl)){

//获取当前请求的 URL 所需要的角色信息

List roles=menu.getRoles();

String[] roleArr =new String[roles.size()];

for (int i=0;i<roleArr.length;i++){

roleArr[i] = roles.get(i).getRolename();

}

//返回角色信息

return SecurityConfig.createList(roleArr);

}

}

//如果不存在匹配的角色信息,返回ROLE_LOGIN,即登录就可访问

return SecurityConfig.createList(“ROLE_LOGIN”);

}

/**

  • @return 返回所有定义好的权限资源, Spring Security 在启动时会校验

  • 相关配置是否正确 ,如果不需要校验,那么该方法直接返回 null 即可

*/

@Override

public Collection getAllConfigAttributes() {

return null;

}

/**

  • @param aClass

  • @return 返回类对象是否支持校验

*/

@Override

public boolean supports(Class<?> aClass) {

return FilterInvocation.class.isAssignableFrom(aClass) ;

}

}

5.2.4、自定义 AccessDecisionManager

当一个请求走完FilterlnvocationSecurityMetadataSource的getAttributes方法之后,会

来到AccessDecisionManager类中进行角色信息的比对,自定义AccessDecisionManager类如下:

最后

手绘了下图所示的kafka知识大纲流程图(xmind文件不能上传,导出图片展现),但都可提供源文件给每位爱学习的朋友

[外链图片转存中…(img-i6OcK0Bk-1714463156627)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 29
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot 是基于 Spring 框架的一个快速开发框架,它能够简化 Spring 应用的初始化过程、开发和部署,提高开发效率。下面是 Spring Boot学习笔记: ## 1. Spring Boot 简介 Spring BootSpring 家族的一个全新的框架,它的设计目的是为了快速开发基于 Spring 的应用程序。 Spring Boot 采用了约定大于配置的原则,通过自动配置、快速开发、可插拔的特性,让开发者可以更加专注于业务逻辑。 ## 2. Spring Boot 的优点 - 简化配置:Spring Boot 通过自动配置的方式,避免了繁琐的配置。 - 快速开发:Spring Boot 集成了很多常用的框架和工具,使得开发过程更加快速。 - 易于部署:Spring Boot 可以将应用程序打包成 jar 包或 war 包,方便部署和运行。 - 微服务支持:Spring Boot 对于微服务的支持非常好,可以使用 Spring Cloud 进行服务治理。 ## 3. Spring Boot 的核心原理 Spring Boot 的核心原理是通过自动配置和约定大于配置来简化开发过程。 自动配置是指 Spring Boot 会根据项目的依赖和配置,自动为项目进行配置。开发者只需要添加相应的依赖,就可以使用相应的功能,无需进行繁琐的配置。 约定大于配置是指 Spring Boot 对于一些常见的配置,采用一些约定的方式进行配置。开发者只需要按照约定的方式进行开发,就可以使用相应的功能。 ## 4. Spring Boot 的常用注解 - @SpringBootApplication:Spring Boot 应用程序的入口,同时也是 Spring 的配置文件。 - @RestController:用于定义 RESTful 接口的控制器。 - @RequestMapping:用于定义请求的 URL 和请求方法。 - @Autowired:用于自动注入依赖。 - @Value:用于获取配置文件中的属性值。 ## 5. Spring Boot 的常用组件 - Spring MVC:用于定义 RESTful 接口和处理请求。 - Spring Data:用于简化数据库操作。 - Spring Security:用于实现安全认证和授权。 - Spring Boot Actuator:用于监控和管理应用程序。 ## 6. Spring Boot 的开发步骤 - 创建 Spring Boot 项目。 - 添加项目依赖。 - 编写业务代码。 - 运行项目。 ## 7. Spring Boot 的部署方式 - 打包成 jar 包,使用 java -jar 命令运行。 - 打包成 war 包,部署到 Tomcat 或其他 Servlet 容器中。 ## 8. Spring Boot 的监控和管理 Spring Boot Actuator 提供了一系列的监控和管理功能,包括健康检查、性能监控、日志记录等。可以通过 HTTP 接口或 JMX 进行访问和管理。 ## 9. Spring Boot 的测试 Spring Boot 提供了很好的测试支持,可以使用 JUnit 进行单元测试和集成测试。同时也可以使用 Spring Boot Test 提供的测试框架进行测试。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值