Shiro框架学习(*)

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

主要功能
三个核心组件:Subject, SecurityManager 和 Realms.
Subject:即“当前操作的用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是Shiro的“用户”概念。

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的整体架构,各组件的概念
② shiro认证,授权的过程
③ shiro自定义的realm、Filter
④ shiro session管理
⑤ shiro的缓存管理
⑥ shiro继承spring

二、shiro安全框架的简介
① shiro是Apache提供的一个强大灵活的安全框架

② shiro提供了认证、授权、企业会话管理、加密、缓存管理相关的功能,使用shiro可以非常方便的完成项目的权限管理模块开发

三、shiro与spring Security的比较(spring的官网也是用shiro做安全管理的)

① Apache shiro: 简单灵活、可脱离spring、粒度较粗
② spring Security: 复杂笨重、不可脱离spring、粒度更细

四、Shiro的整体架构(下图来自慕课视频截图也是shiro官网提供的整体架构图)
在这里插入图片描述
①上面标记为1的是shiro的主体部分subject,可以理解为当前的操作用户

② Security Manager为Shiro的核心,shiro是通过security Manager来提供安全服务的,security Manager管理着Session Manager、Cache Manager等其他组件的实例:Authenticator(认证器,管理我们的登录登出) Authorizer(授权器,负责赋予主体subject有哪些权限) Session Manager(shiro自己实现的一套session管理机制,可以不借助任何web容器的情况下使用session) Session Dao(提供了session的增删改查操作) cache Manager(缓存管理器,用于缓存角色数据和权限数据) Pluggable Realms(shiro与数据库/数据源之间的桥梁,shiro获取认证信息、权限数据、角色数据都是通过Realms来获取)

③ 上图标记为2的cryptography是用来做加密的,使用它可以非常方便快捷的进行数据加密。

④ 上面箭头的流程可以这样理解:主体提交请求到Security Manager,然后由Security Manager调用Authenticator去做认证,而Authenticator去获取认证数据的时候是通过Realms从数据源中来获取的,然后把从数据源中拿到的认证信息与主体提交过来的认证信息做比对。授权器Authorizer也是一样。

五、Shiro的认证
① 创建Security Manager:Security Manager是用来提供安全服务的,所以在做shiro认证的时候要先创建此对象
② 主体Subject提交请求给Security Manager
③ Security Manager调用Authenticator组件做认证
④Authenticator通过Realm从数据源中获取认证数据

以下通过代码来演示shiro是如何做认证的

① 引入依赖

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
        </dependency>

② 使用示例

package com.imooc.test;
 
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
 
 
/**
 * @auther xiehuaxin
 * @create 2018-07-02 14:06
 * @todo 创建一个测试类,测试认证
 */
public class AuthenticationTest {
 
    //除了SimpleAccountRealm还有JdbcRealm等
    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
 
    //在认证之前先在Realm中添加一个用户,创建Security Manager的时候要用到Realm
    @Before
    public void addUser() {
        simpleAccountRealm.addAccount("xiehuaxin","123456");
    }
 
    @Test
    public void testAutentication() {
        //1.构建Security Manager环境(Security Manager是用来提供安全服务的,所以在做shiro认证的时候要先创建此对象,创建Security Manager对象之后要设置Realm)
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);
 
        //2.获取向Security Manager提交请求的subject,而主体subject可以通过shiro提供的一个工具类SecurityUtils来获取
        SecurityUtils.setSecurityManager(defaultSecurityManager);//使用SecurityUtils之前要设置Security Manager环境
        Subject subject = SecurityUtils.getSubject();
 
        //3.主体Subject提交请求给Security Manager -->  subject.login(token);
        UsernamePasswordToken token = new UsernamePasswordToken("xiehuaxin","123456");//提交请求时需要一个token,所以要先创建token
        subject.login(token);
 
        //4. shiro提供了一个检查主体subject是否认证的方法isAuthenticated(),此方法的返回结果是一个boolean值
        System.out.println(subject.isAuthenticated());
 
        subject.logout();
        System.out.println(subject.isAuthenticated());
        
    }
}

六、Shiro的授权(shiro授权过程与认证过程基本一致)
① 创建Security Manager
② 主体subject授权
③ 主体授权是交给Security Manager授权
④ Security Manager调用授权器Authorizer授权
⑤ 通过Realm在数据库或者缓存中来获取授权的数据(角色数据和权限数据)

以下通过代码来演示shiro是如何做授权的

package com.imooc.test;
 
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
 
/**
 * @auther xiehuaxin
 * @create 2018-07-02 14:06
 * @todo 创建一个测试类,测试认证
 */
public class AuthenticationTest {
 
    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
 
    @Before
    public void addUser() {
        //添加用户的时候为此用户添加角色,一个用户可以拥一个或多个角色
        simpleAccountRealm.addAccount("xiehuaxin", "123456", "admin", "user");
    }
 
    @Test
    public void testAutentication() {
        //1.构建Security Manager环境(Security Manager是用来提供安全服务的,所以在做shiro认证的时候要先创建此对象,创建Security Manager对象之后要设置Realm)
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);
        //2.获取向Security Manager提交请求的subject,而主体subject可以通过shiro提供的一个工具类SecurityUtils来获取
        SecurityUtils.setSecurityManager(defaultSecurityManager);//使用SecurityUtils之前要设置Security Manager环境
        Subject subject = SecurityUtils.getSubject();
        //3.主体Subject提交请求给Security Manager -->  subject.login(token);
        UsernamePasswordToken token = new UsernamePasswordToken("xiehuaxin", "123456");//提交请求时需要一个token,所以要先创建token
        subject.login(token);
        //4. shiro提供了一个检查主体subject是否认证的方法isAuthenticated(),此方法的返回结果是一个boolean值
        System.out.println(subject.isAuthenticated());
 
        //校验角色
        subject.checkRoles("admin");
    }
}

七、Shiro自定义Realm

  1. 在说自定义Realm之前先讲一下Shiro的内置Realm
    ① IniRealm

    a) 在项目中新建一个ini文件,下面内容的意思是,新建了一个用户,账号是xiehuaxin,密码为123456,此账号的角色为admin,而admin角色又具有删除用户和更新用户的权限(多个权限中间用逗号分隔),注意这里的“user:delete”不是一定要这样写,只是一个字符串,你也可以写成"admin=shanchuyonghu",这要在校验的时候也用“shanchuyonghu”来匹配就行了,当然这种命名习惯还是不建议的,建议使用“user:delete”这种对象+动作的命名方式。

[users]
xiehuaxin=123456,admin
[roles]
admin=user:delete,user:update

package com.imooc.test;
 
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
 
/**
 * @auther xiehuaxin
 * @create 2018-07-02 14:47
 * @todo
 */
public class IniRealmTest {
 
    @Test
    public void testAutentication() {
        IniRealm iniRealm = new IniRealm("classpath:user.ini");
        //1.构建Security Manager环境(Security Manager是用来提供安全服务的,所以在做shiro认证的时候要先创建此对象,创建Security Manager对象之后要设置Realm) 
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(iniRealm);
        //2.获取向Security Manager提交请求的subject,而主体subject可以通过shiro提供的一个工具类SecurityUtils来获取 SecurityUtils.setSecurityManager(defaultSecurityManager);//使用SecurityUtils之前要设置Security Manager环境 Subject subject = SecurityUtils.getSubject(); //3.主体Subject提交请求给Security Manager --> subject.login(token); UsernamePasswordToken token = new UsernamePasswordToken("xiehuaxin","123456");//提交请求时需要一个token,所以要先创建token subject.login(token); //4. shiro提供了一个检查主体subject是否认证的方法isAuthenticated(),此方法的返回结果是一个boolean值 System.out.println(subject.isAuthenticated());
        subject.checkRoles("admin");
        subject.checkPermission("user:delete");
    }
}

② JdbcRealm
a) 引入数据库相关依赖

    <!-- 引入驱动包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.45</version>
        </dependency>
        <!--引入数据源-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.4</version>
        </dependency>
package com.imooc.test;
 
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
 
/**
 * @auther xiehuaxin
 * @create 2018-07-02 15:16
 * @todo
 */
public class JdbcRealmTest {
 
    DruidDataSource dataSource = new DruidDataSource();
 
    {
        dataSource.setUrl("jdbc:mysql://localhost:3306/test");
        dataSource.setUsername("root");
        dataSource.setPassword("123123");
    }
 
    @Test
    public void testAutentication() {
        //1.创建jdbcReaml对象
        JdbcRealm jdbcRealm = new JdbcRealm();
        //2.设置数据源
        jdbcRealm.setDataSource(dataSource);
         //这里需要注意的是,在使用JdbcRealm的时候要设置权限的开关,只有设置为true时才会去查询权限数据
        jdbcRealm.setPermissionsLookupEnabled(true);
        //使用自定义的表/sql
        String sql = "select password from test_user where user_name = ?";
        jdbcRealm.setAuthenticationQuery(sql);
        String roleSql = "select role_name from test_user_role where user_name = ?";
        jdbcRealm.setUserRolesQuery(roleSql);
 
        //1.构建Security Manager环境(Security Manager是用来提供安全服务的,所以在做shiro认证的时候要先创建此对象,创建Security Manager对象之后要设置Realm)
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(jdbcRealm);
        //2.获取向Security Manager提交请求的subject,而主体subject可以通过shiro提供的一个工具类SecurityUtils来获取
        SecurityUtils.setSecurityManager(defaultSecurityManager);//使用SecurityUtils之前要设置Security Manager环境
        Subject subject = SecurityUtils.getSubject(); 
        //3.主体Subject提交请求给Security Manager --> subject.login(token);
        UsernamePasswordToken token = new UsernamePasswordToken("xiehuaxin","123456");//提交请求时需要一个token,所以要先创建token 
        subject.login(token);
        // 4. shiro提供了一个检查主体subject是否认证的方法isAuthenticated(),此方法的返回结果是一个boolean值
        System.out.println(subject.isAuthenticated());
    }}

在使用JdbcRealm时,如果没有设置查询语句,JdbcReal有自己默认的查询语句。

八、自定义Realm
通过查看JdbcRealm或者IniRealm的源码可以发现,它们都继承自AuthorizingRealm,所以要自定义Realm也要使自定义的Realm继承AuthorizingRealm.

package com.imooc.test;
 
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 java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
 
/**
 * @auther xiehuaxin
 * @create 2018-07-02 15:48
 * @todo
 */
public class CustomRealm extends AuthorizingRealm {
 
    Map<String, String> userMap = new HashMap<String, String>(16);
    {
        userMap.put("xiehuaxin","123456");
        super.setName("customReal");//自定义
    }
 
    /**
     * 用来做授权(就是checkRole,checkPermission时用到的)
     * @param principalCollection
     * @return
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.先获取到用户名
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //2.从数据库或者缓存中获取角色数据
        Set<String> roles = getRolesByUserName(userName);
        //3.从数据库或缓存中获取权限数据
        Set<String> permissions = getPermissionsByUserName(userName);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }
 
    /**
     * 从数据库或缓存中获取权限数据(这里模拟数据库查询)
     * @param userName
     * @return
     */
    private Set<String> getPermissionsByUserName(String userName) {
        Set<String> sets = new HashSet<String>();
        sets.add("user:delete");
        sets.add("user:add");
        return sets;
    }
 
    /**
     * 通过用户名获取到角色数据(这里为了简单点就不真的去查询数据库了,仅模拟数据库查询)
     * @param userName
     * @return
     */
    private Set<String> getRolesByUserName(String userName) {
        Set<String> sets = new HashSet<String>();
        sets.add("admin");
        sets.add("user");
        return sets;
    }
 
    /**
     * 用来做认证(就是login时用到的)
     * @param authenticationToken           主体subject传过来的验证信息
     * @return
     * @throws AuthenticationException
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.先通过主体传过来的验证信息获取用户名
        String userName = (String) authenticationToken.getPrincipal();
        //2.通过用户名去数据库中获取凭证
        String password = getPasswordByUserName(userName);
        if(password == null) {
            return null;
        }
        //查询到用户,则返回AuthenticationInfo对象
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("xiehuaxin",password,"customRealm");
        return authenticationInfo;
    }
 
    private String getPasswordByUserName(String userName) {
        //这里我就不写查询数据库了,就模拟去查数据库
        return userMap.get(userName);
    }
}

测试自定义Realm

package com.imooc.test;
 
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
 
/**
 * @auther xiehuaxin
 * @create 2018-07-02 16:02
 * @todo
 */
public class CustomRealmTest {
 
    @Test
    public void testAutentication() {
 
        CustomRealm customRealm = new CustomRealm();
 
        //1.构建Security Manager环境(Security Manager是用来提供安全服务的,所以在做shiro认证的时候要先创建此对象,创建Security Manager对象之后要设置Realm)
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(customRealm);
 
        //2.获取向Security Manager提交请求的subject,而主体subject可以通过shiro提供的一个工具类SecurityUtils来获取
        SecurityUtils.setSecurityManager(defaultSecurityManager);//使用SecurityUtils之前要设置Security Manager环境
        Subject subject = SecurityUtils.getSubject();
        //3.主体Subject提交请求给Security Manager -->  subject.login(token);
        UsernamePasswordToken token = new UsernamePasswordToken("xiehuaxin","123456");//提交请求时需要一个token,所以要先创建token
        subject.login(token);
        //4. shiro提供了一个检查主体subject是否认证的方法isAuthenticated(),此方法的返回结果是一个boolean值
        System.out.println(subject.isAuthenticated());
 
        subject.checkRoles("admin");
        subject.checkPermission("user:delete");
 
    }
}

九、Shiro的加密

package com.imooc.test;
 
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
 
/**
 * @auther xiehuaxin
 * @create 2018-07-02 16:02
 * @todo
 */
public class CustomRealmTest {
 
    @Test
    public void testAutentication() {
 
        CustomRealm customRealm = new CustomRealm();
 
        //1.构建Security Manager环境(Security Manager是用来提供安全服务的,所以在做shiro认证的时候要先创建此对象,创建Security Manager对象之后要设置Realm)
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(customRealm);
 
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("md5");//设置加密算法名称
        matcher.setHashIterations(1);//设置加密的次数
        customRealm.setCredentialsMatcher(matcher);
 
        //2.获取向Security Manager提交请求的subject,而主体subject可以通过shiro提供的一个工具类SecurityUtils来获取
        SecurityUtils.setSecurityManager(defaultSecurityManager);//使用SecurityUtils之前要设置Security Manager环境
        Subject subject = SecurityUtils.getSubject();
        //3.主体Subject提交请求给Security Manager -->  subject.login(token);
        UsernamePasswordToken token = new UsernamePasswordToken("xiehuaxin","123456");//提交请求时需要一个token,所以要先创建token
        subject.login(token);
        //4. shiro提供了一个检查主体subject是否认证的方法isAuthenticated(),此方法的返回结果是一个boolean值
        System.out.println(subject.isAuthenticated());
 
        subject.checkRoles("admin");
        subject.checkPermission("user:delete");
 
    }
}
package com.imooc.test;
 
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;
 
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
 
/**
 * @auther xiehuaxin
 * @create 2018-07-02 15:48
 * @todo
 */
public class CustomRealm extends AuthorizingRealm {
 
    Map<String, String> userMap = new HashMap<String, String>(16);
    {
        userMap.put("xiehuaxin","123456");
        super.setName("customReal");//自定义
    }
 
    /**
     * 用来做授权(就是checkRole,checkPermission时用到的)
     * @param principalCollection
     * @return
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.先获取到用户名
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //2.从数据库或者缓存中获取角色数据
        Set<String> roles = getRolesByUserName(userName);
        //3.从数据库或缓存中获取权限数据
        Set<String> permissions = getPermissionsByUserName(userName);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }
 
    /**
     * 从数据库或缓存中获取权限数据(这里模拟数据库查询)
     * @param userName
     * @return
     */
    private Set<String> getPermissionsByUserName(String userName) {
        Set<String> sets = new HashSet<String>();
        sets.add("user:delete");
        sets.add("user:add");
        return sets;
    }
 
    /**
     * 通过用户名获取到角色数据(这里为了简单点就不真的去查询数据库了,仅模拟数据库查询)
     * @param userName
     * @return
     */
    private Set<String> getRolesByUserName(String userName) {
        Set<String> sets = new HashSet<String>();
        sets.add("admin");
        sets.add("user");
        return sets;
    }
 
    /**
     * 用来做认证(就是login时用到的)
     * @param authenticationToken           主体subject传过来的验证信息
     * @return
     * @throws AuthenticationException
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.先通过主体传过来的验证信息获取用户名
        String userName = (String) authenticationToken.getPrincipal();
        //2.通过用户名去数据库中获取凭证
        String password = getPasswordByUserName(userName);
        if(password == null) {
            return null;
        }
        //查询到用户,则返回AuthenticationInfo对象
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("xiehuaxin",password,"customRealm");
        authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("xie"));//这里要把盐也返回(如果密码做加密处理的时候也加了盐的话),真实项目中这里不该写死
        return authenticationInfo;
    }
 
    private String getPasswordByUserName(String userName) {
        //这里我就不写查询数据库了,就模拟去查数据库
        return userMap.get(userName);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值