SpringBoot整合Shiro实现认证和授权实例

权限表设计

/*
Navicat MySQL Data Transfer

Source Server         : root
Source Server Version : 50642
Source Host           : localhost:3306
Source Database       : study

Target Server Type    : MYSQL
Target Server Version : 50642
File Encoding         : 65001

Date: 2019-02-27 11:37:01
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for customer
-- ----------------------------
DROP TABLE IF EXISTS `customer`;
CREATE TABLE `customer` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `customer_id` varchar(20) NOT NULL COMMENT '会员编号',
  `customer_name` varchar(20) NOT NULL COMMENT '会员名称',
  `password` varchar(32) DEFAULT NULL COMMENT '密码',
  `phone_number` varchar(11) DEFAULT NULL COMMENT '手机号码',
  `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
  `status` enum('1','2','3') NOT NULL DEFAULT '1' COMMENT '状态:1-正常;2-冻结;3-不可用',
  `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `customer_id` (`customer_id`),
  UNIQUE KEY `phone_number` (`phone_number`),
  UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='会员表';

-- ----------------------------
-- Records of customer
-- ----------------------------
INSERT INTO `customer` VALUES ('1', 'abc123456', 'abc', 'e10adc3949ba59abbe56e057f20f883e', '13412345678', 'abc@163.com', '1', '2019-02-21 10:16:35', '2019-02-21 10:16:35');
INSERT INTO `customer` VALUES ('2', 'asd123456', 'ssd', 'e10adc3949ba59abbe56e057f20f883e', '13412345679', 'asd@163.com', '1', '2019-02-21 10:16:35', '2019-02-21 10:16:35');
INSERT INTO `customer` VALUES ('3', 'qwe123456', 'qwe', 'e10adc3949ba59abbe56e057f20f883e', '13412345632', 'qwe@163.com', '1', '2019-02-21 10:16:35', '2019-02-21 10:16:35');
INSERT INTO `customer` VALUES ('4', 'zxc123456', 'zxc', 'e10adc3949ba59abbe56e057f20f883e', '13412345622', 'zxc@163.com', '1', '2019-02-21 10:16:35', '2019-02-21 10:16:35');

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `parent_id` int(11) DEFAULT NULL COMMENT '父级权限id',
  `name` varchar(20) NOT NULL COMMENT '权限名称',
  `permission` varchar(50) NOT NULL COMMENT '权限字符串,如employees:create,employees:update,employees:delete',
  `type` enum('0','1','2') NOT NULL DEFAULT '0' COMMENT '权限类型:0-目录;1-菜单;2-按钮',
  `url` varchar(100) DEFAULT NULL COMMENT '资源路径',
  `status` enum('0','1') NOT NULL DEFAULT '1' COMMENT '状态:0-不可用;1-可用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COMMENT='权限表';

-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES ('1', null, '会员管理', 'customer', '0', '/customer', '1');
INSERT INTO `sys_permission` VALUES ('2', '1', '会员列表', 'customer:list', '1', '/customer/list', '1');
INSERT INTO `sys_permission` VALUES ('3', '1', '会员等级', 'customer:level', '1', '/customer/level', '1');
INSERT INTO `sys_permission` VALUES ('4', '2', '查看会员', 'customer:view', '2', '/customer/view', '1');
INSERT INTO `sys_permission` VALUES ('5', '2', '添加会员', 'customer:add', '2', '/customer/add', '1');
INSERT INTO `sys_permission` VALUES ('6', '2', '修改会员', 'customer:update', '2', '/customer/update', '1');
INSERT INTO `sys_permission` VALUES ('7', '2', '删除会员', 'customer:delete', '2', '/customer/delete', '1');
INSERT INTO `sys_permission` VALUES ('8', null, '商品管理', 'product', '0', '/product', '1');
INSERT INTO `sys_permission` VALUES ('9', '8', '商品列表', 'product:list', '1', '/product/list', '1');
INSERT INTO `sys_permission` VALUES ('10', '8', '商品分类', 'product:category', '1', '/product/category', '1');
INSERT INTO `sys_permission` VALUES ('11', '8', '商品回收站', 'product:recycle', '1', '/product/recycle', '1');
INSERT INTO `sys_permission` VALUES ('12', '9', '添加商品', 'product:add', '2', '/product/add', '1');
INSERT INTO `sys_permission` VALUES ('13', '9', '修改商品', 'product:update', '2', '/product/update', '1');
INSERT INTO `sys_permission` VALUES ('14', '9', '删除商品', 'product:delete', '2', '/product/delete', '1');
INSERT INTO `sys_permission` VALUES ('15', null, '订单管理', 'order', '0', '/order', '1');
INSERT INTO `sys_permission` VALUES ('16', '15', '订单列表', 'order:list', '1', '/order/list', '1');
INSERT INTO `sys_permission` VALUES ('17', '16', '查看订单', 'order:view', '2', '/order/view', '1');
INSERT INTO `sys_permission` VALUES ('18', '16', '修改订单', 'order:update', '2', '/order/update', '1');
INSERT INTO `sys_permission` VALUES ('19', '16', '删除订单', 'order:delete', '2', '/order/delete', '1');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `role` varchar(20) NOT NULL COMMENT '角色名称',
  `description` varchar(255) DEFAULT NULL COMMENT '角色描述',
  `status` enum('0','1') NOT NULL DEFAULT '1' COMMENT '状态:0-不可用;1-可用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='角色表';

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'manager', '管理员', '1');
INSERT INTO `sys_role` VALUES ('2', 'producter', '生产员', '1');
INSERT INTO `sys_role` VALUES ('3', 'salesman', '销售员', '1');

-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
  `role_id` int(11) NOT NULL COMMENT '角色表主键',
  `permission_id` int(11) NOT NULL COMMENT '权限表主键'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限表';

-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES ('1', '1');
INSERT INTO `sys_role_permission` VALUES ('1', '2');
INSERT INTO `sys_role_permission` VALUES ('1', '3');
INSERT INTO `sys_role_permission` VALUES ('1', '4');
INSERT INTO `sys_role_permission` VALUES ('1', '5');
INSERT INTO `sys_role_permission` VALUES ('1', '6');
INSERT INTO `sys_role_permission` VALUES ('1', '7');
INSERT INTO `sys_role_permission` VALUES ('1', '8');
INSERT INTO `sys_role_permission` VALUES ('1', '9');
INSERT INTO `sys_role_permission` VALUES ('1', '10');
INSERT INTO `sys_role_permission` VALUES ('1', '11');
INSERT INTO `sys_role_permission` VALUES ('1', '12');
INSERT INTO `sys_role_permission` VALUES ('1', '13');
INSERT INTO `sys_role_permission` VALUES ('1', '14');
INSERT INTO `sys_role_permission` VALUES ('1', '15');
INSERT INTO `sys_role_permission` VALUES ('1', '16');
INSERT INTO `sys_role_permission` VALUES ('1', '17');
INSERT INTO `sys_role_permission` VALUES ('1', '18');
INSERT INTO `sys_role_permission` VALUES ('1', '19');
INSERT INTO `sys_role_permission` VALUES ('2', '8');
INSERT INTO `sys_role_permission` VALUES ('2', '9');
INSERT INTO `sys_role_permission` VALUES ('2', '10');
INSERT INTO `sys_role_permission` VALUES ('2', '11');
INSERT INTO `sys_role_permission` VALUES ('2', '12');
INSERT INTO `sys_role_permission` VALUES ('2', '13');
INSERT INTO `sys_role_permission` VALUES ('2', '14');
INSERT INTO `sys_role_permission` VALUES ('3', '15');
INSERT INTO `sys_role_permission` VALUES ('3', '16');
INSERT INTO `sys_role_permission` VALUES ('3', '17');
INSERT INTO `sys_role_permission` VALUES ('3', '18');
INSERT INTO `sys_role_permission` VALUES ('3', '19');

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(20) NOT NULL COMMENT '用户名',
  `password` varchar(32) NOT NULL COMMENT '密码',
  `salt` varchar(32) DEFAULT NULL COMMENT '加密盐值',
  `status` enum('1','2','3') NOT NULL DEFAULT '1' COMMENT '状态:1-正常;2-冻结;3-不可用',
  `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='用户表';

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', 'f6a93fb80dccf33119610712eb41a3cb', 'admin#123456', '1', '2019-02-19 03:02:27', '2019-02-20 17:56:56');
INSERT INTO `sys_user` VALUES ('2', 'product', '47d7920d1af0a48c3ad9a4897009bd5c', 'product#123456', '1', '2019-02-19 03:02:27', '2019-02-20 18:00:25');
INSERT INTO `sys_user` VALUES ('3', 'sale', '866ff773736059bbbd48e89676712c59', 'sale#123456', '1', '2019-02-20 17:59:49', '2019-02-20 17:59:49');

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
  `user_id` int(11) NOT NULL COMMENT '用户表主键',
  `role_id` int(11) NOT NULL COMMENT '角色表主键'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色表';

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '2');
INSERT INTO `sys_user_role` VALUES ('3', '3');

导入依赖

<!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

创建实体类、Mapper接口、Service类

这里我使用的是

mybatis-plus逆向生成工具,具体可以看源码

创建自定义安全数据源Realm

package com.lx.springboot.config;

import com.lx.springboot.entity.SysPermission;
import com.lx.springboot.entity.SysRole;
import com.lx.springboot.entity.SysUser;
import com.lx.springboot.service.SysPermissionService;
import com.lx.springboot.service.SysRoleService;
import com.lx.springboot.service.SysUserService;
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 org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @ClassName ShiroRealm
 * @Description 自定义安全数据源
 */
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysPermissionService sysPermissionService;

    /**
     * 授权
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //获取已经认证通过的用户信息
        SysUser sysUser = (SysUser) principals.getPrimaryPrincipal();
        //根据用户id查询角色信息
        List<SysRole> sysRoles = sysRoleService.getSysRoleByUserId(sysUser.getId());
        //函数式编程+lambda表达式
        Set<String> roles = sysRoles.stream().map(SysRole::getRole).collect(Collectors.toSet());
        authorizationInfo.setRoles(roles);
        //根据用户id查询权限信息
        List<SysPermission> sysPermissions = sysPermissionService.getSysPermissionByUserId(sysUser.getId());
        Set<String> permissions = sysPermissions.stream().map(SysPermission::getPermission).collect(Collectors.toSet());
        authorizationInfo.setStringPermissions(permissions);
        return authorizationInfo;
    }

    /**
     * 认证
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取用户名
        String username = (String) token.getPrincipal();
        //根据用户名查询用户信息
        SysUser sysUser = sysUserService.getSysUserByUsername(username);
        //判断用户是否存在
        if (Objects.isNull(sysUser)) {
            throw new UnknownAccountException();
        }
        //判断用户状态是否正常
        if (Objects.equals("2", sysUser.getStatus()) || Objects.equals("3", sysUser.getStatus())) {
            throw new DisabledAccountException();
        }
        return new SimpleAuthenticationInfo(sysUser, sysUser.getPassword(), ByteSource.Util.bytes(sysUser.getSalt()), getName());
    }
}

创建SpringBoot整合Shiro配置类

package com.lx.springbootshiro.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Author: 水越帆
 * QQ:1548353431
 */
@Configuration
public class ShiroConfig {
    /**
     * 配置拦截器
     *
     * 定义拦截URL权限,优先级从上到下
     * 1). anon : 匿名访问,无需登录
     * 2). authc : 登录后才能访问
     * 3). logout: 登出
     * 4). roles : 角色过滤器
     *
     * URL 匹配风格
     * 1). ?:匹配一个字符,如 /admin? 将匹配 /admin1,但不匹配 /admin 或 /admin/;
     * 2). *:匹配零个或多个字符串,如 /admin* 将匹配 /admin 或/admin123,但不匹配 /admin/1;
     * 2). **:匹配路径中的零个或多个路径,如 /admin/** 将匹配 /admin/a 或 /admin/a/b
     *
     * 配置身份验证成功,失败的跳转路径
     *
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shirFilter(DefaultWebSecurityManager securityManager)
    {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        filterChainDefinitionMap.put("/static/**", "anon"); // 静态资源匿名访问
        filterChainDefinitionMap.put("/bootstrap/**", "anon"); // 静态资源匿名访问
        filterChainDefinitionMap.put("/css/**", "anon"); // 静态资源匿名访问
        filterChainDefinitionMap.put("/js/**", "anon"); // 静态资源匿名访问
        filterChainDefinitionMap.put("/img/**", "anon"); // 静态资源匿名访问
        filterChainDefinitionMap.put("/user/login", "anon");// 登录匿名访问
        filterChainDefinitionMap.put("/user/doLogin", "anon");// 登录匿名访问
        filterChainDefinitionMap.put("/user/logout", "logout"); // 用户退出,只需配置logout即可实现该功能
        filterChainDefinitionMap.put("/**", "authc"); // 其他路径均需要身份认证,一般位于最下面,优先级最低
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        shiroFilterFactoryBean.setLoginUrl("/user/login"); // 登录的路径
        shiroFilterFactoryBean.setSuccessUrl("/index"); // 登录成功后跳转的路径
        shiroFilterFactoryBean.setUnauthorizedUrl("/403"); // 验证失败后跳转的路径
        return shiroFilterFactoryBean;
    }

    /**
     * SecurityManager 安全管理器;Shiro的核心
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

    /**
     * 自定义Realm,可以多个
     */
    @Bean
    public ShiroRealm shiroRealm() {
        ShiroRealm shiroRealm = new ShiroRealm();
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroRealm;
    }

    /**
     * 配置Shiro生命周期处理器
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 自动创建代理类,若不添加,Shiro的注解可能不会生效。
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new
                DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 开启Shiro的注解
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new
                AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 凭证匹配器
     *
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 )
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new
                HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    /**
     * Shiro方言,支持Thymeleaf中使用shiro标签
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
}

登录认证

package com.lx.springboot.controller;


import com.lx.springboot.entity.SysUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpSession;

/**
 * <p>
 * 用户表 前端控制器
 * </p>
 */
@Controller
public class SysUserController {

    /**
     * 跳转到登陆页面
     *
     * @return
     */
    @RequestMapping({"", "/user/login"})
    public String login(){
        return "login";
    }

    /**
     * 登陆
     *
     * @param user
     * @return
     */
    @PostMapping("/user/doLogin")
    public String doLogin(SysUser user, HttpSession session){
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        SecurityUtils.getSubject().login(token);
        //用户名保存到session中
        session.setAttribute("username", user.getUsername());
        return "redirect:/index";
    }

    /**
     * 退出
     *
     * @return
     */
    @RequestMapping("/user/logout")
    public String logout(){
        SecurityUtils.getSubject().logout();
        return "redirect:/user/login";
    }
}

使用Shiro注解授权

@Controller
@RequestMapping("/users")
 public class UserController {
 
    @GetMapping("")
    @RequiresPermissions({ "user" })
    public String userList() {
        return "/user/user";    
 } }

在Thymeleaf中使用Shiro标签

<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

 使用shiro标签

<span shiro:authenticated="true" >      <span>欢迎您:<span th:text="${userInfo.realName}"></span></span> </span>

权限验证失败统一处理

@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理认证失败异常
     *
     * @param e
     * @param model
     * @return
     */
    @ExceptionHandler(AuthenticationException.class)
    public String authenticationExceptionProcessing(AuthenticationException e, Model model) {
        String msg = null;
        if (e instanceof DisabledAccountException) {
            msg = "账户异常";
        } else if (e instanceof IncorrectCredentialsException) {
            msg = "账户/密码错误";
        } else {
            msg = "系统发生异常";
        }
        model.addAttribute("errorMsg", msg);
        return "forward:/user/login";
    }

    /**
     * 处理授权失败异常
     *
     * @return
     */
    @ExceptionHandler(UnauthorizedException.class)
    public String authorizedExceptionProcessing() {
        return "error/unauthorizedException";
    }
}

总结 
Shiro 四个核心功能:身份认证,授权,数据加密,Seesion管理。 Shiro 三个重要角色:Subject,SecurityManager,Realm。

github源码:https://github.com/SHUIYUEFAN/springboot-shiro

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值