shiro多种登陆方式的设置

shiro是我目前用的项目的一个鉴权框架,也是apache基金会的,用来处理登录,鉴权这部分。下面是官网的介绍页:

Shro 官网网站,不管是了解还是学习,都是第一手资料

网上一个用XML文件配置的,这里我用注解的方式来配置跟他做一个对比

一般来说对于登录,鉴权,身份验证,session管理其实不管什么项目都是一个不容忽视的部分。采用shiro就是希望有个对用的框架来直接使用,而不是自己去修改,很多时候,项目紧急容不得你“慢悠悠”的造轮子。而且shiro的使用还算方便,这里主要是讨论shiro如何去设置多种方式登录。

一 问题:登录可以支持多种方式登录,账户密码,手机验证码,或者微信钉钉的自动登录?
这其实是一个很现实的问题,尤其是现在很多平台,开放权限可以通过大平台自身的鉴权验证,来让用户用第三方账号来登录使用。

shiro支持多种方式,除了验证方式不同,比如有的是微信扫码,微信服务器返回验证信息的第三方登录,有的是自己数据库的用户密码验证。甚至可以来源不同的数据库,只要你能访问这个数据库信息即可。

二 实操:如何设置多种方式?
(1)导入依赖shiro
首先你要把shiro框架,弄到你项目来进行配置maven项目依赖(gradle什么的都可以,就是对应的文件和写法有区别),由shiro来负责执行身份验证,授权,加密和会话管理。
首先我从网上看到一个配置代码,贴了过来:

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

<!-- spring整合shiro  -->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.3.2</version>
</dependency>


<!-- ehcache-->
<dependency>
  <groupId>net.sf.ehcache</groupId>
  <artifactId>ehcache-core</artifactId>
  <version>2.6.11</version>
</dependency>

我们自己的项目,采用的是一个shiro的开源插件,由于我们自己包装了一层,可能没什么推荐价值,也发一下

 <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>${shiro.redis.version}</version>
        </dependency>

(2)shiro拦截配置
这里我看有的地方可能还要配置web.xml,这里我就跳过这一步,说一下shiro的配置,这时候先拿网友配置文件的方式
XML配置文件

1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
 5 
 6 
 7 <!-- 配置ehCache缓存支持-->
 8 <bean name="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
 9 <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"></property>
10 </bean>
11 
12 
13     <!-- 自定义Realm-->
14 <bean name="userRealm" class="com.newer.web.shiro.MyRealm" >
15 
16     <!--注入加密算法类 -->
17     <property name="credentialsMatcher">
18 
19     <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
20         <!--加密算法名 -->
21         <property name="hashAlgorithmName" value="MD5"/>
22         <!--加密次数 -->
23         <property name="hashIterations" value="6"/>
24     </bean>
25 
26     </property>
27 
28 
29 </bean>
30 
31 
32 <!-- shiro安全管理器-->
33 <bean name="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
34 <property name="cacheManager" ref="cacheManager"></property>
35 <property name="realm" ref="userRealm"></property>
36 </bean>
37 
38 
39 <!-- 管理shiro bean的生命周期 -->
40 <bean name="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean>
41 
42 
47 
48     <!--shiro核心过滤器配置-->
49     <bean name="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
50         <property name="securityManager" ref="securityManager"/>
51         <property name="loginUrl" value="/login.jsp"></property> <!-- 没有认证  跳转的登录连接 -->
52         <property name="unauthorizedUrl" value="/unauthorized.jsp"></property> <!-- 没有访问权限 跳转的页面 -->
82         <!--这里使用 从数据库查询出来要拦截的url集合 注入到这个对象中 实现拦截 -->
83         <property name="filterChainDefinitionMap" ref="map"/>
86     </bean>
87 
88 
89    <!--实例工厂方法  将执行shiroFilterUtils类的build的方法 -->
90   <bean name="map" factory-bean="shiroFilterUtils" factory-method="build"/>
91 
92     <!--这个类将从数据库中查询要拦截的url信息-->
93     <bean name="shiroFilterUtils" class="com.newer.util.ShiroFilterUtils"/>
97 </beans>

然后我用注解的方式来完成的配置来对比(因为缓存组件彼此都不同,他用的是ehcache,我用的是redis之类的一些细节不同,但是大体思路上主要的部分都是相同的)

package com.lxzl.ams.app.config.shiro.common;

import com.lxzl.ams.app.config.shiro.account.AccountShiroRealm;
import com.lxzl.ams.app.config.shiro.email.EmailShiroRealm;
import com.lxzl.ams.app.config.shiro.filter.ShiroLoginFilter;
import com.lxzl.ams.app.config.shiro.phone.DingTaskShiroRealm;
import com.lxzl.ams.app.config.shiro.phone.FeiShuWorkShiroRealm;
import com.lxzl.ams.app.config.shiro.phone.PhoneShiroRealm;
import com.lxzl.ams.app.config.shiro.phone.WeChatWorkShiroRealm;
import com.lxzl.ams.app.config.shiro.session.OnlineWebSessionManager;
import com.lxzl.ams.app.config.shiro.session.SpringSessionValidationScheduler;
import com.lxzl.ams.common.config.RedisConfig;
import com.lxzl.ams.common.core.CommonConstants;
import com.lxzl.skull.util.StringUtil;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.*;

/**
 * Shiro 配置类
 */
@Configuration
class ShiroConfiguration {

    @Value("${server.servlet.session.timeout}")
    private Long sessionTimeOut;

    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.password}")
    private String password;

    @Autowired
    private RedisConfig redisConfig;

    @Value("${server.servlet.session.timeout}")
    private Long sessionTimeout;

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);


    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在
     * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
     * Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
     * <p>
     * 部分过滤器可指定参数,如perms,roles
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {


        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();//获取filters
        //将自定义 的权限验证失败的过滤器ShiroFilterFactoryBean注入shiroFilter
        //filters.put("perms", new ShiroPermissionsFilter());

//        filters.put("authc", new AjaxPermissionsAuthorizationFilter());
        filters.put("authc", new ShiroLoginFilter());

        // 必须设置SecuritManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");

        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/ams/403");

        // 权限控制map.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
//        filterChainDefinitionMap.put("/user/logout", "anon");

        filterChainDefinitionMap.put("/ams/403", "anon");
        filterChainDefinitionMap.put("/ams/login", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/user/logout", "anon");
        filterChainDefinitionMap.put("/ws/**", "anon");

        //允许访问静态资源a
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/favicon.ico**", "anon");
        filterChainDefinitionMap.put("/kaptcha/getKaptchaImage**", "anon");
        //允许访问健康检查
        filterChainDefinitionMap.put("/zabbix/service/status/monitor", "anon");
        // 从数据库获取所有的权限
       /*  Map<String, String> map = sysMenuSupport.selectPermsAll();
         filterChainDefinitionMap.putAll(map);*/
        //filterChainDefinitionMap.putAll(SpringUtils.getBean(SysMenuService.class).selectPermsAll());

        /*  if(!CollectionUtil.isEmpty(map)){
            filterChainDefinitionMap.putAll(map);
        }*/

        //current_Permission?.getUrl(),"perms[" + current_Permission?.getPermission() + "]"
        //filterChainDefinitionMap.put("manage-system/admin","perms[user:pageUser]");
    // 允许访问健康检查
        filterChainDefinitionMap.put("/zabbix/service/status/monitor", "anon");

//        filterChainDefinitionMap.put("/user/**","roles[45,46]");

        //过滤链定义,从上向下顺序执行,一般将放在最为下边
        filterChainDefinitionMap.put("/**", "authc");
        //authc表示需要验证身份才能访问,还有一些比如anon表示不需要验证身份就能访问等。
        logger.info("拦截器链:" + filterChainDefinitionMap);

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    //SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        ThreadContext.bind(securityManager);
        //设置realm.
        securityManager.setAuthenticator(modularRealmAuthenticator());
        List<Realm> realms = new ArrayList<>();
        //添加多个Realm
        realms.add(accountShiroRealm());
        realms.add(phoneShiroRealm());
        realms.add(emailShiroRealm());
        realms.add(weChatWorkShiroRealm());
        realms.add(feiShuWorkShiroRealm());
        realms.add(dingTaskShiroRealm());
        securityManager.setRealms(realms);

        //注入记住我管理器;
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }




    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisManager redisManager() {
        logger.info("创建shiro redisManager,连接Redis..URL= " + redisConfig.redisStandaloneConfiguration().getHostName() + ":" + redisConfig.redisStandaloneConfiguration().getPort());
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisConfig.redisStandaloneConfiguration().getHostName());
        redisManager.setPort(redisConfig.redisStandaloneConfiguration().getPort());
//        redisManager.setExpire(1800);// 配置缓存过期时间
        redisManager.setTimeout(timeout);
        if (StringUtil.isNotBlank(password)) {
            redisManager.setPassword(password);
        }
        return redisManager;
    }

    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     * @return
     */
//    public RedisCacheManager cacheManager() {
//        logger.info("创建RedisCacheManager...");
//        RedisCacheManager redisCacheManager = new RedisCacheManager();
//        redisCacheManager.setRedisManager(redisManager());
//        return redisCacheManager;
//    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setKeyPrefix(CommonConstants.APP_SHIRO_SESSION);
        redisSessionDAO.setExpire((int) (sessionTimeOut / 1000));
        return redisSessionDAO;
    }

    /**
     * Session Manager
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public OnlineWebSessionManager sessionManager() {
        OnlineWebSessionManager sessionManager = new OnlineWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        // 设置全局session超时时间
        sessionManager.setGlobalSessionTimeout(sessionTimeOut);
        // 相隔多久检查一次session的有效性
        sessionManager.setSessionValidationInterval(sessionTimeOut);
        // 删除过期的session
        sessionManager.setDeleteInvalidSessions(true);
        // 是否定时检查session
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdCookieEnabled(true);
        SimpleCookie cookie = new SimpleCookie(UUID.randomUUID().toString());
        cookie.setHttpOnly(true);
        cookie.setMaxAge(60 * 60 * 1000);
        sessionManager.setSessionIdCookie(cookie);

        return sessionManager;
    }

    /**
     * cookie对象;
     *
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie() {
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //<!-- 记住我cookie生效时间30天 ,单位秒;-->
        simpleCookie.setMaxAge(2592000);
        return simpleCookie;
    }

    /**
     * cookie管理对象;记住我功能
     *
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }

    /**
     * 系统自带的Realm管理,主要针对多realm
     */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator() {
        //自己重写的ModularRealmAuthenticator
        UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthenticator;
    }

    @Bean
    public AccountShiroRealm accountShiroRealm() {
        AccountShiroRealm accountShiroRealm = new AccountShiroRealm();
        accountShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());//设置解密规则
        return accountShiroRealm;
    }

    @Bean
    public PhoneShiroRealm phoneShiroRealm() {
        PhoneShiroRealm phoneShiroRealm = new PhoneShiroRealm();
        //phoneShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());//设置解密规则
        return phoneShiroRealm;
    }

    @Bean
    public WeChatWorkShiroRealm weChatWorkShiroRealm() {
        WeChatWorkShiroRealm weChatWorkShiroRealm = new WeChatWorkShiroRealm();
        weChatWorkShiroRealm.setCredentialsMatcher(allowAllCredentialsMatcher());
        return weChatWorkShiroRealm;
    }
    @Bean
    public FeiShuWorkShiroRealm feiShuWorkShiroRealm() {
        FeiShuWorkShiroRealm feiShuWorkShiroRealm = new FeiShuWorkShiroRealm();
        feiShuWorkShiroRealm.setCredentialsMatcher(allowAllCredentialsMatcher());
        return feiShuWorkShiroRealm;
    }

    @Bean
    public DingTaskShiroRealm dingTaskShiroRealm() {
        DingTaskShiroRealm dingTaskShiroRealm = new DingTaskShiroRealm();
        dingTaskShiroRealm.setCredentialsMatcher(allowAllCredentialsMatcher());
        return dingTaskShiroRealm;
    }

    @Bean
    public EmailShiroRealm emailShiroRealm() {
        EmailShiroRealm emailShiroRealm = new EmailShiroRealm();
        return emailShiroRealm;
    }

    //因为我们的密码是加过密的,所以,如果要Shiro验证用户身份的话,需要告诉它我们用的是md5加密的,并且是加密了两次。同时我们在自己的Realm中也通过SimpleAuthenticationInfo返回了加密时使用的盐。这样Shiro就能顺利的解密密码并验证用户名和密码是否正确了。
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");//散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
        return hashedCredentialsMatcher;
    }

    @Bean
    public AllowAllCredentialsMatcher allowAllCredentialsMatcher() {
        AllowAllCredentialsMatcher allowAllCredentialsMatcher = new AllowAllCredentialsMatcher();
        return allowAllCredentialsMatcher;
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     * 开启 权限注解
     * Controller才能使用@RequiresPermissions
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 自定义sessionFactory调度器
     */
    @Bean
    public SpringSessionValidationScheduler sessionValidationScheduler() {
        SpringSessionValidationScheduler sessionValidationScheduler = new SpringSessionValidationScheduler();
        // 设置会话验证调度器进行会话验证时的会话管理器

        sessionValidationScheduler.setSessionManager(sessionManager());
        return sessionValidationScheduler;
    }

}


实际上从这里大家也可以看出,注解和配置文件差不多,你配置文件里面配置一个bean和我写一个@Bean注解其实是相同的

(3)配置多种登录方式
实际上你细心看我的配置类,你会发现有

       //添加多个Realm
        realms.add(accountShiroRealm());
        realms.add(phoneShiroRealm());
        realms.add(emailShiroRealm());
        realms.add(weChatWorkShiroRealm());
        realms.add(feiShuWorkShiroRealm());
        realms.add(dingTaskShiroRealm());
        securityManager.setRealms(realms);

其实这些realm就是,shiro里面所说的“域”,也就是多种方式,shiro允许设置多个realm验证,一个不行就下一个,这几个配置的realm又成功的,那就登陆成功。在上面配置文件,我就add了不少realm,每个realm对应一个具体的验证业务逻辑。

然后我们就看一下realm我只怎么写的

package com.lxzl.ams.app.config.shiro.phone;

import com.lxzl.ams.app.config.shiro.common.UserToken;
import com.lxzl.ams.common.core.option.LoginType;
import com.lxzl.ams.common.dto.user.request.LoginPhoneParam;
import com.lxzl.ams.common.dto.user.response.UserLoginResponse;
import com.lxzl.ams.service.sysMenu.support.SysMenuSupport;
import com.lxzl.ams.service.user.UserService;
import com.lxzl.skull.exception.BusinessException;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

public class PhoneShiroRealm extends AuthorizingRealm {
    private static final Logger logger = LoggerFactory.getLogger(PhoneShiroRealm.class);
    @Override
    public String getName() {
        return LoginType.PHONE.toString();
    }
    @Autowired
    private UserService userService;

    @Autowired
    private SysMenuSupport sysMenuSupport;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        logger.info("开始手机号短信身份认证..");
        UserToken userToken = (UserToken) token;
        String phone = userToken.getUsername();
        LoginPhoneParam loginPhoneParam = new LoginPhoneParam();
        String verifyCode = "";
        if (((UserToken) token).getPassword() != null) {
            verifyCode = new String(((UserToken) token).getPassword());
        }
        loginPhoneParam.setPhone(phone);
        loginPhoneParam.setVerifyCode(verifyCode);
        UserLoginResponse userLoginResponse = null;

//这里就是自己定义的具体验证逻辑,这里是验证手机验证码的service,你可以根据情况用不同的业务验证逻辑来完成
        try {
            userLoginResponse = (UserLoginResponse) userService.quickLoginPhone(loginPhoneParam, userToken.getIp()).getData();
        } catch (BusinessException e) {
            e.printStackTrace();
        }


        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userLoginResponse, verifyCode, getName());
        logger.info("返回手机号短信认证信息:" + authenticationInfo);
        return authenticationInfo;
    }

    //当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("开始手机号短信权限授权");
        return sysMenuSupport.checkPermission(principals);
    }
}

这样从第二步的配置,到第三部具体一个“域”realm的编码,你差不多对于如何配置多个realm组成的shiro使用了,当然可有很多遗漏,你可以通过文档和其他人的文章进行对比学习。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值