Shiro入门

身份认证

基本流程

在这里插入图片描述
流程如下:

  1. Shiro把用户的数据封装成标识token,token一般封装着用户名,密码等信息
  2. 使用Subject门面获取到封装着用户的数据的标识token
  3. Subject把标识token交给SecurityManager,在SecurityManager安全中心中,SecurityManager把标识token委托给认证器Authenticator进行身份验证。认证器的作用一般是用来指定如何验证,它规定本次认证用到哪些Realm
  4. 认证器Authenticator将传入的标识token,与数据源Realm对比,验证token是否合法

演示

需求

使用shiro完成一个用户的登录

实现

新建项目
在这里插入图片描述

导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-> 4.0.0.xsd">
    <parent>
        <artifactId>shiro</artifactId>
        <groupId>cn.com.javakf</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <!--身份认证-->
    <artifactId>shiro01_authenticator</artifactId>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.3</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <!-- compiler插件, 设定JDK版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                    <showWarnings>true</showWarnings>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>

编写shiro.ini

#声明用户账号
[users]
javakf=123456

HelloShiro

package cn.com.javakf;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Test;

/**
 * @Description:身份认证
 */
public class HelloShiro {

    @Test
    public void shiroLogin() {
        //导入INI配置创建工厂
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //工厂构建安全管理器
        SecurityManager securityManager = factory.getInstance();
        //使用工具生效安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        //使用工具获得subject主体
        Subject subject = SecurityUtils.getSubject();
        //构建账户密码
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("javakf", "123456");
        //使用subject主体去登录
        subject.login(usernamePasswordToken);
        //打印登录信息
        System.out.println("登录结果:" + subject.isAuthenticated());
    }

}

测试

在这里插入图片描述

小结

1、权限定义:ini文件
2、加载过程:
  导入权限ini文件构建权限工厂
  工厂构建安全管理器
  使用SecurityUtils工具生效安全管理器
  使用SecurityUtils工具获得主体
  使构建账号token用SecurityUtils工具获得主体
  构建账号token
  登录操作

Realm

Realm接口

在这里插入图片描述
所以,一般在真实的项目中,我们不会直接实现Realm接口,我们一般的情况就是直接继承AuthorizingRealm,能够继承到认证与授权功能。

它需要强制重写两个方法

public class DefinitionRealm extends AuthorizingRealm {
 
    /**
	 * @Description 认证
	 * @param authcToken token对象
	 * @return 
	 */
	public abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
        return null;
    }

	/**
	 * @Description 鉴权
	 * @param principals 令牌
	 * @return
	 */
	public abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
        return null;
    }
}

自定义Realm

需求

自定义Realm,取得密码用于比较

实现

创建项目
在这里插入图片描述
定义SecurityService

package cn.com.javakf.service;

/**
 * @Description:模拟数据库操作服务接口
 */
public interface SecurityService {

    /**
     * @param loginName 用户名称
     * @return 密码
     * @Description 查找用户密码
     */
    String findPasswordByLoginName(String loginName);

}

SecurityServiceImpl

package cn.com.javakf.service.impl;

import cn.com.javakf.service.SecurityService;

/**
 * @Description:模拟数据库操作服务接口实现
 */
public class SecurityServiceImpl implements SecurityService {

    @Override
    public String findPasswordByLoginName(String loginName) {
        return "123456";
    }

}

定义DefinitionRealm

package cn.com.javakf.realm;

import cn.com.javakf.service.SecurityService;
import cn.com.javakf.service.impl.SecurityServiceImpl;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
 * @Description:声明自定义realm
 */
public class DefinitionRealm extends AuthorizingRealm {

    /**
     * @Description 认证方法
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取登录名
        String loginName = (String) authenticationToken.getPrincipal();
        SecurityService securityService = new SecurityServiceImpl();
        String password = securityService.findPasswordByLoginName(loginName);
        if ("".equals(password)) {
            throw new UnknownAccountException("账户不存在");
        }
        return new SimpleAuthenticationInfo(loginName, password, getName());
    }

    /**
     * @Description 鉴权方法
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }


}

编辑shiro.ini

#声明自定义的realm,且为安全管理器指定realms
[main]
definitionRealm=cn.com.javakf.realm.DefinitionRealm
securityManager.realms=$definitionRealm
#声明用户账号
#[users]
#javakf=123456

测试

在这里插入图片描述

认证源码跟踪

1)通过debug模式追踪源码subject.login(token) 发现。首先是进入Subject接口的默认实现类。果然,Subject将用户的用户名密码委托给了securityManager去做。
在这里插入图片描述

2)然后,securityManager说:“卧槽,认证器authenticator小弟,听说你的大学学的专业就是认证呀,那么这个认证的任务就交给你咯”。遂将用户的token委托给内部认证组件authenticator去做
在这里插入图片描述

3)事实上,securityManager的内部组件一个比一个懒。内部认证组件authenticator说:“你们传过来的token我需要拿去跟数据源Realm做对比,这样吧,这个光荣的任务就交给Realm你去做吧”。Realm对象:“一群大懒虫!”。
在这里插入图片描述

4)Realm在接到内部认证组件authenticator组件后很伤心,最后对电脑前的你说:“大兄弟,对不住了,你去实现一下呗”。从图中的方法体中可以看到,当前对象是Realm类对象,即将调用的方法是doGetAuthenticationInfo(token)。而这个方法,就是你即将要重写的方法。如果帐号密码通过了,那么返回一个认证成功的info凭证。如果认证失败,抛出一个异常就好了。你说:“什么?最终还是劳资来认证?”没错,就是苦逼的你去实现了,谁叫你是程序猿呢。所以,你不得不查询一下数据库,重写doGetAuthenticationInfo方法,查出来正确的帐号密码,返回一个正确的凭证info
在这里插入图片描述
5)好了,这个时候你自己编写了一个类,继承了AuthorizingRealm,并实现了上述doGetAuthenticationInfo方法。你在doGetAuthenticationInfo中编写了查询数据库的代码,并将数据库中存放的用户名与密码封装成了一个AuthenticationInfo对象返回。可以看到下图中,info这个对象是有值的,说明从数据库中查询出来了正确的帐号密码
在这里插入图片描述
6)那么,接下来就很简单了。把用户输入的帐号密码与刚才你从数据库中查出来的帐号密码对比一下即可。token封装着用户的帐号密码,AuthenticationInfo封装着从数据库中查询出来的帐号密码。再往下追踪一下代码,最终到了下图中的核心区域。如果没有报异常,说明本次登录成功。
在这里插入图片描述

编码、解码

Shiro提供了base64和16进制字符串编码/解码的API支持,方便一些编码解码操作。

Shiro内部的一些数据的【存储/表示】都使用了base64和16进制字符串。

演示

需求

理解base64和16进制字符串编码/解码

实现

新建项目
在这里插入图片描述

新建EncodesUtil

package cn.com.javakf.utils;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.Hex;

/**
 * @Description:封装base64和16进制编码解码工具类
 */
public class EncodesUtil {

    /**
     * @param input 输入数组
     * @return String
     * @Description HEX-byte[]--String转换
     */
    public static String encodeHex(byte[] input) {
        return Hex.encodeToString(input);
    }

    /**
     * @param input 输入字符串
     * @return byte数组
     * @Description HEX-String--byte[]转换
     */
    public static byte[] decodeHex(String input) {
        return Hex.decode(input);
    }

    /**
     * @param input 输入数组
     * @return String
     * @Description Base64-byte[]--String转换
     */
    public static String encodeBase64(byte[] input) {
        return Base64.encodeToString(input);
    }

    /**
     * @param input 输入字符串
     * @return byte数组
     * @Description Base64-String--byte[]转换
     */
    public static byte[] decodeBase64(String input) {
        return Base64.decode(input);
    }

}

新建ClientTest

package cn.com.test;


import cn.com.javakf.utils.EncodesUtil;
import org.junit.Test;

/**
 * @Description:测试
 */
public class ClientTest {

    /**
     * @Description 测试16进制编码
     */
    @Test
    public void testHex() {
        String val = "javakf";
        String flag = EncodesUtil.encodeHex(val.getBytes());
        String valHandler = new String(EncodesUtil.decodeHex(flag));
        System.out.println("比较结果:" + val.equals(valHandler));
    }

    /**
     * @Description 测试base64编码
     */
    @Test
    public void testBase64() {
        String val = "javakf";
        String flag = EncodesUtil.encodeBase64(val.getBytes());
        String valHandler = new String(EncodesUtil.decodeBase64(flag));
        System.out.println("比较结果:" + val.equals(valHandler));
    }


}

测试

请添加图片描述

散列算法

散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如salt(即盐);这样散列的对象是“密码+salt”,这样生成的散列值相对来说更难破解。

shiro支持的散列算法:

  • Md2Hash
  • Md5Hash
  • Sha1Hash
  • Sha256Hash
  • Sha384Hash
  • Sha512Hash

在这里插入图片描述

演示

需求

理解散列算法

实现

新建项目
在这里插入图片描述

新建DigestsUtil

package cn.com.javakf.utils;

import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;

import java.util.HashMap;
import java.util.Map;

/**
 * @Description:摘要
 */
public class DigestsUtil {

    private static final String SHA1 = "SHA-1";

    private static final Integer ITERATIONS = 512;

    /**
     * @param input 需要散列字符串
     * @param salt  盐字符串
     * @return
     * @Description sha1方法
     */
    public static String sha1(String input, String salt) {
        return new SimpleHash(SHA1, input, salt, ITERATIONS).toString();
    }

    /**
     * @return
     * @Description 随机获得salt字符串
     */
    public static String generateSalt() {
        SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
        return randomNumberGenerator.nextBytes().toHex();
    }


    /**
     * @param
     * @return
     * @Description 生成密码字符密文和salt密文
     */
    public static Map<String, String> entryptPassword(String passwordPlain) {
        Map<String, String> map = new HashMap<>();
        String salt = generateSalt();
        String password = sha1(passwordPlain, salt);
        map.put("salt", salt);
        map.put("password", password);
        return map;
    }
}

新建ClientTest

package cn.com.javakf.test;


import cn.com.javakf.utils.DigestsUtil;
import org.junit.Test;

import java.util.Map;

/**
 * @Description:测试
 */
public class ClientTest {

    @Test
    public void testDigestsUtil() {
        Map<String, String> map = DigestsUtil.entryptPassword("123456");
        System.out.println("获得结果:" + map.toString());
    }


}

测试

在这里插入图片描述

Realm使用散列算法

上面我们了解编码,解码,以及散列算法,那么在realm中怎么使用?在shiro02_realm中我们使用的密码是明文的校验方式,也就是SecurityServiceImpl中findPasswordByLoginName返回的是明文123456的密码

package cn.com.javakf.service.impl;

import cn.com.javakf.service.SecurityService;

/**
 * @Description:模拟数据库操作服务接口实现
 */
public class SecurityServiceImpl implements SecurityService {

    @Override
    public String findPasswordByLoginName(String loginName) {
        return "123456";
    }

}

演示

需求

Realm使用散列算法

实现

新建项目
在这里插入图片描述

创建密文密码
  
使用ClientTest的testDigestsUtil创建密码为“123456”的password密文和salt密文

password=d4906b788ecb221866ef851db5e4f0306844bd7f, salt=d72ed5895db97729ce2e096fc1f1ab7f}

修改SecurityService

package cn.com.javakf.service;

import java.util.Map;

/**
 * @Description:模拟数据库操作服务接口
 */
public interface SecurityService {

    /**
     * @param loginName 用户名称
     * @return 密码
     * @Description 查找用户密码
     */
    Map<String, String> findPasswordByLoginName(String loginName);

}
package cn.com.javakf.service.impl;

import cn.com.javakf.service.SecurityService;
import cn.com.javakf.utils.DigestsUtil;

import java.util.Map;

/**
 * @Description:模拟数据库操作服务接口实现
 */
public class SecurityServiceImpl implements SecurityService {

    @Override
    public Map<String, String> findPasswordByLoginName(String loginName) {
    	//模拟数据库中存储的密文信息
        return DigestsUtil.entryptPassword("123456");
    }

}

指定密码匹配方式
  
为DefinitionRealm类添加构造方法如下:

public DefinitionRealm() {
	//指定密码匹配方式sha1
	HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(DigestsUtil.SHA1);
	//指定密码迭代此时
	hashedCredentialsMatcher.setHashIterations(DigestsUtil.ITERATIONS);
	//使用父层方法是匹配方式生效
	setCredentialsMatcher(hashedCredentialsMatcher);
}

修改DefinitionRealm类的认证doGetAuthenticationInfo方法如下:

/**
 * @Description 认证方法
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
	//获取登录名
	String loginName = (String) authenticationToken.getPrincipal();
	SecurityService securityService = new SecurityServiceImpl();
	Map<String, String> map = securityService.findPasswordByLoginName(loginName);
	if (map.isEmpty()) {
		throw new UnknownAccountException("账户不存在");
	}
	String salt = map.get("salt");
	String password = map.get("password");
	//传递账号和密码:参数1:登录名,参数2:明文密码,参数三:字节salt,参数4:当前DefinitionRealm名称
	return new SimpleAuthenticationInfo(loginName, password, ByteSource.Util.bytes(salt), getName());
}

测试

在这里插入图片描述

身份授权

基本流程

在这里插入图片描述

  1. 首先调用Subject.isPermitted/hasRole接口,其会委托给SecurityManager。
  2. SecurityManager接着会委托给内部组件Authorizer;
  3. Authorizer再将其请求委托给我们的Realm去做;Realm才是真正干活的;
  4. Realm将用户请求的参数封装成权限对象。再从我们重写的doGetAuthorizationInfo方法中获取从数据库中查询到的权限集合。
  5. Realm将用户传入的权限对象,与从数据库中查出来的权限对象,进行一一对比。如果用户传入的权限对象在从数据库中查出来的权限对象中,则返回true,否则返回false。

进行授权操作的前提:用户必须通过认证。

在真实的项目中,角色与权限都存放在数据库中。为了快速上手,我们先创建一个自定义DefinitionRealm,模拟它已经登录成功。直接返回一个登录验证凭证,告诉Shiro框架,我们从数据库中查询出来的密码是也是就是你输入的密码。所以,不管用户输入什么,本次登录验证都是通过的。

/**
 * @Description 认证接口
 * @param token 传递登录token
 * @return
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //从AuthenticationToken中获得登录名称
    String loginName = (String) token.getPrincipal();
    SecurityService securityService = new SecurityServiceImpl();
    Map<String, String> map = securityService.findPasswordByLoginName(loginName);
    if (map.isEmpty()){
        throw new UnknownAccountException("账户不存在");
    }
    String salt = map.get("salt");
    String password = map.get("password");
     //传递账号和密码:参数1:登录名,参数2:明文密码,参数三:字节salt,参数4:当前DefinitionRealm名称
    return  new SimpleAuthenticationInfo(loginName,password, ByteSource.Util.bytes(salt),getName());
}

好了,接下来,我们要重写doGetAuthorizationInfo核心方法了。在DefinitionRealm中找到下列方法:

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    return null;
}

此方法的传入的参数PrincipalCollection principals,是一个包装对象,它表示"用户认证凭证信息"。包装的是谁呢?没错,就是认证doGetAuthenticationInfo()方法的返回值的第一个参数loginName。你可以通过这个包装对象的getPrimaryPrincipal()方法拿到此值,然后再从数据库中拿到对应的角色和资源,构建SimpleAuthorizationInfo。

/**
 * @Description 授权方法
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    //拿到用户认证凭证信息
    String loginName = (String) principals.getPrimaryPrincipal();
    //从数据库中查询对应的角色和资源
    SecurityService securityService = new SecurityServiceImpl();
    List<String> roles = securityService.findRoleByloginName(loginName);
    List<String> permissions = securityService.findPermissionByloginName(loginName);
    //构建资源校验
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    authorizationInfo.addRoles(roles);
    authorizationInfo.addStringPermissions(permissions);
    return authorizationInfo;
}

演示

需求

1、实现doGetAuthorizationInfo方法实现鉴权
2、使用subject类实现权限的校验

实现

创建项目
在这里插入图片描述

编写SecurityService

在SecurityService中添加

/**
 * @param loginName 用户名
 * @return 角色字符串列表
 * @Description 查询角色
 */
List<String> findRoleByLoginName(String loginName);

/**
 * @param loginName 用户名
 * @return 资源字符串列表
 * @Description 查询资源
 */
List<String> findPermissionByLoginName(String loginName);

SecurityServiceImpl添加实现

@Override
public List<String> findRoleByLoginName(String loginName) {
	List<String> list = new ArrayList<>();
	list.add("admin");
	list.add("dev");
	return list;
}

@Override
public List<String> findPermissionByLoginName(String loginName) {
	List<String> list = new ArrayList<>();
	list.add("order:add");
	list.add("order:list");
	list.add("order:del");
	return list;
}

编写DefinitionRealm

在DefinitionRealm中修改doGetAuthorizationInfo方法如下

/**
 * @Description 鉴权方法
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
	//拿到用户凭证信息
	String loginName = (String) principalCollection.getPrimaryPrincipal();
	//从数据库中查询对应的角色和权限
	SecurityService securityService = new SecurityServiceImpl();
	List<String> roles = securityService.findRoleByLoginName(loginName);
	List<String> permissions = securityService.findPermissionByLoginName(loginName);
	//构建资源校验对象
	SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
	simpleAuthorizationInfo.addRoles(roles);
	simpleAuthorizationInfo.addStringPermissions(permissions);
	return simpleAuthorizationInfo;
}

编写HelloShiro

package cn.com.javakf;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Test;

/**
 * @Description:身份认证
 */
public class HelloShiro {

    @Test
    public void testPermissionRealm() {
        Subject subject = shiroLogin();
        //打印登录信息
        System.out.println("登录结果:" + subject.isAuthenticated());
        //-------校验当前用户是否拥有管理员的架势
        System.out.println("是否有管理员角色" + subject.hasRole("admin"));
        //-------校验当前用户没有的角色
        try {
            subject.checkRole("coder");
            System.out.println("当前用户有coder角色");
        } catch (Exception ex) {
            System.out.println("当前用户没有coder角色");
        }
        //-------校验当前用户的权限信息
        System.out.println("是否有查看订单的权限" + subject.isPermitted("order:list"));
        //-------校验当前用户满意的权限
        try {
            subject.checkPermission("order:update");
            System.out.println("当前用户有修改的权限");
        } catch (Exception ex) {
            System.out.println("当前用户没有修改的权限");
        }
    }


    public Subject shiroLogin() {
        //导入INI配置创建工厂
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //工厂构建安全管理器
        SecurityManager securityManager = factory.getInstance();
        //使用工具生效安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        //使用工具获得subject主体
        Subject subject = SecurityUtils.getSubject();
        //构建账户密码
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("javakf", "123456");
        //使用subject主体去登录
        subject.login(usernamePasswordToken);
        return subject;
    }

}

测试

在这里插入图片描述

授权源码追踪

1)客户端调用 subject.hasRole(“admin”),判断当前用户是否有"admin"角色权限。
在这里插入图片描述
2)Subject门面对象接收到要被验证的角色信息"admin",并将其委托给securityManager中验证。
在这里插入图片描述
3)securityManager将验证请求再次委托给内部的小弟:内部组件Authorizer authorizer
在这里插入图片描述
4)内部小弟authorizer也是个混子,将其委托给了我们自定义的Realm去做
在这里插入图片描述
5) 先拿到PrincipalCollection principal对象,同时传入校验的角色循环校验,循环中先创建鉴权信息
在这里插入图片描述
6)先看缓存中是否已经有鉴权信息
在这里插入图片描述
在这里插入图片描述
7)都是一群懒货!!最后干活的还是我这个猴子!
在这里插入图片描述

小结

1、鉴权需要实现doGetAuthorizationInfo方法
2、鉴权使用门面subject中方法进行鉴权
  以check开头的会抛出异常
  以is和has开头会返回布尔值

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值