Shiro学习

Shiro

教学来自b站
讲的真的特别好

入门概述

Apache Shiro | Simple. Java. Security.

是什么?

ApacheShiro是一个功能强大且易于使用的Java安全(权限)框架。Shiro可以完成:认证、授权、加密、会话管理、与Web集成、缓存等。借助Shiro您可以快速轻松地保护任何应用程序——从最小的移动应用程序到最大的Web和企业应用程序。

为什么要使用Shiro

自2003年以来,框架格局发生了相当大的变化,因此今天仍然有很多系统在使用Shiro。这与Shiro 的特性密不可分。

  • 易于使用:使用Shiro构建系统安全框架非常简单。就算第一次接触也可以快速掌握。
  • 全面: Shiro包含系统安全框架需要的功能,满足安全需求的“一站式服务”。
  • 灵活: Shiro·可以在任何应用程序环境中工作。虽然它可以在·Web、EJB和IoC·环境中工作,但不需要依赖它们。Shiro也没有强制要求任何规范,甚至没有很多依赖项。
  • 强力支持Web: Shiro具有出色的Web应用程序支持,可以基于应用程序URL和Web协议(例如REST)创建灵活的安全策略,同时还提供一组JSP库来控制页面输出。
  • 兼容性强: Shiro 的设计模式使其易于与其他框架和应用程序集成。Shiro与Spring、Grails、Wicket、Tapestry、Mule、Apache·Camel、Vaadin等框架无缝集成。
  • 社区支持:Shiro 是 Apache 软件基金会的一个开源项目,有完备的社区支持,文档 支持。如果需要,像 Katasoft 这样的商业公司也会提供专业的支持和服务。

权限的管理

权限管理

  • 基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
  • 权限管理包括用户身份认证授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。

什么是身份认证

身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的 用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。

什么是授权

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的

shiro的核心架构

image-20221014094805453

Subject

Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。

Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权

SecurityManager

SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。

SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。

Authenticator

Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。

Authorizer

Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。

Realm

Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。

注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。

SessionManager

sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。

SessionDAO

SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。

CacheManager

CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。

Cryptography

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

Shiro中的认证

认证

身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。

shiro中认证的关键对象

Subject:主体
访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;

Principal:身份信息

是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。

credential:凭证信息

是只有主体自己知道的安全信息,如密码、证书等

认证流程

image-20221014111812326

demo

引入依赖

   <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.5.3</version>
        </dependency>

shiro 配置文件

.ini 结尾的文件

用来学习shiro时书写系统中的相关数据权限

image-20221014135837787

demo

  1. 创建安全管理器对象
  2. 给安全管理器设置realm
  3. SecurityUtils给全局安全工具类设置安全管理器
  4. 关键对象 subject 主体
  5. 创建令牌
package com.bo;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;

/**
 * @author: bo
 * @date: 2022/10/14
 * @description:
 */
public class TestAuthenticator {
    public static void main(String[] args) {
        //1.创建安全管理器对象
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        //2.给安全管理器设置realm
        securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        //3.SecurityUtils给全局安全工具类设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        //4.关键对象 subject 主体
        Subject subject = SecurityUtils.getSubject();
        //5.创建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("xiaozhang","123");
        try {
            System.out.println("认证状态"+subject.isAuthenticated());
            subject.login(token);
            System.out.println("认证状态"+subject.isAuthenticated());
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("认证失败:用户名不存在");
        }catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("认证失败,密码错误");
        }

    }
}

常见的异常类型

  • DisabledAccountException(帐号被禁用)
  • LockedAccountException(帐号被锁定)
  • ExcessiveAttemptsException(登录失败次数过多)
  • ExpiredCredentialsException(凭证过期)等

看看认证的源码

打个断点

image-20221014143034731

做认证的还是securityManager

image-20221014143138187

image-20221014143150349

再进入login

image-20221014143229760

再进入authenticate方法

image-20221014143342196

再进入doAuthenticate

image-20221014143414264

先做一个断言,然后用getRealms拿到域

image-20221014143505525

调用了realm.getAuthenticationInfo

image-20221014143532718

进入

先去缓存中拿了一下认证信息,没有缓存

进入这个方法

image-20221014143925623

拿到用户名,看看加没加锁,密码过期了吗

image-20221014143941251

最终执行用户名比较是在这里image-20221014144147194

最终密码校验在这里

image-20221014144318331

总结:

AuthenticatingRealm 认证realm doGetAuthenticationInfo

AuthorizingRealm 授权realm doGetAuthorizationInfo

我们要自定义认证用户名就需要继承AuthorizingRealm类

AuthorizingRealm类2个方法都有,因为继承了AuthenticatingRealm类

密码是shiro自动验证的

为什么密码是shiro自己处理的呢

因为密码可能会做加密,自己处理可能影响到shiro的加密的使用,所以密码的校验由shiro完成

image-20221014154156793

自定义Realm的作用:放弃使用.ini文件,使用数据库查询,去继承AuthorizingRealm

上边的程序使用的是Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm。

SimpleAccountRealm的部分源码中有两个方法一个是 认证 一个是 授权

部分源码

public class SimpleAccountRealm extends AuthorizingRealm {
		//.......省略
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        SimpleAccount account = getUser(upToken.getUsername());

        if (account != null) {

            if (account.isLocked()) {
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }
            if (account.isCredentialsExpired()) {
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }

        }

        return account;
    }

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = getUsername(principals);
        USERS_LOCK.readLock().lock();
        try {
            return this.users.get(username);
        } finally {
            USERS_LOCK.readLock().unlock();
        }
    }
}

自定义realm

package com.bo.realm;

import jdk.nashorn.internal.parser.Token;
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;

/**
 * @author: bo
 * @date: 2022/10/14
 * @description: 自定义realm实现,将认证、授权数据的来源转化为数据库的实现
 */
public class CustomRealm extends AuthorizingRealm {
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //在token中获取用户名
        String principal = (String) token.getPrincipal();
        System.out.println(principal);
        //根据身份信息使用jdbc mybatis查询数据库
        if("xiaozhang".equals(principal)){
            //数据库查到的参数1:用户名 参数2:密码 参数3
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("xiaozhang", "123",this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}

测试类

package com.bo;

import com.bo.realm.CustomRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

/**
 * @author: bo
 * @date: 2022/10/14
 * @description: 使用自定义realm
 */
public class TestCustomerRealmAuthenticator {
    public static void main(String[] args) {
        //创建securityManager
        DefaultSecurityManager manager = new DefaultSecurityManager();
        //设置自定义realm
        manager.setRealm(new CustomRealm());
        //将安全工具类设置安全工具类
        SecurityUtils.setSecurityManager(manager);
        //通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();
        //创建token
        UsernamePasswordToken token = new UsernamePasswordToken("xiaozhang", "123456");

        try {
            subject.login(token);
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        }catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }


    }
}

MD5+Salt+Hash

注册的时候加盐,把盐和加密后的密码都存数据库

登录的时候,把用户传过来的密码从数据库取出来,加盐处理和数据库加密后的密码比较,一样就登录成功

使用md5
Md5Hash md5Hash =new Md5Hash(“123”);
System.out.println(md5Hash.toHex());

md5+盐

Md5Hash md5Hash1 = new Md5Hash(“123”, “dasdas”);
System.out.println(md5Hash1.toHex());

md5+盐+hash散列第三个参数是次数
Md5Hash md5Hash2 = new Md5Hash(“123”, “dasdas”,1024);
System.out.println(md5Hash2.toHex());

改造Shiro

md5加密

使用时候告诉要用md5加密

package com.bo;

import com.bo.realm.CustomerMD5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

/**
 * @author: bo
 * @date: 2022/10/14
 * @description:
 */
public class TestCustomerMD5RealmAuthenicator {
    public static void main(String[] args) {
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        CustomerMD5Realm realm = new CustomerMD5Realm();
        //设置real使用hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("md5");
        realm.setCredentialsMatcher(credentialsMatcher);
        defaultSecurityManager.setRealm(realm);
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("xiaozhang", "123");
        try {
            subject.login(token);
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }


    }
}

package com.bo.realm;

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;

/**
 * @author: bo
 * @date: 2022/10/14
 * @description: 自定义realm,加入md5+salt+hash
 */
public class CustomerMD5Realm extends AuthorizingRealm {
    public static void main(String[] args) {

    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取身份信息
        String principal = (String) authenticationToken.getPrincipal();
        if ("xiaozhang".equals(principal)) {
            return new SimpleAuthenticationInfo("principal", "202cb962ac59075b964b07152d234b70", this.getName());
        }
        return null;
    }
}

md5+盐

image-20221014204231025

md5+盐+散列

image-20221014204907160

授权

授权

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。

关键对象

授权可简单理解为who对what(which)进行How操作:

Who,即主体(Subject),主体需要访问系统中的资源。

What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。

How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。

image-20221015103553201

授权方式

基于角色的访问控制

RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制

if(subject.hasRole("admin")){
   //操作什么资源
}

基于资源的访问控制

RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制

if(subject.isPermission("user:update:01")){ //资源实例
  //对资源01用户具有修改的权限
}
if(subject.isPermission("user:update:*")){  //资源类型
  //对 所有的资源 用户具有更新的权限
}

权限字符串

权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*

通配符。

例子:

用户创建权限:user:create,或user:create:*
用户修改实例001的权限:user:update:001
用户实例001的所有权限:user:*:001

shiro授权实现方式

编程式

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
	//有权限
} else {
	//无权限
}

注释式

@RequiresRoles("admin")
public void hello() {
	//有权限
}

标签式

JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
	<!— 有权限—>
</shiro:hasRole>
注意: Thymeleaf 中使用shiro需要额外集成!

在自定义realm中的doGetAuthorizationInfo方法进行授权

测试类

image-20221015104924951

单角色控制

package com.bo;

import com.bo.realm.CustomerMD5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

/**
 * @author: bo
 * @date: 2022/10/14
 * @description:
 */
public class TestCustomerMD5RealmAuthenicator {
    public static void main(String[] args) {
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        CustomerMD5Realm realm = new CustomerMD5Realm();
        //设置real使用hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("md5");
        //散列次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        defaultSecurityManager.setRealm(realm);
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("xiaozhang", "123");
        try {
            subject.login(token);
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }
        //认证用户进行授权
        if (subject.isAuthenticated()) {
            //1.基于角色权限控制
            System.out.println(subject.hasRole("admin"));
        }

    }
}

package com.bo.realm;

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 org.apache.shiro.util.ByteSource;

/**
 * @author: bo
 * @date: 2022/10/14
 * @description: 自定义realm,加入md5+salt+hash
 */
public class CustomerMD5Realm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("身份信息" + primaryPrincipal);
        //根据身份信息 用户名 获取当前用户的角色信息,以及权限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //将数据库查询的角色信息赋值给权限对象
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRole("user");
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取身份信息
        String principal = (String) authenticationToken.getPrincipal();
        //参数1数据库的用户名,参数2,md5+salt加密的密码,参数3加的盐,参数4realm的名字
        if ("xiaozhang".equals(principal)) {
            return new SimpleAuthenticationInfo("xiaochen",
                    "3e89a6098a07842cf0ee50cb975b8829", ByteSource.Util.bytes("dasdas"), this.getName());
        }
        return null;
    }
}

角色权限是user或者admin都可以通过

多角色权限控制

//基于多角色权限控制
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));

image-20221015110308582

是否具有其中一个角色

//是否具有其中一个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "super"));
for (boolean aBoolean : booleans) {
    System.out.println(aBoolean);
}

基于权限字符串的访问控制, 资源标识符:操作:资源类型

image-20221015110606905

image-20221015110801209

image-20221015111034010

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
学习Shiro框架可以按照以下步骤进行: 1. 了解基础概念:首先,你需要了解Shiro的基本概念和术语,例如主体(Subject)、认证(Authentication)、授权(Authorization)、Realm等。可以阅读Shiro的官方文档或者相关的教程来获得这些知识。 2. 安装和配置:安装Shiro框架并进行基本的配置。你可以在Shiro的官方网站上找到安装指南和配置示例。 3. 认证功能:学习如何使用Shiro进行用户认证,包括用户名密码认证、Remember Me功能、多Realm认证等。可以尝试编写简单的认证示例来理解这些功能。 4. 授权功能:学习如何使用Shiro进行用户授权,包括角色授权和权限授权。了解如何定义角色和权限,并且如何在代码中进行授权判断。 5. Session管理:了解Shiro如何管理用户的会话信息,包括会话超时、会话验证等。学习如何使用Shiro提供的Session API来管理会话。 6. 整合框架:如果你使用其他的Java框架,例如Spring或者Spring Boot,学习如何将Shiro与这些框架进行整合,以便更好地利用Shiro的功能。 7. 安全性优化:深入了解Shiro安全性能优化技巧,例如密码加密、安全配置、防止常见安全漏洞等。 8. 实战练习:通过编写实际的应用程序来巩固所学的知识。可以尝试开发一个简单的Web应用程序,使用Shiro进行用户认证和授权。 除了官方文档和教程,还可以参考一些优秀的书籍或在线教程,例如《Apache Shiro官方指南》、《深入浅出Shiro安全框架》等。此外,加入Shiro的社区或者论坛,与其他开发者交流经验也是一个很好的学习方式。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值