Apache Shiro之-------------从入门到入土!!!(教程)

1 篇文章 0 订阅

什么是shiro???
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

主要功能
三个核心组件:Subject, SecurityManager 和 Realms.
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
  Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
  SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
  Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
  从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
  Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。

好了,现在大致了解了什么是shiro,现在我进一步取了解
1:Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
上源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//



public interface Subject {
    Object getPrincipal();

    PrincipalCollection getPrincipals();

    boolean isPermitted(String var1);

    boolean isPermitted(Permission var1);

    boolean[] isPermitted(String... var1);

    boolean[] isPermitted(List<Permission> var1);

    boolean isPermittedAll(String... var1);

    boolean isPermittedAll(Collection<Permission> var1);

    void checkPermission(String var1) throws AuthorizationException;

    void checkPermission(Permission var1) throws AuthorizationException;

    void checkPermissions(String... var1) throws AuthorizationException;

    void checkPermissions(Collection<Permission> var1) throws AuthorizationException;

    boolean hasRole(String var1);

    boolean[] hasRoles(List<String> var1);

    boolean hasAllRoles(Collection<String> var1);

    void checkRole(String var1) throws AuthorizationException;

    void checkRoles(Collection<String> var1) throws AuthorizationException;

    void checkRoles(String... var1) throws AuthorizationException;

    void login(AuthenticationToken var1) throws AuthenticationException;

    boolean isAuthenticated();

    boolean isRemembered();

    Session getSession();

    Session getSession(boolean var1);

    void logout();

    <V> V execute(Callable<V> var1) throws ExecutionException;

    void execute(Runnable var1);

    <V> Callable<V> associateWith(Callable<V> var1);

    Runnable associateWith(Runnable var1);

    void runAs(PrincipalCollection var1) throws NullPointerException, IllegalStateException;

    boolean isRunAs();

    PrincipalCollection getPreviousPrincipals();

    PrincipalCollection releaseRunAs();

    public static class Builder {
        private final SubjectContext subjectContext;
        private final SecurityManager securityManager;

        public Builder() {
            this(SecurityUtils.getSecurityManager());
        }

        public Builder(SecurityManager securityManager) {
            if (securityManager == null) {
                throw new NullPointerException("SecurityManager method argument cannot be null.");
            } else {
                this.securityManager = securityManager;
                this.subjectContext = this.newSubjectContextInstance();
                if (this.subjectContext == null) {
                    throw new IllegalStateException("Subject instance returned from 'newSubjectContextInstance' cannot be null.");
                } else {
                    this.subjectContext.setSecurityManager(securityManager);
                }
            }
        }

        protected SubjectContext newSubjectContextInstance() {
            return new DefaultSubjectContext();
        }

        protected SubjectContext getSubjectContext() {
            return this.subjectContext;
        }

        public Subject.Builder sessionId(Serializable sessionId) {
            if (sessionId != null) {
                this.subjectContext.setSessionId(sessionId);
            }

            return this;
        }

        public Subject.Builder host(String host) {
            if (StringUtils.hasText(host)) {
                this.subjectContext.setHost(host);
            }

            return this;
        }

        public Subject.Builder session(Session session) {
            if (session != null) {
                this.subjectContext.setSession(session);
            }

            return this;
        }

        public Subject.Builder principals(PrincipalCollection principals) {
            if (principals != null && !principals.isEmpty()) {
                this.subjectContext.setPrincipals(principals);
            }

            return this;
        }

        public Subject.Builder sessionCreationEnabled(boolean enabled) {
            this.subjectContext.setSessionCreationEnabled(enabled);
            return this;
        }

        public Subject.Builder authenticated(boolean authenticated) {
            this.subjectContext.setAuthenticated(authenticated);
            return this;
        }

        public Subject.Builder contextAttribute(String attributeKey, Object attributeValue) {
            if (attributeKey == null) {
                String msg = "Subject context map key cannot be null.";
                throw new IllegalArgumentException(msg);
            } else {
                if (attributeValue == null) {
                    this.subjectContext.remove(attributeKey);
                } else {
                    this.subjectContext.put(attributeKey, attributeValue);
                }

                return this;
            }
        }

        public Subject buildSubject() {
            return this.securityManager.createSubject(this.subjectContext);
        }
    }
}

这里面包含了“当前用户”的所有信息,包括role,session,Permission,
上工具类 ShiroUtils



/**
 * shiro 工具类
 *
 * @author lp
 */
@Component
public class ShiroUtils {
    private static final SysUserConfigService userService= SpringUtils.getBean(SysUserConfigService.class);

    private ShiroUtils(){}

    /**
     * 获取shiro subject
     * @return
     * @author lp
     * @Date 2019年11月21日 上午10:00:55
     */
    public static Subject getSubjct()
    {
        return SecurityUtils.getSubject();
    }

    /**
     * 获取登录session
     * @return
     * @author lp
     * @Date 2019年11月21日 上午10:00:41
     */
    public static Session getSession()
    {
        return SecurityUtils.getSubject().getSession();
    }

    /**
     * 退出登录
     * @author lp
     * @Date 2019年11月21日 上午10:00:24
     */
    public static void logout()
    {
        getSubjct().logout();
    }

    /**
     * 获取登录用户model
     * @return
     * @author lp
     * @Date 2019年11月21日 上午10:00:10
     */
    public static TsysUser getUser()
    {

    	TsysUser user = null;
        Object obj = getSubjct().getPrincipal();
        if (StringUtils.isNotNull(obj))
        {
            user = new TsysUser();
            BeanUtils.copyBeanProp(user, obj);
        }
        if(user ==null ) return null;
        //添加用户会员信息
        user.setSysUserConfig(userService.selectByUserId(user.getId()));
        return user;
    }

    /**
     * set用户
     * @param user
     * @author lp
     * @Date 2019年11月21日 上午9:59:52
     */
    public static void setUser(TsysUser user)
    {
        Subject subject = getSubjct();
        PrincipalCollection principalCollection = subject.getPrincipals();
        String realmName = principalCollection.getRealmNames().iterator().next();
        PrincipalCollection newPrincipalCollection = new SimplePrincipalCollection(user, realmName);
        // 重新加载Principal
        subject.runAs(newPrincipalCollection);
    }

    /**
     * 清除授权信息
     * @author lp
     * @Date 2019年11月21日 上午9:59:37
     */
    public static void clearCachedAuthorizationInfo()
    {
        RealmSecurityManager rsm = (RealmSecurityManager) SecurityUtils.getSecurityManager();
        MyShiroRealm realm = (MyShiroRealm) rsm.getRealms().iterator().next();
        realm.clearCachedAuthorizationInfo();
    }

    /**
     * 获取登录用户id
     * @return
     * @author lp
     * @Date 2019年11月21日 上午9:58:55
     */
    public static String getUserId()
    {
        TsysUser tsysUser = getUser();
        if (tsysUser == null || tsysUser.getId() == null){
            throw new RuntimeException("用户不存在!");
        }
        return tsysUser.getId().trim();
    }

    /**
     * 获取登录用户name
     * @return
     * @author lp
     * @Date 2019年11月21日 上午9:58:48
     */
    public static String getLoginName()
    {
        TsysUser tsysUser = getUser();
        if (tsysUser == null){
            throw new RuntimeException("用户不存在!");
        }
        return tsysUser.getUsername();
    }

    /**
     * 获取登录用户ip
     * @return
     * @author lp
     * @Date 2019年11月21日 上午9:58:26
     */
    public static String getIp()
    {
        return getSubjct().getSession().getHost();
    }

    /**
     * 获取登录用户sessionid
     * @return
     * @author lp
     * @Date 2019年11月21日 上午9:58:37
     */
    public static String getSessionId()
    {
        return String.valueOf(getSubjct().getSession().getId());
    }
}

这个工具类基本包含了正常开发使用所需要的东西
注意这段代码,在Subject中获取用户信息

    private static final SysUserConfigService userService= SpringUtils.getBean(SysUserConfigService.class); //如何再非spring环境使用spring,这样我们就可以使用spring 中定义的service

/**
     * 获取登录用户model
     * @return
     * @author lp
     * @Date 2019年11月21日 上午10:00:10
     */
    public static TsysUser getUser()
    {

    	TsysUser user = null;
        Object obj = getSubjct().getPrincipal();
        if (StringUtils.isNotNull(obj))
        {
            user = new TsysUser();
            BeanUtils.copyBeanProp(user, obj);
        }
        if(user ==null ) return null;
        //添加用户会员信息
        user.setSysUserConfig(userService.selectByUserId(user.getId()));
        //在返回model 中添加用户配置信息,也可以在Realm 中添加
        return user;
        //系统中获取当前登录用户,直接shiroUtiles.getUser()
    }

SecurityManager 它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
//实现了 Authenticator类,Authorizer,SessionManager  SecurityManager 的核心所在
    Subject login(Subject var1, AuthenticationToken var2) throws AuthenticationException;

    void logout(Subject var1);

    Subject createSubject(SubjectContext var1);
}

Realm Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
如何封装合适自己的Realm,上代码,教大家配置一个属于自己的Realm
MyShiroRealm



/**
 * 身份校验核心类
 *
 * @ClassName: MyShiroRealm
 * @author lp
 * @date 2019年8月25日
 *
 */
@Service
public class MyShiroRealm extends AuthorizingRealm {

	@Autowired
	private TsysUserDao tsysUserDao;

	@Autowired
	private PermissionDao permissionDao;//权限dao

	@Autowired
	private RoleDao roleDao ;//角色dao


	/**
	 * 认证登陆
	 */
	@SuppressWarnings("unused")
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken token) throws AuthenticationException {

		 //加这一步的目的是在Post请求的时候会先进认证,然后在到请求
        if (token.getPrincipal() == null) {
            return null;
        }
		String username = (String) token.getPrincipal();
		String password = new String((char[]) token.getCredentials());
		// 通过username从数据库中查找 User对象,如果找到,没找到.
		// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
		TsysUser userInfo = tsysUserDao.queryUserName(username);
		if (userInfo == null)
			return null;
		else{
			SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
					userInfo, // 用户名
					userInfo.getPassword(), // 密码
					getName() // realm name
			);
			return authenticationInfo;
		}

	}

	 /**
     * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用.
     */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		//System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
		if(principals == null){
	       throw new AuthorizationException("principals should not be null");
	    }
		SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
		TsysUser userinfo  = (TsysUser)principals.getPrimaryPrincipal();
		String uid=userinfo.getId();
		List<TsysRole> tsysRoles= roleDao.queryUserRole(uid);
		for(TsysRole userrole:tsysRoles){
			//System.out.println("角色名字:"+gson.toJson(userrole));
			String rolid=userrole.getId();//角色id
			authorizationInfo.addRole(userrole.getName());//添加角色名字
			List<TsysPermission> permissions=permissionDao.queryRoleId(rolid);
			for(TsysPermission p:permissions){
				//System.out.println("角色下面的权限:"+gson.toJson(p));
				if(StringUtils.isNotEmpty(p.getPerms())){
					authorizationInfo.addStringPermission(p.getPerms());
				}

			}
		}

		return authorizationInfo;
	}

	 /**
     * 清理缓存权限
     */
    public void clearCachedAuthorizationInfo()
    {
        this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
    }

}

以上便是shiro的精华,当然还需要各种配置才能是shrio在项目中“活”起来
配置一个属于自己的shiroConfig

/**
 * 权限配置文件
 * @ClassName: ShiroConfiguration
 * @author lp
 * @date 2019年8月25日
 *
 */
@Configuration
public class ShiroConfig {

	/**
	 * 这是shiro的大管家,相当于mybatis里的SqlSessionFactoryBean
	 * @param securityManager
	 * @return
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//		Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
//		//自定义拦截器
//		filtersMap.put("authc", new ShiroLoginFilter());
//		shiroFilterFactoryBean.setFilters(filtersMap);
		shiroFilterFactoryBean.setFilterChainDefinitionMap(ShiroFilterMapFactory.shiroFilterMap());
		//登录
		shiroFilterFactoryBean.setLoginUrl("/admin/login");
		//首页
		shiroFilterFactoryBean.setSuccessUrl("/admin/login");
		shiroFilterFactoryBean.setSuccessUrl("/admin/index");
//		//错误页面,认证不通过跳转
		shiroFilterFactoryBean.setUnauthorizedUrl("/error/403");
//		//页面权限控制


		shiroFilterFactoryBean.setSecurityManager(securityManager);
		return shiroFilterFactoryBean;
	}

	/**
	 * web应用管理配置
	 * @param shiroRealm
	 * @param cacheManager
	 * @param manager
	 * @return
	 */
	@Bean
	public DefaultWebSecurityManager securityManager(Realm shiroRealm,CacheManager cacheManager,RememberMeManager manager) {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setCacheManager(cacheManager);
		securityManager.setRememberMeManager(manager);//记住Cookie
		securityManager.setRealm(shiroRealm);
		securityManager.setSessionManager(sessionManager());
		return securityManager;
	}
	/**
	 * session过期控制
	 * @return
	 * @author lp
	 * @Date 2019年11月2日 下午12:49:49
	 */
	@Bean
	public  DefaultWebSessionManager sessionManager() {
		DefaultWebSessionManager defaultWebSessionManager=new DefaultWebSessionManager();
		// 设置session过期时间3600s
		Long timeout=60L*1000*60;//毫秒级别
		defaultWebSessionManager.setGlobalSessionTimeout(timeout);
		return defaultWebSessionManager;
	}
	/**
	 * 加密算法
	 * @return
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcher() {
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
		hashedCredentialsMatcher.setHashAlgorithmName("MD5");//采用MD5 进行加密
		hashedCredentialsMatcher.setHashIterations(1);//加密次数
		return hashedCredentialsMatcher;
	}

	/**
	 * 记住我的配置
	 * @return
	 */
	@Bean
	public RememberMeManager rememberMeManager() {
		Cookie cookie = new SimpleCookie("rememberMe");
        cookie.setHttpOnly(true);//通过js脚本将无法读取到cookie信息
        cookie.setMaxAge(60 * 60 * 24);//cookie保存一天
		CookieRememberMeManager manager=new CookieRememberMeManager();
		manager.setCookie(cookie);
		return manager;
	}
	/**
	 * 缓存配置
	 * @return
	 */
	@Bean
	public CacheManager cacheManager() {
		MemoryConstrainedCacheManager cacheManager=new MemoryConstrainedCacheManager();//使用内存缓存
		return cacheManager;
	}

	/**
	 * 配置realm,用于认证和授权
	 * @param hashedCredentialsMatcher
	 * @return
	 */
	@Bean
	public AuthorizingRealm shiroRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
		MyShiroRealm shiroRealm = new MyShiroRealm();
		//校验密码用到的算法
		shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
		return shiroRealm;
	}

	/**
	 * 启用shiro方言,这样能在页面上使用shiro标签
	 * @return
	 */
	@Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

	/**
     * 启用shiro注解
     *加入注解的使用,不加入这个注解不生效
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }


}

shiroFilterMap 封装shiro内部拦截顺序

/**
 * @ClassName: ShiroFilterMapFactory
 * @author lp
 * @date 2019年8月26日
 *
 */
public class ShiroFilterMapFactory {

/**
anon:例子/admins/**=anon 没有参数,表示可以匿名使用。

authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,没有参数

roles(角色):例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。

perms(权限):例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。

rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。

port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString

是你访问的url里的?后面的参数。

authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证

ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https

user:例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查

*/

	public static Map<String, String> shiroFilterMap() {

//		设置路径映射,注意这里要用LinkedHashMap 保证有序
		LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
		filterChainDefinitionMap.put("/Api/**/**", "anon");
		//对所有用户认证
		filterChainDefinitionMap.put("/user/loginByUser", "anon"); //用户登录
		filterChainDefinitionMap.put("/user/loginByPhone", "anon");//手机号登录
		filterChainDefinitionMap.put("/user/getPhoneCode", "anon");//获取手机验证码
		filterChainDefinitionMap.put("/user/registered", "anon");//注册
		filterChainDefinitionMap.put("/user/forgetPassword", "anon");//忘记密码
		//swagger放开过滤
		filterChainDefinitionMap.put("/swagger-ui.html", "anon");
		filterChainDefinitionMap.put("/webjars/**", "anon");
		filterChainDefinitionMap.put("/v2/**", "anon");
		filterChainDefinitionMap.put("/swagger-resources/**", "anon");

		filterChainDefinitionMap.put("/static/**", "anon");//静态文件
		filterChainDefinitionMap.put("/admin/login", "anon");//登录页面
		filterChainDefinitionMap.put("/admin/logout", "logout");//登出页面
		//放验证码
		filterChainDefinitionMap.put("/captcha/**", "anon");//验证码
		// 释放 druid 监控画面
		filterChainDefinitionMap.put("/druid/**", "anon");
		//释放websocket请求
		filterChainDefinitionMap.put("/websocket", "anon");
		//前端
		filterChainDefinitionMap.put("/", "anon");
		filterChainDefinitionMap.put("/index", "anon");//任务调度暂时放开

		filterChainDefinitionMap.put("/quartz/**", "anon");

		//
		//对所有页面进行认证
		filterChainDefinitionMap.put("/**","authc");
		return filterChainDefinitionMap;
	}
}

配置结束,但是如何在项目中使用呢 ? 接下来模拟一个用户登录场景,我们从shiro验证用户权限,和信息 用户登录接口

 String userName = user.getUsername();
 Subject currentUser = SecurityUtils.getSubject();
			 //是否验证通过
			 if(!currentUser.isAuthenticated()) {
				 UsernamePasswordToken token =new UsernamePasswordToken(userName,user.getPassword());
				 try {
					 if(rememberMe) {
						 token.setRememberMe(true);
					 }
					 //存入用户
					 currentUser.login(token);
					 if(StringUtils.isNotNull(ShiroUtils.getUser())) {
			     		 return  AjaxResult.success();
			     	 }else {
			     		 return  AjaxResult.error(500,"未知账户");
			     	 }
				 }catch (UnknownAccountException uae) {
			            logger.info("对用户[" + userName + "]进行登录验证..验证未通过,未知账户");
			         	return  AjaxResult.error(500,"未知账户");
			        }

shrio 会根据用户名,密码跳到我们的Realm里面验证

/**
	 * 配置realm,用于认证和授权
	 * @param hashedCredentialsMatcher
	 * @return
	 */
	@Bean
	public AuthorizingRealm shiroRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
		MyShiroRealm shiroRealm = new MyShiroRealm();
		//校验密码用到的算法
		shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
		return shiroRealm;
	}

验证成功后,读取我们在ShiroConfig配置的setSuccessUrl

//登录
		shiroFilterFactoryBean.setLoginUrl("/admin/login");
		//首页
		shiroFilterFactoryBean.setSuccessUrl("/admin/login");
		shiroFilterFactoryBean.setSuccessUrl("/admin/index");
//		//错误页面,认证不通过跳转
		shiroFilterFactoryBean.setUnauthorizedUrl("/error/403");

ok shiro 配置结束!!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值