Shiro安全框架(一)

1. 是什么

apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的
API,可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
Apache Shiro 的首要目标是易于使用和理解。安全有时候是很复杂的,甚至是痛苦的,但它没有必要这样。框架 应该尽可能掩盖复杂的地方,露出一个干净而直观的 API,来简化开发人员在使他们的应用程序安全上的努力。以 下是可以用 Apache Shiro 所做的事情:

  • 验证用户来核实他们的身份
  • 对用户执行访问控制,如:
    • 判断用户是否被分配了一个确定的安全角色
    • 判断用户是否被允许做某事
  • 在任何环境下使用 Session API,即使没有 Web 或 EJB 容器。 在身份验证,访问控制期间或在会话的生命周期,对事件作出反应
  • 聚集一个或多个用户安全数据的数据源,并作为一个单一的复合用户“视图”。
  • 启用单点登录(SSO)功能。关于单点登录看百度百科,或者这位的博客;
  • 为没有关联到登录的用户启用"Remember Me"服务

2. 去哪下

官网
在这里插入图片描述

3. 怎么玩

3.1 主要功能

在这里插入图片描述
这张图真是在网上的博客中到处都是。没啥意思。主要的几个模块功能如下:

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份。
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情。
  • Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话 中;会话可以是普通JavaSE环境的,也可以是如Web环境的。
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
  • Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。
  • Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。
  • Testing:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全。
  • “Run As”:一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用
  • “Remember Me”:记住我。

肯定的,上面的这些看完肯定是不知道说了啥。来,让我挑几个,按照我的理解来说一下吧

  • Authentication:登录的时候判断 用户账号密码是否正确。
  • Authorization:上面的判断通过之后, 判断用户的角色和权限的
  • Session Management:Shiro不管是不是在web环境下,它都有自己的session。Session Management就是管理session要存放在哪里。对session进行管理的。因为分布式的情况下,就会有一个共享的session。Shiro支持将sessin放在 mysql,memcached,redis等,这些数据库中。一般来说主要是redis中吧,还有session的生命周期
  • Cryptography:这个就是加密,将用户的密码加密放在数据库中去。这个可以用,也可以不用。如果不用就自己手动加密。保存就好,在Authentication 中,将用户传来的密码又手动加密一下,在和数据库里面存储的做对比。

3.2 体系架构

3.2.1 主要概念

图1
                                                   图一

Shiro的架构有3个主要的概念:SubjectSecurityManagerRealms
按照上面图上的英语先自己看一下,具体的解析在下面。稍等

3.2.2 详细架构

在这里插入图片描述

  • Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;也就是当前登录的用户,通过这个对象就能获得角色和权限,所以说这个对象就是角色和权限的一个实体
  • SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心 脏;所有具体的交互都通过 SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。管理所有的object,也就是管理所有的角色和权限,那和这些相关的它都能管理到。这就是一个核心
  • Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实 现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;看密码和账号是不是正确的
  • Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的 哪些功能;找用户的角色和权限
  • Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可 以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储; 所以我们一般在应用中都需要实现自己的Realm;这里就是写怎么怎么在数据库中查找用户角色和权限的。
  • SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个 组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、E JB等环境; 所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;
  • SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可 以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的 Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到 缓存中后可以提高访问的性能
  • Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

按照 图一 的描述
一个最简单的一个Shiro应用:

  1. 应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
  2. 我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。

4. 入门案例

这个案例就是一个简单的入门案例,和Spring的整合在下面的章节

4.1 pom文件

 <dependencies>
   <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.12</version> <scope>test</scope>
   </dependency>
</dependencies>

    <!--添加log4j2相关jar包-->
       <dependency>
           <groupId>org.apache.logging.log4j</groupId>
           <artifactId>log4j-api</artifactId>
           <version>2.7</version>
       </dependency>
       <dependency>
           <groupId>org.apache.logging.log4j</groupId>
           <artifactId>log4j-core</artifactId>
           <version>2.7</version>
       </dependency>

4.2 用户登录

  • shiro.ini文件(位置在 src/main/resources目录)
    这个文件都是我从官网上弄来的,绝对靠谱。
# =============================================================================
#Tutorial INI configuration
#
#Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
#=============================================================================

#-----------------------------------------------------------------------------
#Users and their (optional) assigned roles
#username = password, role1, role2, ..., roleN
#-----------------------------------------------------------------------------
[users]
#数据格式 用户名=密码,角色1,角色2……
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
  • 代码验证
package com.liuchen;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;

/**
* @program: shrio
* @description: 原生test
* @author: lc
* @date: 2020/6/26
**/
public class NativeTest {
   /**
    *  这里的值就是在 shiro.ini 配置的值
    */
   public static String userName = "root";
   /**
    *  这里的值就是在 shiro.ini 配置的值
    */
   public static String password = "secret";

   public static void main(String[] args) {
       Logger logger = LogManager.getLogger(NativeTest.class);

       //1.加载ini配置文件创建SecurityManager工厂类
       IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
       //2.获取securityManager
       SecurityManager securityManager = factory.getInstance();
       //3.将securityManager绑定到当前运行环境
       SecurityUtils.setSecurityManager(securityManager);
       //4.创建主体(此时的主体还为经过认证)
       Subject currentUser = SecurityUtils.getSubject();
       /**
        * 这里啊 就假设数据是从前端传过来的 用户名和密码了
        */
       //判断当前用户是否登录过。也就是是否通过了 Authentication 认证了。
       if (!currentUser.isAuthenticated()) {
           try {
               logger.info("用户开始登录:{}", userName);
               //5.构造主体登录的凭证(即用户名/密码)
               //第一个参数:登录用户名,第二个参数:登录密码
               UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
               //6.主体登录
               currentUser.login(token);
               //7.验证是否登录成功
               logger.info("用户登录成功= {}", currentUser.isAuthenticated());
//                System.out.println("用户登录成功=" + currentUser.isAuthenticated());
               //getPrincipal 获取登录成功的安全数据
               System.out.println(currentUser.getPrincipal());

               //登录尝试失败,可以捕获各种特定的异常,这些异常可以准确地告诉发生了什么,并允许进行相应的处理和响应:
               //在这里 可以根据业务需求来匹配。详情请看 http://shiro.apache.org/static/current/apidocs/org/apache/shiro/authc/AuthenticationException.html
           } catch (UnknownAccountException uae) {
               //用户名不存在
               logger.info("用户不存在:{}", userName);
               logger.info(uae.getMessage());
           } catch (IncorrectCredentialsException ice) {
               //密码不匹配
               logger.info("密码错误:{}", userName);
               logger.info(ice.getMessage());
           } catch (LockedAccountException lae) {
               //账户或者密码锁定
               logger.info("账号锁定:{}", userName);
               logger.info(lae.getMessage());
           } catch (AuthenticationException ae) {
               logger.info("未通过授权:{}", userName);
               logger.info(ae.getMessage());
           }
       }
   }
}

在这里插入图片描述
如果将userName换掉,就会抛出异常,程序会走到 UnknownAccountException cath块里面。别的异常在之后慢慢的尝试吧

4.3 用户授权

  • 在刚才的shiro.ini 文件里面追加下面的内容
# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
# 模拟从数据库查询的角色和权限列表 #数据格式 角色名=权限1,权限2
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

看代码

package com.liuchen;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;

/**
 * @program: shrio
 * @description: 原生test
 * @author: lc
 * @date: 2020/6/26
 **/
public class NativeTest {
    /**
     *  这里的值就是在 shiro.ini 配置的值
     */
    public static String userName = "darkhelmet";
    /**
     *  这里的值就是在 shiro.ini 配置的值
     */
    public static String password = "ludicrousspeed";

    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(NativeTest.class);

        //1.加载ini配置文件创建SecurityManager工厂类
        IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //2.获取securityManager
        SecurityManager securityManager = factory.getInstance();
        //3.将securityManager绑定到当前运行环境
        SecurityUtils.setSecurityManager(securityManager);
        //4.创建主体(此时的主体还为经过认证)
        Subject currentUser = SecurityUtils.getSubject();
        /**
         * 这里啊 就假设数据是从前端传过来的 用户名和密码了
         */
        //判断当前用户是否登录过。也就是是否通过了 Authentication 认证了。
        if (!currentUser.isAuthenticated()) {
            try {
                logger.info("用户开始登录:{}", userName);
                //5.构造主体登录的凭证(即用户名/密码)
                //第一个参数:登录用户名,第二个参数:登录密码
                UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
                //6.主体登录
                currentUser.login(token);
                //7.验证是否登录成功
                logger.info("用户登录成功= {}", currentUser.isAuthenticated());
//                System.out.println("用户登录成功=" + currentUser.isAuthenticated());
                //getPrincipal 获取登录成功的安全数据
                System.out.println(currentUser.getPrincipal());

                //8.用户认证成功之后才可以完成授权工作
                /*******************************************************

						用户授权判断 
				************************************************************
                */
               logger.info("{}是否有lightsaber:save的权限:{}",userName,currentUser.isPermitted("lightsaber:save"));

                //登录尝试失败,可以捕获各种特定的异常,这些异常可以准确地告诉发生了什么,并允许进行相应的处理和响应:
                //在这里 可以根据业务需求来匹配。详情请看 http://shiro.apache.org/static/current/apidocs/org/apache/shiro/authc/AuthenticationException.html
            } catch (UnknownAccountException uae) {
                //用户名不存在
                logger.info("用户不存在:{}", userName);
                logger.info(uae.getMessage());
            } catch (IncorrectCredentialsException ice) {
                //密码不匹配
                logger.info("密码错误:{}", userName);
                logger.info(ice.getMessage());
            } catch (LockedAccountException lae) {
                //账户或者密码锁定
                logger.info("账号锁定:{}", userName);
                logger.info(lae.getMessage());
            } catch (AuthenticationException ae) {
                logger.info("未通过授权:{}", userName);
                logger.info(ae.getMessage());
            }
        }
    }
}

这个和上面的差别就是多了一行

  logger.info("{}是否有lightsaber:save的权限:{}",userName,currentUser.isPermitted("lightsaber:save"));

用户判断用户有没有这个权限。关于shiro.ini 这个文件的详细的配置请看这篇文章
当然在实际中,没有人会直接在ini文件中直接写死权限的配置的。还记得上面说的Realm么?,实际中,权限和角色的数据都是从这个里面通过查找数据库找的

4.4 自定义Realm

Realm域:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么 它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行 验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源

4.4.1 自定义 realm
自定义realm,需要继承AuthorizingRealm父类
重写父类中的两个方法 
	- doGetAuthorizationInfo :授权 
	- doGetAuthenticationInfo :认证

自定义的realm

package com.liuchen;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.ArrayList;
import java.util.List;

/**
 * @program: shrio
 * @description: 自定义Realm
 * @author: lc
 * @date: 2020/6/26
 **/
public class MyPermissionRealm extends AuthorizingRealm {
    Logger logger = LogManager.getLogger(NativeTest.class);

    /**
     * 给这个realm给个名字
     *
     * @param name
     */
    @Override
    public void setName(String name) {
        super.setName("MyPermissionRealm");
    }

    /**
     * 授权:授权的主要目的就是查询数据库获取用户的所有角色和权限信息
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 1.从principals获取已认证用户的信息
        String username = (String) principalCollection.getPrimaryPrincipal();
        /**
         * 正式系统:应该从数据库中根据用户名或者id查询 * 这里为了方便演示,手动构造
         * 实际中,这里就是显示的是配置的权限,一般都是页面的路由地址和请求的url
         */
        // 2.模拟从数据库中查询的用户所有权限
        List<String> permissions = new ArrayList<String>();
        permissions.add("user:save");
        permissions.add("user:delete");
        permissions.add("user:update");// 商品更新权限


        // 3.模拟从数据库中查询的用户所有角色
        /**
         * 实际中这些个角色可能就是 部门经理啊,人事啊。这些角色.
         */
        List<String> roles = new ArrayList<String>();
        roles.add("role1");
        roles.add("role2");

        // 4.构造权限数据
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 5.将查询的权限数据保存到 simpleAuthorizationInfo
        simpleAuthorizationInfo.addStringPermissions(permissions);
        // 6.将查询的角色数据保存到simpleAuthorizationInfo
        simpleAuthorizationInfo.addRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 认证:认证的主要目的,比较用户输入的用户名密码是否和数据库中的一致
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
        String username = usernamePasswordToken.getUsername();
        String password = new String(usernamePasswordToken.getPassword());
        /**
         * 实际来说,这里都是从数据库中查找数据的. 这里我就模拟一下就好了
         */
        if ("admin".equals(username) && "admin".equals(password)) {
            logger.info("{} 登录成功:", username);
            return new SimpleAuthenticationInfo(username, password, this.getName());
        } else {
            throw new RuntimeException("用户名或者密码错误");
        }

    }
}
4.4.2 将自己写的realm 告诉 securityManager
# =============================================================================
# Tutorial INI configuration
#
# Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
# =============================================================================
# 注意:注意: # 开头表示注释哦.
# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
# [users]
# 数据格式 用户名=密码,角色1,角色2……
# root = secret, admin
# guest = guest, guest
# presidentskroob = 12345, president
# darkhelmet = ludicrousspeed, darklord, schwartz
# lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
# 模拟从数据库查询的角色和权限列表 #数据格式 角色名=权限1,权限2
# [roles]
# admin = *
# schwartz = lightsaber:*
# goodguy = winnebago:drive:eagle5


[main]
# 声明Realm域  名字=全限定类名  这个名字是随便起的。
permReam=com.liuchen.MyPermissionRealm
#注册realm到securityManager中     securityManager.realms=$上面自己起的名字。注意。等号左边的不能动,这个就相当于Spring配置文件中的属性注入.
securityManager.realms=$permReam 
4.4.3 测试

这个还是之前的代码,修改了用户名和密码,下面是修改的代码
在这里插入图片描述在这里插入图片描述
下面是完整的代码

package com.liuchen;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;

/**
 * @program: shrio
 * @description: 原生test
 * @author: lc
 * @date: 2020/6/26
 **/
public class NativeTest {
    /**
     *  这里的值就是在 shiro.ini 配置的值
     */
    public static String userName = "admin";
    /**
     *  这里的值就是在 shiro.ini 配置的值
     */
    public static String password = "admin";

    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(NativeTest.class);

        //1.加载ini配置文件创建SecurityManager工厂类
        IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //2.获取securityManager
        SecurityManager securityManager = factory.getInstance();
        //3.将securityManager绑定到当前运行环境
        SecurityUtils.setSecurityManager(securityManager);
        //4.创建主体(此时的主体还为经过认证)
        Subject currentUser = SecurityUtils.getSubject();
        /**
         * 这里啊 就假设数据是从前端传过来的 用户名和密码了
         */
        //判断当前用户是否登录过。也就是是否通过了 Authentication 认证了。
        if (!currentUser.isAuthenticated()) {
            try {
                logger.info("用户开始登录:{}", userName);
                //5.构造主体登录的凭证(即用户名/密码)
                //第一个参数:登录用户名,第二个参数:登录密码
                UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
                //6.主体登录
                currentUser.login(token);
                //7.验证是否登录成功
                logger.info("用户登录成功= {}", currentUser.isAuthenticated());
//                System.out.println("用户登录成功=" + currentUser.isAuthenticated());
                //getPrincipal 获取登录成功的安全数据
                System.out.println(currentUser.getPrincipal());

                //7.用户认证成功之后才可以完成授权工作
               logger.info("{}是否有user:save的权限:{}",userName,currentUser.isPermitted("user:save"));
                logger.info("{}是否有role1的权限:{}",userName,currentUser.hasRole("role1"));

                //登录尝试失败,可以捕获各种特定的异常,这些异常可以准确地告诉发生了什么,并允许进行相应的处理和响应:
                //在这里 可以根据业务需求来匹配。详情请看 http://shiro.apache.org/static/current/apidocs/org/apache/shiro/authc/AuthenticationException.html
            } catch (UnknownAccountException uae) {
                //用户名不存在
                logger.info("用户不存在:{}", userName);
                logger.info(uae.getMessage());
            } catch (IncorrectCredentialsException ice) {
                //密码不匹配
                logger.info("密码错误:{}", userName);
                logger.info(ice.getMessage());
            } catch (LockedAccountException lae) {
                //账户或者密码锁定
                logger.info("账号锁定:{}", userName);
                logger.info(lae.getMessage());
            } catch (AuthenticationException ae) {
                logger.info("未通过授权:{}", userName);
                logger.info(ae.getMessage());
            }
        }
    }
}

在这里插入图片描述

哦。对了,我里面的日志用户的log4j2,下面是它的配置文件 lo4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </Console>
    </Appenders>
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>
</Configuration>

差点也忘了,贴出我的 github 上的demo,这个里面还有 jwt的简单使用,jwt我就没这么系统的阐述了,readme.md上面记录了一点我的思路。https://github.com/daliuchen/shrio.git
下一篇是SpringBoot和shiro的整合.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值