初涉shrio权限管理

最近接触项目,大佬要求我看一看权限管理部分的内容。首先要有权限管理的概念,其次是shrio的架构问题。

1.权限管理

只要有用户参与的系统一般都要有权限管理,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

对权限的管理又分为两大类别:

  • 用户认证
  • 用户授权

1.1用户认证

用户认证,用户去访问系统,系统要验证用户身份的合法性

最常用的用户身份验证的方法:1、用户名密码方式、2、指纹打卡机、3、基于证书验证方法。。系统验证用户身份合法,用户方可访问系统的资源。

举个例子:

  • 当我们输入了自己的淘宝的账户和密码,才能打开购物车

用户认证的流程:

  • 判断该资源能否不认证就能访问【登陆页面、首页】
  • 如果该资源需要认证后才能访问,那么判断该访问者是否认证了
  • 如果还没有认证,那么需要返回到【登陆页面】进行认证
  • 认证通过后才能访问资源

从用户认证我们可以抽取出这么几个概念

  • subject主体:理解为用户,可能是程序,都要去访问系统的资源,系统需要对subject进行身份认证
  • principal身份信息:通常是唯一的,一个主体还有多个身份信息,但是都有一个主身份信息(primary principal)【我们可以选择身份证认证、学生证认证等等都是我们的身份信息】
  • credential凭证信息:可以是密码 、证书、指纹。

总结:主体在进行身份认证时需要提供身份信息和凭证信息。

1.2用户授权


用户授权,简单理解为访问控制,在用户认证通过后,系统对用户访问资源进行控制,用户具有资源的访问权限方可访问。

用户授权的流程

  • 到达了用户授权环节,当然是需要用户认证之后了
  • 用户访问资源,系统判断该用户是否有权限去操作该资源
  • 如果该用户有权限才能够访问,如果没有权限就不能访问了

授权的过程可以简单理解为:主体认证之后,系统进行访问控制

subject必须具备资源的访问权限才可访问该资源..

权限/许可(permission) :针对资源的权限或许可,subject具有permission访问资源,如何访问/操作需要定义permission,权限比如:用户添加、用户修改、商品删除

资源可以分为两种

  • 资源类型:系统的用户信息就是资源类型,相当于java类。
  • 资源实例:系统中id为001的用户就是资源实例,相当于new的java对象。

2.权限管理模型

一般地,我们可以抽取出这么几个模型:

  • 主体(账号、密码)
  • 资源(资源名称、访问地址)
  • 权限(权限名称、资源id)
  • 角色(角色名称)
  • 角色和权限关系(角色id、权限id)
  • 主体和角色关系(主体id、角色id)

3.分配权限

用户需要分配相应的权限才可访问相应的资源。权限是对于资源的操作许可。

通常给用户分配资源权限需要将权限信息持久化,比如存储在关系数据库中。把用户信息、权限管理、用户分配的权限信息写到数据库(权限数据模型)

3.1基于角色访问控制

RBAC(role based access control),基于角色的访问控制。


//如果该user是部门经理则可以访问if中的代码
if(user.hasRole('部门经理')){
	//系统资源内容
	//用户报表查看
}

角色针对人划分的,人作为用户在系统中属于活动内容,如果该 角色可以访问的资源出现变更,需要修改你的代码了,


if(user.hasRole('部门经理') || user.hasRole('总经理')  ){
	//系统资源内容
	//用户报表查看
}

基于角色的访问控制是不利于系统维护(可扩展性不强)。

3.2基于资源的访问控制

RBAC(Resource based access control),基于资源的访问控制。

资源在系统中是不变的,比如资源有:类中的方法,页面中的按钮。


对资源的访问需要具有permission权限,代码可以写为:

if(user.hasPermission ('用户报表查看(权限标识符)')){
	//系统资源内容
	//用户报表查看
}

建议使用基于资源的访问控制实现权限管理。

4.shrio

shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。

spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。


  • subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
  • securityManager:安全管理器,主体进行认证和授权都 是通过securityManager进行。
  • authenticator:认证器,主体进行认证最终通过authenticator进行的。
  • authorizer:授权器,主体进行授权最终通过authorizer进行的。
  • sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
  • SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
  • cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
  • realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。

cryptography:密码管理,提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

5.shrio的优点

我们在使用URL拦截的时候,要将所有的URL都配置起来,繁琐、不易维护

而我们的Shiro实现系统的权限管理,有效提高开发效率,从而降低开发成本。

实战部分了出处

最先开始我采用的是建 用户表, 角色表, 权限表,之后在拦截器中对每一个请求进行拦截,再到数据库中进行查询看当前用户是否有该权限,这样的设计能满足大多数中小型系统的需求。不过这篇所介绍的Shiro能满足之前的所有需求,并且使用简单,安全性高,而且现在越来越的多企业都在使用Shiro,这应该是一个收入的你的技能库。

1.创建自定义MyRealm类

有关Shiro的基础请看前面的内容。接下来是干货了。最后会整合Spring来进行权限验证。

首先在使用Shiro的时候我们要考虑在什么样的环境下使用:

  • 登录的验证
  • 对指定角色的验证
  • 对URL的验证

基本上我们差不多只有这三个需求。所以同时我们也需要三个方法:

      1.findUserByUserName(String  username)根据username查询用户,之后Shiro会根据(一般是从数据库中)查询出来的User的密码来和提交上来的密码进行比对。

    2.findRoles(String username)根据username查询用户的所有角色,用于角色验证。

    3.findPermissions(String username)根据username查询他所拥有的权限信息,用于权限判断。

下面是mapper代码:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.crossoverJie.dao.T_userDao" >
    <resultMap id="BaseResultMap" type="com.crossoverJie.pojo.T_user" >
        <result property="id" column="id"/>
        <result property="userName" column="userName"/>
        <result property="password" column="password"/>
        <result property="roleId" column="roleId"/>
    </resultMap>
    <sql id="Base_Column_List" >
        id, username, password,roleId
    </sql>

    <select id="findUserByUsername" parameterType="String" resultMap="BaseResultMap">
        select <include refid="Base_Column_List"/>
        from t_user where userName=#{userName}
    </select>

    <select id="findRoles" parameterType="String" resultType="String">
        select r.roleName from t_user u,t_role r where u.roleId=r.id and u.userName=#{userName}
    </select>

    <select id="findPermissions" parameterType="String" resultType="String">
        select p.permissionName from t_user u,t_role r,t_permission p
        where u.roleId=r.id and p.roleId=r.id and u.userName=#{userName}
    </select>
</mapper>

三个方法都有对应点sql语句,应该很容易理解。看不下去的先把原博主的项目fork下来吧

接下来需要创建自定义的MyReal类,这个还是比较重要的。继承至shiro的AuthorizingRealm类,用于处理自己的验证逻辑,下面是代码:

package com.crossoverJie.shiro;

import com.crossoverJie.pojo.T_user;
import com.crossoverJie.service.T_userService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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 javax.annotation.Resource;
import java.util.Set;

/**
 * Created with IDEA
 * Created by ${jie.chen} on 2016/7/14.
 * Shiro自定义域
 */
public class MyRealm extends AuthorizingRealm {

    @Resource
    private T_userService t_userService;

    /**
     * 用于的权限的认证。
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = principalCollection.getPrimaryPrincipal().toString() ;
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo() ;
        Set<String> roleName = t_userService.findRoles(username) ;
        Set<String> permissions = t_userService.findPermissions(username) ;
        info.setRoles(roleName);
        info.setStringPermissions(permissions);
        return info;
    }

    /**
     * 首先执行这个登录验证
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        //获取用户账号
        String username = token.getPrincipal().toString() ;
        T_user user = t_userService.findUserByUsername(username) ;
        if (user != null){
            //将查询到的用户账号和密码存放到 authenticationInfo用于后面的权限判断。第三个参数传入realName。
            AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUserName(),user.getPassword(),
                    "a") ;
            return authenticationInfo ;
        }else{
            return  null ;
        }
    }
}

关键点就是需要继承AuthorizingRealm类之后覆写它的两个方法,doGetAuorizationInfo,doGetAuthentiationInfo,这两个方法都有注释,且逻辑也比较简单。

doGetAuthentiationInfo主要用于登陆验证,在登陆的时候需要将数据封装到Shiro的一个token中,执行Shiro的login()方法,之后只要我们将MyRealm这个类配置到Spring中,登陆的时候Shiro就会自动的调用doGetAuthentiationInfo()方法进行验证下面是登陆的Controller:

package com.crossoverJie.controller;

import com.crossoverJie.pojo.T_user;
import com.crossoverJie.service.T_userService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Resource;

/**
 * Created with IDEA
 * Created by ${jie.chen} on 2016/7/14.
 * 后台Controller
 */
@Controller
@RequestMapping("/")
public class T_userController {

    @Resource
    private T_userService t_userService ;

    @RequestMapping("/loginAdmin")
    public String login(T_user user, Model model){
        Subject subject = SecurityUtils.getSubject() ;
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(),user.getPassword()) ;
        try {
            subject.login(token);
            return "admin" ;
        }catch (Exception e){
            //这里将异常打印关闭是因为如果登录失败的话会自动抛异常
//            e.printStackTrace();
            model.addAttribute("error","用户名或密码错误") ;
            return "../../login" ;
        }
    }

    @RequestMapping("/admin")
    public String admin(){
        return "admin";
    }

    @RequestMapping("/student")
    public String student(){
        return "admin" ;
    }

    @RequestMapping("/teacher")
    public String teacher(){
        return "admin" ;
    }
}

主要就是login()方法,逻辑比较简单,只是在登陆验证的时候不是像之前那样直接查询数据库然后返回是否有用户,而是调用了Subject的login()方法,就是我们上面提到的,调用login()方法时,Shiro会自动调用我们自定义的MyRealm类中的doGetAuthentiationInfo方法进行验证的,验证逻辑是先根据用户名查询用户,如果查询到的话,再将查询到的用户名和密码放到SimpleAuthenticationInfo对象中,Shiro会自动根据用户输入的密码和查询到的密码进行匹配,如果匹配不上就会抛出异常,匹配之上后,就会执行doGetAuthorizationInfo()进行相应的权限验证。doGetAuthorizationInfo()的处理逻辑也比较简单,根据用户名获取到他所拥有的角色以及权限,然后赋值到SimpleAuthorizationInfo对象中即可,Shiro就会按照我们配置的XX角色对应XX权限来进行判断,这个配置在下面的整合中会讲到。

整合Spring

接下来应该是大家比较关系的一步:整合Spring

我是在之前的 Spring SpringMVC Mybatis的基础上进行整合的。

web.xml配置

首先我们需要在web.xml进行配置Shiro的过滤器。只贴Shiro部分的,其余的和之前配置是一样的。

<!-- shiro过滤器定义 -->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 -->
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

配置还是比较简单的,这样会过滤所有的请求。之后我们还需要在Spring中配置一个shiroFilter的bean。

Spring-mybatis.xml

由于这里配置较多,全部贴一下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context-3.1.xsd">
    <!-- 自动扫描 -->
    <context:component-scan base-package="com.crossoverJie" />
    <!-- 引入配置文件 -->
    <bean id="propertyConfigurer"
          class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:jdbc.properties" />
    </bean>

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <!-- 指定连接数据库的驱动 -->
        <property name="driverClassName" value="${jdbc.driverClass}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.user}" />
        <property name="password" value="${jdbc.password}" />

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="3" />
        <property name="minIdle" value="3" />
        <property name="maxActive" value="20" />

        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="60000" />

        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />

        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000" />

        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testWhileIdle" value="true" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />

        <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
                  value="20" />

        <!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 -->
        <property name="filters" value="stat" />
    </bean>

    <!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 自动扫描mapping.xml文件 -->
        <property name="mapperLocations" value="classpath:mapping/*.xml"></property>
    </bean>

    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.crossoverJie.dao" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
    </bean>

    <!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>


    <!-- 配置自定义Realm -->
    <bean id="myRealm" class="com.crossoverJie.shiro.MyRealm"/>

    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"/>
    </bean>

    <!-- Shiro过滤器 核心-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!-- Shiro的核心安全接口,这个属性是必须的 -->
        <property name="securityManager" ref="securityManager"/>
        <!-- 身份认证失败,则跳转到登录页面的配置 -->
        <property name="loginUrl" value="/login.jsp"/>
        <!-- 权限认证失败,则跳转到指定页面 -->
        <property name="unauthorizedUrl" value="/nopower.jsp"/>
        <!-- Shiro连接约束配置,即过滤链的定义 -->
        <property name="filterChainDefinitions">
            <value>
                <!--anon 表示匿名访问,不需要认证以及授权-->
                /loginAdmin=anon

                <!--authc表示需要认证 没有进行身份认证是不能进行访问的-->
                /admin*=authc


                /student=roles[teacher]
                /teacher=perms["user:create"]
            </value>
        </property>
    </bean>

    <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- 开启Shiro注解 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>
</beans>

在这里我们配置了上文中所提到的自定义myRealm,这样Shiro就可以按照我们自定义的逻辑来进行权限验证了。其余的都比较见到那,看注释应该都能明白。着重讲解一下:

<property name="filterChainDefinitions">
            <value>
                <!--anon 表示匿名访问,不需要认证以及授权-->
                /loginAdmin=anon

                <!--authc表示需要认证 没有进行身份认证是不能进行访问的-->
                /admin*=authc


                /student=roles[teacher]
                /teacher=perms["user:create"]
            </value>
        </property>
/loginAdmin=anon的意思的意思是,发起/loginAdmin这个请求是不需要进行身份认证的,这个请求在这次项目中是一个登录请求,一般对于这样的请求都是不需要身份认证的。/admin*=authc表示 /admin,/admin1,/admin2这样的请求都是需要进行身份认证的,不然是不能访问的。/student=roles[teacher]表示访问/student请求的用户必须是 teacher角色,不然是不能进行访问的。/teacher=perms["user:create"]表示访问/teacher请求是需要当前用户具有 user:create权限才能进行访问的。
使用Shiro标签库
Shiro还有着强大标签库,可以在前端帮我获取信息和判断。贴一下这里登陆完成之后显示的界面:
<%--
  Created by IntelliJ IDEA.
  User: Administrator
  Date: 2016/7/14
  Time: 13:17
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<html>
<head>
    <title>后台</title>
</head>
<body>
<shiro:hasRole name="admin">
    这是admin角色登录:<shiro:principal></shiro:principal>
</shiro:hasRole>

<shiro:hasPermission name="user:create">
    有user:create权限信息
</shiro:hasPermission>
<br>
登录成功
</body>
</html>

要想使用Shiro标签,只需要引入一下标签即可:

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

下面是一些标签的用法:

<shiro:hasRole name="admin">具有 admin角色才会显示标签内的信息。<shiro:principal></shiro:principal>获取用户信息。默认调用 Subject.getPrincipal()获取,即 Primary Principal。<shiro:hasPermission name="user:create"> 用户拥有 user:create这个权限才回显示标签内的信息。


这是我的测试数据。
首先来验证一下登录:

先输入一个错误的账号和密码:


接下来输入一个正确的:



可以看到我登录的用户是crossoverJie他是有admin的角色,并且拥有user:*(ps:系统数据详见上面的数据库截图)的权限,所以在这里:

<shiro:hasRole name="admin">   
    这是admin角色登录:<shiro:principal></shiro:principal>
</shiro:hasRole>
<shiro:hasPermission name="user:create">
    有user:create权限信息
</shiro:hasPermission>
是能显示出标签内的信息,并把用户信息也显示出来了。

接着我们来访问一下/student这个请求,因为在Spring的配置文件中:

<property name="filterChainDefinitions">
            <value>
                <!--anon 表示匿名访问,不需要认证以及授权-->
                /loginAdmin=anon

                <!--authc表示需要认证 没有进行身份认证是不能进行访问的-->
                /admin*=authc


                /student=roles[teacher]
                /teacher=perms["user:create"]
            </value>
        </property>

只有teacher角色才能访问/student这个请求的:


果然,Shiro做了安全控制是不能进行访问的。

然后我们换aaa用户登录,他正好是teacher角色,看能不能访问/student


果然是能访问的。

因为我在控制器里访问/student返回的是同一个界面所以看到的还是这个界面。

 @RequestMapping("/teacher")
    public String teacher(){
        return "admin" ;
    }

并且没有显示之前Shiro标签内的内容。

其他的我就不测了,大家可以自己在数据库里加一些数据,或者是改下拦截的权限多试试,这样对Shiro的理解就会更加深刻。

MD5

Shiro还封装了一个我认为非常不错的功能,那就是MD5加密,代码如下:

package com.crossoverJie.shiro;

import org.apache.shiro.crypto.hash.Md5Hash;

/**
 * Created with IDEA
 * 基于Shiro的MD5加密
 * Created by ${jie.chen} on 2016/7/13.
 */
public class MD5Util {

    public static String md5(String str,String salt){
        return new Md5Hash(str,salt).toString() ;
    }

    public static void main(String[] args) {
        String md5 = md5("abc123","crossoverjie") ;
        System.out.println(md5);
    }
}
代码非常简单,只需要调用 Md5Hash(str,salt)方法即可,这里多了一个参数,第一个参数不用多解释,是需要加密的字符串。第二个参数 salt中文翻译叫盐,加密的时候我们传一个字符串进去,只要这个salt不被泄露出去,那原则上加密之后是无法被解密的,在存用户密码的时候可以使用,感觉还是非常屌的。

以上就是Shiro实际使用的案例,将的比较初略,但是关于Shiro的核心东西都在里面了。大家可以去github上下载源码,只要按照我给的数据库就没有问题,项目跑起来之后试着改下里面的东西可以加深对Shiro的理解。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值