Shiro权限管理之自定义Realm

1. SpringBoot集成shiro快速入门

导入shiro依赖

<dependency>
     <groupId>org.apache.shiro</groupId>
     <artifactId>shiro-spring</artifactId>
     <version>1.4.1</version>
</dependency>

1. shiro 用户认证

@SpringBootTest
class ShiroApplicationTests {

    @Test
    void authentication() {
        //构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //创建一个SimpleAccountRealm 域
        SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
        //添加一个测试账号(后面可以做成读取动态读取数据库)
        simpleAccountRealm.addAccount("zhangsan","123456");
        //设置Realm
        defaultSecurityManager.setRealm(simpleAccountRealm);
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //获取主体
        Subject subject = SecurityUtils.getSubject();
        //用户名和密码
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
        try {
            // 进行登录,提交认证
            subject.login(token);
        }catch (IncorrectCredentialsException exception){
        System.out.println("用户名密码不匹配");
        }catch (LockedAccountException exception){
            System.out.println("账号已被锁定");
        }catch (DisabledAccountException exception){
            System.out.println("账号已被禁用");
        }catch (UnknownAccountException exception){
            System.out.println("用户不存在");
        }catch (UnauthorizedException ae ) {
            System.out.println("用户没有权限");
        }
        System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
        System.out.println("执行logout()方法");
        subject.logout();
        System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
    }
}
用户的认证状态:isAuthenticated=true
执行logout()方法
用户的认证状态:isAuthenticated=false

将密码改成:

simpleAccountRealm.addAccount("zhangsan","1234");
用户名密码不匹配
用户的认证状态:isAuthenticated=false
执行logout()方法
用户的认证状态:isAuthenticated=false

将用户名改成lisi:

simpleAccountRealm.addAccount("lisi","1234");
用户不存在
用户的认证状态:isAuthenticated=false
执行logout()方法
用户的认证状态:isAuthenticated=false

2. shiro用户授权

做权限管理的时候,分为两块,一个认证,一个是授权,认证是判断用户是否账号密 码正确,授权是判断用户登入以后有什么权限。

@SpringBootTest
class ShiroApplicationTests {

    @Test
    public void authentication1(){
        //构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //创建一个SimpleAccountRealm 域
        SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
        //添加一个测试账号、和所拥有的角色(后面可以做成读取动态读取数据库),user拥有admin角色
        simpleAccountRealm.addAccount("zhangsan","123456","admin","user");
        //设置realm
        defaultSecurityManager.setRealm(simpleAccountRealm);
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //获取主体
        Subject subject = SecurityUtils.getSubject();
        //用户名和密码(用户输入的用户名密码)生成token
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
        try {
            // 登录,提交认证
            subject.login(token);
            //检测用户是否拥有传入的角色,即只要有一个不是用户所拥有的角色就会抛出异常。
            subject.checkRoles("admin","user");
        }catch (IncorrectCredentialsException exception){
            System.out.println("用户名密码不匹配");
        }catch (LockedAccountException exception){
            System.out.println("账号已被锁定");
        }catch (DisabledAccountException exception){
            System.out.println("账号已被禁用");
        }catch (UnknownAccountException exception){
            System.out.println("用户不存在");
        }catch ( UnauthorizedException ae ) {
            System.out.println("用户没有权限");
        }
        System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
        System.out.println("执行logout()方法");
        subject.logout();
        System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
    }
}
用户的认证状态:isAuthenticated=true
执行logout()方法
用户的认证状态:isAuthenticated=false

将用户改成test,看其是否有admin的权限:

subject.checkRoles("admin","test");
用户没有权限
用户的认证状态:isAuthenticated=true
执行logout()方法
用户的认证状态:isAuthenticated=false

2. SpringBoot 使用IniRealm进行认证授权

SimpleAccountRealm 在程序中写死了用户安全数据,接下来我们使用.ini将数据移到配置文件中。IniRealm是Shiro提供一种Realm实现。用户、角色、权限等信息集中在一个.ini文件那里。

在 resources 目录下创建一个 shiro.ini 文件

# 账号信息
# 账号=密码,角色
[users]
test=123456,test
admin=123456,admin

# 角色信息
# 角色=权限1,权限2
[roles]
test=user:list,user:deleted,user:edit
# 拥有所有权限
admin= *

测试:

@SpringBootTest
class ShiroApplicationTests {

    @Test
    public void testIniRealm(){
        //配置文件中的用户权限信息,文件在类路径下
        IniRealm iniRealm = new IniRealm("classpath:shiro.ini");

        //构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //设置realm
        defaultSecurityManager.setRealm(iniRealm);
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //获取主体
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("test", "123456");
        try {
            //主体提交认证请求
            subject.login(token);
            //检查是否有角色,判断该用户是否拥有 test 角色
            subject.checkRoles("test");
            //检查是否拥有权限,检查用户是否拥有 user:list 的权限
            subject.checkPermissions("user:list");
        }catch (IncorrectCredentialsException exception){
            System.out.println("用户名或密码错误");
        }catch (LockedAccountException exception){
            System.out.println("账号已被锁定");
        }catch (DisabledAccountException exception){
            System.out.println("账号已被禁用");
        }catch (UnknownAccountException exception){
            System.out.println("用户不存在");
        }catch ( UnauthorizedException ae ) {
            System.out.println("用户没有权限");
        }
    }
}

3. Spring Boot 使用 JdbcRealm 进行认证授权

把用户安全信息(相应的角色/权限)配置在 .ini 文件,使用 IniRealm 去读取 .ini 文件获得用户的安全信息。也是 有局限性的。因为我们得事先把所有用户信息配置在.ini 文件,这样显然是行不通的,我们的系统用户都是动态的不固定的,它的一些 用户信息权限信息都是变化的,所以固定在.ini 配置文件显然是行不通的。这些数据通常我们都是把它存入到DB 中,那shiro 有没有提 供直接从DB读取用户安全信息的域呢 ? (Realm)

shiro 作为一个优秀的开源框架,显然是可以的,即 JdbcRealm。

1. 数据库驱动

<!--数据库驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
<!--数据源-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.10</version>
</dependency>

2. 数据库表结构

下面我们创建一个名为 shiro 的数据库、分别创建三张表 users、user_roles、roles_permissions

CREATE DATABASE IF NOT EXISTS shiro DEFAULT CHARSET utf8 COLLATE utf8_general_ci;

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(25) DEFAULT NULL,
  `password` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `user_roles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(25) DEFAULT NULL,
  `username` varchar(25) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `roles_permissions` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `permission` varchar(255) DEFAULT NULL,
  `role_name` varchar(25) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

USE shiro;
INSERT INTO `shiro`.`users` (`id`, `username`, `password`) VALUES ('1', 'admin', '123456');
INSERT INTO `shiro`.`users` (`id`, `username`, `password`) VALUES ('2', 'test', '123456');
INSERT INTO `shiro`.`user_roles` (`id`, `role_name`, `username`) VALUES ('1', 'admin', 'admin');
INSERT INTO `shiro`.`user_roles` (`id`, `role_name`, `username`) VALUES ('2', 'test', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('1','user:deleted', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('2','user:list', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('3', '*','admin');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('4','user:edit', 'test')

一个用户对应多个权限,一个权限对应多个用户,多对多的关系
在这里插入图片描述

3. 创建 testJdbcRealm方法

@SpringBootTest
class ShiroApplicationTests {

    @Test
    public void testJdbcRealm(){
        //配置数据源
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        //配置文件中的用户权限信息,文件在类路径下
        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setDataSource(dataSource);
        //使用JdbcRealm下面的值需要为true,不然无法查询用户权限
        jdbcRealm.setPermissionsLookupEnabled(true);

        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(jdbcRealm);
        SecurityUtils.setSecurityManager(defaultSecurityManager);

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
        try {
            subject.login(token);
            System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
            //检查是否拥有角色
            subject.checkRoles("admin");
            //检查是否拥有权限
            subject.checkPermissions("user:delete");
        }catch (IncorrectCredentialsException exception){
            System.out.println("用户名或密码错误");
        }catch (LockedAccountException exception){
            System.out.println("账号已被锁定");
        }catch (DisabledAccountException exception){
            System.out.println("账号已被禁用");
        }catch (UnknownAccountException exception){
            System.out.println("用户不存在");
        }catch ( UnauthorizedException ae ) {
            System.out.println("用户没有权限");
        }
    }
}
用户的认证状态:isAuthenticated=true

当用户 test 登录进来的时候:

UsernamePasswordToken token = new UsernamePasswordToken("test", "123456");
用户的认证状态:isAuthenticated=true
用户没有权限

在这里插入图片描述
通过源码可以看出JdbcRealm 已经帮我们写好查询语句了,所以我们就要在数据库创建与之对应的表结构,这样才能查出数据,但是这里只能使用他默认的 sql,在实际的开发中,我们不可能就简单的使用 JdbcRealm 默认的 sql 语句,而是自己自定义的 sql 语句,更多时候我们的数据库以及数据表都是根据业务需要自己创建的,而不是默认的数据库表。

4. 更改数据库表名

USE shiro;
ALTER TABLE users RENAME sys_users;
ALTER TABLE user_roles RENAME sys_user_roles;
ALTER TABLE roles_permissions RENAME sys_roles_permissions;
@SpringBootTest
class ShiroApplicationTests {

    @Test
    public void testJdbcRealm(){
        //配置数据源
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        //配置文件中的用户权限信息,文件在类路径下
        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setDataSource(dataSource);
        //使用JdbcRealm下面的值需要为true,不然无法查询用户权限
        jdbcRealm.setPermissionsLookupEnabled(true);

        //使用自定义sql查询
        String sql = "select password from sys_users where username=?";
        jdbcRealm.setAuthenticationQuery(sql);
        String roleSQl = "select role_name from sys_user_roles where username=?";
        jdbcRealm.setUserRolesQuery(roleSQl);
        String permissionSql = "select permission from sys_roles_permissions where role_name=?";
        jdbcRealm.setPermissionsQuery(permissionSql);

        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(jdbcRealm);
        SecurityUtils.setSecurityManager(defaultSecurityManager);

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
        try {
            subject.login(token);
            System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
            //检查是否拥有角色
            subject.checkRoles("admin");
            //检查是否拥有权限
            subject.checkPermissions("user:delete");
        }catch (IncorrectCredentialsException exception){
            System.out.println("用户名或密码错误");
        }catch (LockedAccountException exception){
            System.out.println("账号已被锁定");
        }catch (DisabledAccountException exception){
            System.out.println("账号已被禁用");
        }catch (UnknownAccountException exception){
            System.out.println("用户不存在");
        }catch ( UnauthorizedException ae ) {
            System.out.println("用户没有权限");
        }
    }
}

4. Spring Boot 使用自定义 Realm 进行认证授权

虽然 jdbcRealm 已经实现了从数据库中获取用户的验证信息,但是 jdbcRealm灵活性也是稍差一些的,如果要实现自己的一些特殊应 用时将不能支持,这个时候可以通过自定义realm来实现身份的认证功能。

通常自定义Realm只需要继承:AuthorizingRealm重写 doGetAuthenticationInfo(用户认证)、doGetAuthorizationInfo(用户授权) 这 两个方法即可。

public class CustomRealm extends AuthorizingRealm {
    /**
     * 模拟数据库中的用户名和密码
     */
    private Map<String, String> userMap =new HashMap<>();
    {
        userMap.put("admin","123456");
        userMap.put("test","123456");
    }

    /**
     * 获取用户认证信息:用户名+密码
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String)authenticationToken.getPrincipal();
        String password = getPasswordByUsername(username);
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,getName());
        return simpleAuthenticationInfo;
    }

    /**
     * 获取用户授权信息:用户+角色+权限
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = (String) principalCollection.getPrimaryPrincipal();
        //从数据库或者缓存中获取角色数据
        List<String> roles = getRolesByUsername(username);
        //从数据库或者缓存中获取权限数据
        List<String> permissions = getPermissionsByUsername(username);
        //创建AuthorizationInfo,并设置角色和权限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addStringPermissions(permissions);
        simpleAuthorizationInfo.addRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 通过数据库,根据用户名获取权限信息
     * @param username
     * @return
     */
    private List<String> getPermissionsByUsername(String username) {
        List<String> permissions = new ArrayList<>();
        /**
         * 只有是 admin 用户才有 新增、删除权限
         */
        if(username.equals("admin")){
            permissions.add("user:delete");
            permissions.add("user:add");
        }
        permissions.add("user:edit");
        permissions.add("user:list");
        return permissions;
    }

    /**
     * 通过数据库,根据用户名获取角色信息
     * @param username
     * @return
     */
    List<String> getRolesByUsername(String username){
        List<String> roles = new ArrayList<>();
        if(username.equals("admin")){
            roles.add("admin");
        }
        roles.add("test");
        return roles;
    }

    private String getPasswordByUsername(String username) {
        return userMap.get(username);
    }
}

测试:

@SpringBootTest
class ShiroApplicationTests {

    @Test
    public void testCustomRealm(){
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        CustomRealm customRealm = new CustomRealm();
        defaultSecurityManager.setRealm(customRealm);
        SecurityUtils.setSecurityManager(defaultSecurityManager);

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
        try {
            subject.login(token);
            System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
            //检查是否拥有角色
            subject.checkRoles("admin");
            //检查是否拥有权限
            subject.checkPermissions("user:delete");
        }catch (IncorrectCredentialsException exception){
            System.out.println("用户名或密码错误");
        }catch (LockedAccountException exception){
            System.out.println("账号已被锁定");
        }catch (DisabledAccountException exception){
            System.out.println("账号已被禁用");
        }catch (UnknownAccountException exception){
            System.out.println("用户不存在");
        }catch ( UnauthorizedException ae ) {
            System.out.println("用户没有权限");
        }
    }
}

shiro 更多的是帮助我们完成验证过程。我们需要从数据库查询当前用户的角色、权限,把这些信息告诉 shiro 框架。当我们执行用户认证的时候首先调用 doGetAuthenticationInfo 进行获取用户认证信息,当我们要校验权限的时候 就会执行 doGetAuthorizationInfo 进行获取用户授权信息。

5. SpringBoot整合shiro之盐值加密认证详解

自定义 Realm,里面的用户认证所使用的密码都是明文,这种方式是不可取的往往我们在实战开发中用户的密码 都是以密文形势进行存储,并且要求加密算法是不可逆的,著名的加密算法有MD5、SHA1等。

public class CustomRealm extends AuthorizingRealm {
    /**
     * 获取用户认证信息:用户名+密码
     * @param authenticationToken
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String)authenticationToken.getPrincipal();
        String password = getPasswordByUsername(username);
        String matcherPassword = getPasswordMatcher(password);
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,matcherPassword,getName());
        return simpleAuthenticationInfo;
    }
    
   /**
     * 获取密文密码
     * @param currentPassword
     */
    private String getPasswordMatcher(String currentPassword){
        return new Md5Hash(currentPassword, null,2).toString();
    }
    
    // 省略...
}
@Test
public void testCustomRealm(){
    DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
    
    CustomRealm customRealm = new CustomRealm();
    // 加密
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    // 采用MD5加密
    matcher.setHashAlgorithmName("md5");
    // 设置加密次数
    matcher.setHashIterations(2);
    customRealm.setCredentialsMatcher(matcher);
    defaultSecurityManager.setRealm(customRealm);
    
    SecurityUtils.setSecurityManager(defaultSecurityManager);
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
    try {
        subject.login(token);
        System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
        //检查是否拥有角色
        subject.checkRoles("admin");
        //检查是否拥有权限
        subject.checkPermissions("user:delete");
    }catch (IncorrectCredentialsException exception){
        System.out.println("用户名或密码错误");
    }catch (LockedAccountException exception){
        System.out.println("账号已被锁定");
    }catch (DisabledAccountException exception){
        System.out.println("账号已被禁用");
    }catch (UnknownAccountException exception){
        System.out.println("用户不存在");
    }catch ( UnauthorizedException ae ) {
        System.out.println("用户没有权限");
    }
}

当两个用户的密码相同时,单纯使用不加盐的MD5加密方式,会发现数据库中存在相同结构的密码,这样也是不安全的。我们希望即 便是两个人的原始密码一样,加密后的结果也不一样。如何做到呢?其实就好像炒菜一样,两道一样的鱼香肉丝,加的盐不一样,炒 出来的味道就不一样。MD5加密也是一样,需要进行盐值加密。

public class CustomRealm extends AuthorizingRealm {

    /**
     * 获取用户认证信息:用户名+密码
     * @param authenticationToken
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String)authenticationToken.getPrincipal();
        String password = getPasswordByUsername(username);
        String salt = UUID.randomUUID().toString().substring(5);
        String matcherPassword = getPasswordMatcher(password,salt);
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,matcherPassword,getName());
        simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
        return simpleAuthenticationInfo;
    }

    /**
     * 获取密文密码
     * @param currentPassword
     * @param salt 
     */
    private String getPasswordMatcher(String currentPassword,String salt){
        return new Md5Hash(currentPassword, salt).toString();
    }
   
    // 省略...
}

测试:

@Test
public void testCustomRealm(){
    DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();

    CustomRealm customRealm = new CustomRealm();
    // 加密
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    // 采用MD5加密
    matcher.setHashAlgorithmName("md5");
    // 设置加密次数
    matcher.setHashIterations(1);
    customRealm.setCredentialsMatcher(matcher);
    defaultSecurityManager.setRealm(customRealm);
    
    SecurityUtils.setSecurityManager(defaultSecurityManager);

    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
    try {
        subject.login(token);
        System.out.println("用户的认证状态:isAuthenticated="+subject.isAuthenticated());
        //检查是否拥有角色
        subject.checkRoles("admin");
        //检查是否拥有权限
        subject.checkPermissions("user:delete");
    }catch (IncorrectCredentialsException exception){
        System.out.println("用户名或密码错误");
    }catch (LockedAccountException exception){
        System.out.println("账号已被锁定");
    }catch (DisabledAccountException exception){
        System.out.println("账号已被禁用");
    }catch (UnknownAccountException exception){
        System.out.println("用户不存在");
    }catch ( UnauthorizedException ae ) {
        System.out.println("用户没有权限");
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我一直在流浪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值