shiro(一):shiro基本概念及基本使用(认证、授权)

这篇具有很好参考价值的文章主要介绍了shiro(一):shiro基本概念及基本使用(认证、授权)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. 权限的管理

1.1 什么是权限管理

基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

权限管理包括用户身份认证授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。

1.2 什么是身份认证

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

1.3 什么是授权

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

2. 什么是shiro

Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。

3. shiro的核心架构

shiro usernamepasswordtoken,shiro,系统安全,网络,安全

3.1 Subject:主体

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

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

3.2 SecurityManager:安全管理器

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

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

3.3 Authenticator:认证器

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

3.4 Authorizer:授权器

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

3.5 Realm:领域

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

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

3.6 SessionManager:会话管理

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

3.7 SessionDAO:会话dao

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

3.8 CacheManager:缓存管理

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

3.9 Cryptography:密码管理

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

4. shiro中的认证

4.1 认证

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

4.2 shiro中认证的关键对象

4.2.1 Subject:主体

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

4.2.2 Principal:身份信息

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

4.2.2 credential:凭证信息

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

4.3 认证流程

shiro usernamepasswordtoken,shiro,系统安全,网络,安全

  1. shiro将用户名(身份信息)和密码(凭证信息)打包成一个token(令牌)
  2. 然后通过token去通过shiro核心架构中的安全管理器进行认证
  3. 安全管理器调用认证器,认证器调用任务去获取数据
  4. 如果获取到的数据与系统存储(数据库等)的一致,则认证通过,否则认证失败

4.4 认证的简单开发

4.4.1 引入依赖
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-core</artifactId>
  <version>1.5.3</version>
</dependency>
4.4.2 引入shiro配置文件

配置文件:名称随意,以 .ini 结尾,放在 resources 目录下;后续用了springboot就不需要此配置文件了,是前期方便我们学习shiro书写我们系统中相关权限数据

注意:在实际的项目开发中并不会使用这种方式,这种方法可以用来初学时练手

[users]
zhangsan=123456
lisi=456789
4.4.3 开发认证代码
public class test01 {
    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("zhangsan","123456");
        try {
            System.out.println("认证状态"+subject.isAuthenticated());//fasle
            //用户认证
            subject.login(token);
            System.out.println("认证状态"+subject.isAuthenticated());
        }catch (UnknownAccountException e){
            e.printStackTrace();
            System.out.println("认证失败,用户名不存在");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("认证失败,密码错误");
        }
    }
}
4.4.4 .常见的异常类型
  • DisabledAccountException(帐号被禁用)
  • LockedAccountException(帐号被锁定)
  • ExcessiveAttemptsException(登录失败次数过多)
  • ExpiredCredentialsException(凭证过期)等
4.4.5 认证过程源码解析

可见:shiro相关源码解析

4.5 自定义Realm

通过分析源码可得:

  • 认证:
    1. 最终执行用户名比较是 在SimpleAccountRealm类 的 doGetAuthenticationInfo 方法中完成用户名校验
    2. 最终密码校验是在 AuthenticatingRealm类 的 assertCredentialsMatch方法 中
  • 总结:
    • AuthenticatingRealm 认证realm doGetAuthenticationInfo
    • AuthorizingRealm 授权realm doGetAuthorizationInfo

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

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

4.5.1 shiro提供的Realm

shiro usernamepasswordtoken,shiro,系统安全,网络,安全

4.5.2 根据认证源码认证使用的是SimpleAccountRealm

shiro usernamepasswordtoken,shiro,系统安全,网络,安全
SimpleAccountRealm的部分源码中有两个方法一个是 认证 一个是 授权:

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    UsernamePasswordToken upToken = (UsernamePasswordToken)token;
    SimpleAccount account = this.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 = this.getUsername(principals);
    this.USERS_LOCK.readLock().lock();

    AuthorizationInfo var3;
    try {
        var3 = (AuthorizationInfo)this.users.get(username);
    } finally {
        this.USERS_LOCK.readLock().unlock();
    }

    return var3;
}
4.5.3 自定义realm
public class CustomRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("==================");
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //在token中获取 用户名
        String principal = (String) authenticationToken.getPrincipal();
        System.out.println(principal);

        //实际开发中应当 根据身份信息使用jdbc mybatis查询相关数据库
        //在这里只做简单的演示
        //假设username,password是从数据库获得的信息
        String username="zhangsan";
        String password="123";
        if(username.equals(principal)){
            //参数1:返回数据库中正确的用户名
            //参数2:返回数据库中正确密码
            //参数3:提供当前realm的名字 this.getName();
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,password,this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}
4.5.4 自定义登录认证
public class TestAuthenticatorCustomRealm {
    public static void main(String[] args) {
        //1.创建安全管理对象 securityManager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //2.给安全管理器设置realm(设置为自定义realm获取认证数据)
        defaultSecurityManager.setRealm(new CustomRealm());
        //IniRealm realm = new IniRealm("classpath:shiro.ini");
        //3.给安装工具类中设置默认安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //4.获取主体对象subject
        Subject subject = SecurityUtils.getSubject();

        //5.创建token令牌
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");
        try {
            subject.login(token);//用户登录
            System.out.println("登录成功~~");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误!!");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误!!!");
        }

    }
}

4.6 使用MD5+SALT+HASH

shiro usernamepasswordtoken,shiro,系统安全,网络,安全

4.6.1 MD5
  • 作用:一般用来加密或者签名(校验和)
  • 特点:MD5算法不可逆如何内容相同无论执行多少次md5生成结果始终是一致
  • 网络上提供的MD5在线解密一般是用穷举的方法
  • 生成结果:始终是一个16进制32位长度字符串
4.6.2 SALT
  • 一段自定义字符,通过此字符+原密码,来使密码更加安全
  • 这个自定义字符最好是自定义在代码中
  • 还有一种方式是随机生成SALT,并将此SALT一起保存在数据库中(此方式黑客还需要判断SALT和密码的拼接方式)
4.6.3 加密基本测试
public static void main(String[] args) {
    //使用md5,通过hash算法让结果更加安全
    //通过构造方法将结果加密
    // 通过源码可以看到没有加盐默认盐值是1
    Md5Hash md5Hash = new Md5Hash("123");
    System.out.println(md5Hash.toHex());

    //使用MD5 + salt处理
    // 默认盐值是加在后面
    Md5Hash md5Hash1 = new Md5Hash("123", "java");
    System.out.println(md5Hash1.toHex());

    //使用md5 + salt + hash散列(参数代表要散列多少次,一般是 1024或2048)
    Md5Hash md5Hash2 = new Md5Hash("123", "java", 1024);
    System.out.println(md5Hash2.toHex());
}

结果:

202cb962ac59075b964b07152d234b70
b67e00dd69ba454e68c5e3dd228e19ec
2c5f16f56505041f97ab92c8e73c363d
4.6.4 自定义md5+salt的realm
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //获取 token中的 用户名
    String principal = (String) token.getPrincipal();

    //假设这是从数据库查询到的信息
    String username="zhangsan";
    String password="2c5f16f56505041f97ab92c8e73c363d";//加密后

    //根据用户名查询数据库
    if (username.equals(principal)) {
        //参数1:数据库用户名
        //参数2:数据库md5+salt之后的密码
        //参数3:注册时的随机盐
        //参数4:realm的名字
        return new SimpleAuthenticationInfo(principal,
                password,
                ByteSource.Util.bytes("java"),
                this.getName());
    }
    return null;
}
4.6.5 使用md5+salt 认证
public class TestAuthenticatorCustomMd5Realm {
    public static void main(String[] args) {
        //1.创建安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //2.注入realm
        CustomMd5Realm realm = new CustomMd5Realm();
        //3.设置realm使用hash凭证匹配器
        // 凭证匹配器是对token中密码和用户中密码进行验证的方法,默认是equals
        // 此处是用散列的凭证匹配器告诉shiro,我需要对密码进行md5加密并且设置一个散列
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //声明:使用的算法
        credentialsMatcher.setHashAlgorithmName("md5");
        //声明:散列次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        defaultSecurityManager.setRealm(realm);

        //4.将安全管理器注入安全工具
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //5.通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();
        //6.认证
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");

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

5. shiro中的授权

5.1 授权

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

5.2 关键对象

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

  • Who,即主体(Subject):主体需要访问系统中的资源。
  • What,即资源(Resource):如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。
  • How,权限/许可(Permission):规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。

5.3 授权流程

shiro usernamepasswordtoken,shiro,系统安全,网络,安全

5.4 授权方式

5.4.1 基于角色的访问控制

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

if(subject.hasRole("admin")){
   //用户拥有次角色可以操作什么资源
}
5.4.2 基于资源的访问控制

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

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

5.5 权限字符串

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

例子:文章来源地址https://www.toymoban.com/news/detail-769574.html

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

5.6 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需要额外集成!

5.7 开发授权

5.7.1 realm中授权实现:
public class CustomMd5Realm extends AuthorizingRealm {
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        String primaryPrincipal = (String)principals.getPrimaryPrincipal();
        System.out.println("身份信息: "+primaryPrincipal); //用户名

        //根据身份信息 用户名 获取当前用户的角色信息,以及权限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //假设 admin,user 是从数据库查到的 角色信息
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRole("user");
        //假设 ... 是从数据库查到的 权限信息赋值给权限对象
        simpleAuthorizationInfo.addStringPermission("user:*:01");
        simpleAuthorizationInfo.addStringPermission("product:*");//第三个参数为*省略

        return simpleAuthorizationInfo;
    }
    
    。。。。
}
5.7.2 授权测试:
public class TestAuthenticatorCustomMd5Realm {
    public static void main(String[] args) {
		。。。。
		try {
		    subject.login(token);
		        System.out.println("登录成功");
		    } catch (UnknownAccountException e) {
		        e.printStackTrace();
		        System.out.println("用户名错误");
		    } catch (IncorrectCredentialsException e) {
		        e.printStackTrace();
		        System.out.println("密码错误");
		    }
		
		    //授权
		    if (subject.isAuthenticated()){
		        //基于角色权限控制
		        System.out.println(subject.hasRole("admin"));
		        //基于多角色的权限控制
		        System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));//true
		        System.out.println(subject.hasAllRoles(Arrays.asList("admin", "manager")));//false
		        //是否具有其中一个角色
		        boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "manager"));
		        for (boolean aBoolean : booleans) {
		            System.out.println(aBoolean);
		        }
		
		        System.out.println("====这是一个分隔符====");
		
		        //基于权限字符串的访问控制  资源标识符:操作:资源类型
		        //用户具有的权限 user:*:01  prodect:*
		        System.out.println("权限:"+subject.isPermitted("user:update:01"));
		        System.out.println("权限:"+subject.isPermitted("product:update:02"));
		
		        //分别具有哪些权限
		        boolean[] permitted = subject.isPermitted("user:*:01", "user:update:02");
		        for (boolean b : permitted) {
		            System.out.println(b);
		        }
		
		        //同时具有哪些权限
		        boolean permittedAll = subject.isPermittedAll("product:*:01", "product:update:03");
		        System.out.println(permittedAll);
		    }
		}
	}
}

到了这里,关于shiro(一):shiro基本概念及基本使用(认证、授权)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • PostMan授权认证使用

    对于很多应用,出于安全考虑我们的接口并不希望对外公开。这个时候就需要使用授权(Authorization)机制。 授权过程验证您是否具有访问服务器所需数据的权限。 当发送请求时,通常必须包含参数,以确保请求具有访问和返回所需数据的权限。 Postman提供授权类型,可以轻松地

    2024年02月04日
    浏览(30)
  • SpringBoot 如何使用 OAuth2 进行认证和授权

    OAuth2 是一种授权框架,可以用于实现第三方应用程序访问用户资源的安全性。在 SpringBoot 中,我们可以使用 Spring Security 和 Spring OAuth2 来实现 OAuth2 的认证和授权功能。本文将介绍如何在 SpringBoot 中使用 OAuth2 进行认证和授权。 在开始介绍如何使用 OAuth2 进行认证和授权之前,

    2024年02月13日
    浏览(35)
  • 漏洞复现 | Apache Shiro 授权绕过漏洞(CVE-2022-32532)

    0x00 漏洞描述         Apache Shiro 是一套用于执行认证、授权、加密和会话管理的 Java 安全框架。2022年06月29日 APache 官方发布了一则关于 Apache Shiro 的安全通告,Apache Shiro 1.9.1 前的版本 RegExPatternMatcher 在使用带有 “.” 的正则时,可能会导致权限绕过。漏洞源于 RegExPatter

    2024年02月02日
    浏览(40)
  • Spring Boot 如何使用 Spring Security 进行认证和授权

    在 Web 应用程序中,认证和授权是非常重要的功能。Spring Security 是一个基于 Spring 框架的强大的安全框架,它提供了完整的认证和授权解决方案,并且可以轻松地集成到 Spring Boot 应用程序中。本文将介绍如何在 Spring Boot 中使用 Spring Security 进行认证和授权,并提供示例代码。

    2024年02月11日
    浏览(41)
  • shiro安全认证之FilterChainDefinitionMap

    最近在用shiro实现安全认证功能时发现一个问题 上图我们定义了一个LinkedHashMap,并往Map里面添加了两对键值对,然后执行  之后在将这两个键值对里面的值进行修改,但是没有执行上面的set操作,debug时发现,虽然没有执行set操作,但是FilterChainDefinitionMap里面的filterMap的键值

    2024年02月14日
    浏览(43)
  • 小程序(二)shiro+jwt登录认证

    Shiro是Java领域非常知名的认证( Authentication )与授权 ( Authorization )框架,用以替代JavaEE中的JAAS功能。相 较于其他认证与授权框架,Shiro设计的非常简单,所以广受好 评。任意JavaWeb项目都可以使用Shiro框架,而Spring Security 必须要使用在Spring项目中。所以Shiro的适用性更加广

    2024年02月10日
    浏览(33)
  • 手把手教你Shiro整合JWT实现登录认证

    SpringBoot Mybatis-plus Shiro JWT Redis Shiro: Shiro 是一个基于 Java 的开源的安全框架。 在 Shiro 的核心架构里面,Subject 是访问系统的用户。SecurityManager 是安全管理器,负责用户的认证和授权,相当于 Shiro 的老大哥。 Realm 相当于数据源,用户的认证和授权都在 Realm 的方法中进行。

    2023年04月17日
    浏览(54)
  • 自定义过滤器配置 Shiro 认证失败返回 json 数据

    by emanjusaka from ​ https://www.emanjusaka.top/2023/10/filter-shiro-authentication-error-json 彼岸花开可奈何 本文欢迎分享与聚合,全文转载请留下原文地址。 Shiro 权限框架认证失败默认是重定向页面的,这对于前后端分离的项目及其不友好,可能会造成请求404的问题。现在我们自定义过滤器

    2024年02月08日
    浏览(43)
  • WebSocket与Shiro认证信息传递的实现与安全性探讨

    在现代Web应用程序中,WebSocket已经成为实时双向通信的重要组件。而Shiro作为一个强大的Java安全框架,用于处理身份验证、授权和会话管理。本文将探讨如何通过WebSocket与Shiro集成,实现认证信息的传递,并关注在这一过程中确保安全性的关键考虑因素。 步骤概述 WebSocket连接

    2024年01月25日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包