Shiro入门--Hello Shiro

Apache Shiro是Apache的一个安全框架.对比Spring Security,可能没有Spring Security功能多,但是在实际并不需要那么重的东西.shiro简小精悍.大多项目绰绰有余.(JBOSS好像也有个什么安全框架…名字忘了,去JBOSS官网找了半天也没找到,找到个jboss sso好像是单点登录方面使用的安全框架)   

Shiro主要功能有认证,授权,加密,会话管理,与Web集成,缓存等.

1.shiro入门测试

新建一个简单的Maven项目,我们只是使用Junit和shiro-core包.POM最后是如下代码:

[xml]  view plain  copy
  1. <code><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  2.     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">  
  3.     <modelVersion>4.0.0</modelVersion>  
  4.     <groupId>org.credo</groupId>  
  5.     <artifactId>shiro-study</artifactId>  
  6.     <version>0.0.1-SNAPSHOT</version>  
  7.     <dependencies>  
  8.         <dependency>  
  9.             <groupId>junit</groupId>  
  10.             <artifactId>junit</artifactId>  
  11.             <version>4.9</version>  
  12.         </dependency>  
  13.         <dependency>  
  14.             <groupId>org.apache.shiro</groupId>  
  15.             <artifactId>shiro-core</artifactId>  
  16.             <version>1.2.3</version>  
  17.         </dependency>  
  18.     </dependencies>  
  19. </project></code>  




在src/test/java下建包,类TestHelloShiro.java .在src/test/resources下新建名为 shiro.ini 的文件.

[java]  view plain  copy
  1. <code>package org.credo.test;  
  2.   
  3. import junit.framework.Assert;  
  4.   
  5. import org.apache.shiro.SecurityUtils;  
  6. import org.apache.shiro.subject.Subject;  
  7. import org.apache.shiro.util.Factory;  
  8. import org.apache.shiro.authc.UsernamePasswordToken;  
  9. import org.apache.shiro.config.IniSecurityManagerFactory;  
  10. import org.apache.shiro.mgt.SecurityManager;  
  11. import org.junit.Test;  
  12.   
  13. public class TestHelloShiro {  
  14.   
  15.         public static final String DEFAULT_INI_RESOURCE_PATH = "classpath:shiro.ini";  
  16.   
  17.     @Test  
  18.     public void TestShiroFirst() {  
  19.         // 使用ini文件方式实例化shiro IniSecurityManagerFactory.  
  20.         IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory(DEFAULT_INI_RESOURCE_PATH<span></span>);  
  21.   
  22.         // 得到SecurityManager实例 并绑定给SecurityUtils  
  23.         SecurityManager securityManager = securityManagerFactory.getInstance();  
  24.         SecurityUtils.setSecurityManager(securityManager);  
  25.   
  26.         //得到Subject  
  27.         Subject shiroSubject = SecurityUtils.getSubject();  
  28.         //创建用户名/密码身份验证Token(即用户身份/凭证)  
  29.         UsernamePasswordToken normalToken = new UsernamePasswordToken("credo""123");  
  30.   
  31.         try {  
  32.             //登录,进行身份验证  
  33.             shiroSubject.login(normalToken);  
  34.         } catch (Exception e) {  
  35.             //登录失败,打印出错误信息,可自定义  
  36.             System.out.println(e.getMessage());  
  37.         }  
  38.         //断言登录成功  
  39.         Assert.assertEquals(true, shiroSubject.isAuthenticated());  
  40.         //登出  
  41.         shiroSubject.logout();  
  42.     }  
  43. }</code>  




shiro.ini文件通过[users]指定了两个user:credo/123、zhaoqian/123,:

?
1
2
3
[users] 
credo=123 
zhaoqian=123

知识点:

  1. shiro.ini–是shiro必须配置的一个重要文件.
  2. [users]:是shiro.ini配置里一个标注.作用就指定用户身份/凭证.
  3. IniSecurityManagerFactory就是Factory<SecurityManager>:通过new IniSecurityManagerFactory实例化的SecurityManager工厂,关于这个工厂下面有源码解释.


2.shiro处理流程的简单理解

从外部观察shiro,shiro的结构就是  外部代码—>Subject—->SecurityManager—->Realm

知识点:

  1. Subject:与外部代码交互的一层.应该理解为一个”用户”,但这个用户不一定是指传统意义的用户.应该理解为与我们当前系统交互的一个”对象”.
  2. SecurityManager:SecurityManager是整个shiro核心控制器,其控制所有的Subject,或者说所有的Subject的操作其实都是交给SecurityManager来处理.在一个应用中只有一个单例的SecurityManager实例存在,Apache Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
  3. Realm:Shiro需要Realm获取安全数据,如用户,角色,权限.Realm可以理解为”域”.简单的理解就是,Realm是像一个数据池,如果Shiro要验证一个当前系统对象的权限,密码,角色,那他就需要从Realm中获取对应的数据.

从此我们就可以理解shiro的处理流程.

  • 1.外部代码访问shiro,通过与Subject的交互来进行安全方面的操作,如授权,认证,资源的权限等.
  • 2.Subject相关的交互信息交由SecurityManager来处理.
  • 3.SecurityManager从Realm中获取对应的”数据”进行处理,返回给外部代码.

我们可以更进一步理解,Realm的数据是怎么来的?当然是我们自己定义的,也就是说,我们需要自己定义权限,角色,授权方面的数据资源(数据库存储或shiro.ini文件存储).


3.IniSecurityManagerFactory就是Factory<SecurityManager>源码解析

shiro的Factory<SecurityManager>是一个工厂模式的应用.我们追溯源码可以看到其内部的实现.

Factory最底层接口:org.apache.shiro.util.Factory.class

[java]  view plain  copy
  1. <code>package org.apache.shiro.util;  
  2.   
  3. //应用工厂设计模式的泛型接口  
  4. public interface Factory<T> {  
  5.     //返回一个实例  
  6.     T getInstance();  
  7. }</code>  




Factory接口声明的getInstance()方法,由其直接子类AbstractFactory实现。
之后AbstractFactory在实现的getInstance()方法中调用了一个新声明的抽象方法,这个方法也是由其直接子类实现的。
这样,从Factory开始,每个子类都实现父类声明的抽象方法,同时又声明一个新的抽象方法并在实现父类的方法中调用。

通过源码追溯,我们可以发现有2个类是实现了Factory接口:

  1. org.apache.shiro.jndi.JndiObjectFactory,泛型类.用于JNDI查找.
  2. 抽象类,我们需要关注的org.apache.shiro.util.AbstractFactory,就是abstract class AbstractFactory<T> implements Factory<T> .

接着是org.apache.shiro.config.IniFactorySupport,抽象类public abstract class IniFactorySupport<T> extends AbstractFactory<T> 
最终是package org.apache.shiro.config包下的IniSecurityManagerFactory.

IniSecurityManagerFactory类主要是用工厂模式创建基于Ini配置SecurityManager实例.

IniSecurityManagerFactory 是 Factory的子类,DefaultSecurityManager是 SecurityManager的子类。

Factory 与 SecurityManager 及其子类的关系


4.Shiro内部的认证流程

从上图可以看到整个Shiro的认证流程

1、首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils. setSecurityManager()设置;  


2、SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证;  


3、  
Authenticator 
才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;  


4、Authenticator可能会委托给相应的  
AuthenticationStrategy 
进行多Realm身份验证,默认  
ModularRealmAuthenticator 
会调用AuthenticationStrategy进行多Realm身份验证;  


5、Authenticator会把相应的token传入  
Realm 
,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。  
此处可以配置多个Realm,将按照相应的顺序及策略进行访问。 


5.Realm

Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。如我们之前的ini配置方式将使用org.apache.shiro.realm.text.IniRealm。

org.apache.shiro.realm.Realm接口如下:

?
1
2
3
String getName();//返回一个唯一的Realm名字 
booleansupports(AuthenticationToken token);//判断此Realm是否支持此Token 
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throwsAuthenticationException; //根据Token获取认证信息


A:单realm实现使用

1.我们先定义一个Realm.

[java]  view plain  copy
  1. <code>package org.credo.test.realm.single;  
  2.   
  3. import org.apache.shiro.authc.AuthenticationException;  
  4. import org.apache.shiro.authc.AuthenticationInfo;  
  5. import org.apache.shiro.authc.AuthenticationToken;  
  6. import org.apache.shiro.authc.IncorrectCredentialsException;  
  7. import org.apache.shiro.authc.SimpleAuthenticationInfo;  
  8. import org.apache.shiro.authc.UnknownAccountException;  
  9. import org.apache.shiro.authc.UsernamePasswordToken;  
  10. import org.apache.shiro.realm.Realm;  
  11.   
  12. public class TestMySingleRealm implements Realm{  
  13.   
  14.     @Override  
  15.     public String getName() {  
  16.         return "TestMySingleReam";  
  17.     }  
  18.   
  19.     @Override  
  20.     public boolean supports(AuthenticationToken token) {  
  21.         return token instanceof UsernamePasswordToken;  
  22.     }  
  23.   
  24.     @Override  
  25.     public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
  26.         String userName=String.valueOf(token.getPrincipal());  
  27.         //注意token的Credentials是char[],z主要转换.  
  28.         String passWord=String.valueOf((char[])token.getCredentials());  
  29.         if(!userName.equals("credo")){  
  30.             throw new UnknownAccountException("无效的账户名!");  
  31.         }  
  32.         if(!passWord.equals("aaa")){  
  33.             throw new IncorrectCredentialsException("密码错误!");  
  34.         }  
  35.         return new SimpleAuthenticationInfo(userName, passWord,getName());  
  36.     }  
  37. }</code>  




2.ini配置文件指定自定义Realm实现(文件名我定义为:shiro-single-realm.ini)

?
1
2
singleRealm=org.credo.test.realm.single.TestMySingleRealm
securityManager.realms=$singleRealm

通过$name来引入之前的realm定义

3.Junit测试代码

[java]  view plain  copy
  1. <code>@Test  
  2. public void testSingleMyRealm() {  
  3.     IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-single-realm.ini");  
  4.   
  5.     SecurityManager securityManager = securityManagerFactory.getInstance();  
  6.     SecurityUtils.setSecurityManager(securityManager);  
  7.   
  8.     Subject shiroSubject = SecurityUtils.getSubject();  
  9.     UsernamePasswordToken normalToken = new UsernamePasswordToken("credo""aaa");  
  10.   
  11.     try {  
  12.         shiroSubject.login(normalToken);  
  13.     } catch (UnknownAccountException e) {  
  14.         System.out.println(e.getMessage());  
  15.     } catch (IncorrectCredentialsException e) {  
  16.         System.out.println(e.getMessage());  
  17.     } catch (AuthenticationException e) {  
  18.         e.printStackTrace();  
  19.     }  
  20.   
  21.     Assert.assertEquals(true, shiroSubject.isAuthenticated());   
  22.     shiroSubject.logout();  
  23.           //解除绑定Subject到线程,防止对下次测试造成影响  
  24.           ThreadContext.unbindSubject();  
  25. }</code>  





B:多个Realms的使用

realm A:

[java]  view plain  copy
  1. <code>package org.credo.test.realm.multi;  
  2.   
  3. import org.apache.shiro.authc.AuthenticationException;  
  4. import org.apache.shiro.authc.AuthenticationInfo;  
  5. import org.apache.shiro.authc.AuthenticationToken;  
  6. import org.apache.shiro.authc.IncorrectCredentialsException;  
  7. import org.apache.shiro.authc.SimpleAuthenticationInfo;  
  8. import org.apache.shiro.authc.UnknownAccountException;  
  9. import org.apache.shiro.authc.UsernamePasswordToken;  
  10. import org.apache.shiro.realm.Realm;  
  11.   
  12.   
  13. public class RealmA implements Realm {  
  14.   
  15.     @Override  
  16.     public String getName() {  
  17.   
  18.         return "RealmA";  
  19.     }  
  20.   
  21.     @Override  
  22.     public boolean supports(AuthenticationToken token) {  
  23.         return token instanceof UsernamePasswordToken;  
  24.     }  
  25.   
  26.     @Override  
  27.     public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
  28.         String userName=String.valueOf(token.getPrincipal());  
  29.         //注意token的Credentials是char[],z主要转换.  
  30.         String passWord=String.valueOf((char[])token.getCredentials());  
  31.         System.out.println("realm A");  
  32.         if(!userName.equals("credo")){  
  33.             throw new UnknownAccountException("RealmA--无效的账户名!");  
  34.         }  
  35.         if(!passWord.equals("123")){  
  36.             throw new IncorrectCredentialsException("RealmA--密码错误!");  
  37.         }  
  38.         System.out.println("pass A");  
  39.         return new SimpleAuthenticationInfo(userName, passWord,getName());  
  40.     }  
  41.   
  42. }</code>  




realmB:

[java]  view plain  copy
  1. <code>package org.credo.test.realm.multi;  
  2.   
  3. import org.apache.shiro.authc.AuthenticationException;  
  4. import org.apache.shiro.authc.AuthenticationInfo;  
  5. import org.apache.shiro.authc.AuthenticationToken;  
  6. import org.apache.shiro.authc.IncorrectCredentialsException;  
  7. import org.apache.shiro.authc.SimpleAuthenticationInfo;  
  8. import org.apache.shiro.authc.UnknownAccountException;  
  9. import org.apache.shiro.authc.UsernamePasswordToken;  
  10. import org.apache.shiro.realm.Realm;  
  11.   
  12.   
  13. public class RealmB implements Realm {  
  14.   
  15.     @Override  
  16.     public String getName() {  
  17.   
  18.         return "RealmsB";  
  19.     }  
  20.   
  21.     @Override  
  22.     public boolean supports(AuthenticationToken token) {  
  23.         return token instanceof UsernamePasswordToken;  
  24.     }  
  25.   
  26.     @Override  
  27.     public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
  28.         String userName=String.valueOf(token.getPrincipal());  
  29.         //注意token的Credentials是char[],z主要转换.  
  30.         String passWord=String.valueOf((char[])token.getCredentials());  
  31.         System.out.println("realm B");  
  32.         if(!userName.equals("credo")){  
  33.             throw new UnknownAccountException("RealmB--无效的账户名!");  
  34.         }  
  35.         if(!passWord.equals("aaa")){  
  36.             throw new IncorrectCredentialsException("RealmB--密码错误!");  
  37.         }  
  38.         System.out.println("pass B");  
  39.         return new SimpleAuthenticationInfo(userName, passWord,getName());  
  40.     }  
  41.   
  42. }</code>  




shiro.ini配置(文件名:shiro-multi-realm.ini):

[java]  view plain  copy
  1. <code>@Test  
  2. public void testMultiMyRealm() {  
  3.     IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-multi-realm.ini");  
  4.   
  5.     SecurityManager securityManager = securityManagerFactory.getInstance();  
  6.     SecurityUtils.setSecurityManager(securityManager);  
  7.   
  8.     Subject shiroSubject = SecurityUtils.getSubject();  
  9.     UsernamePasswordToken normalToken = new UsernamePasswordToken("credo""aaa");  
  10.   
  11.     try {  
  12.         shiroSubject.login(normalToken);  
  13.     } catch (UnknownAccountException e) {  
  14.         System.out.println(e.getMessage());  
  15.     } catch (IncorrectCredentialsException e) {  
  16.         System.out.println(e.getMessage());  
  17.     } catch (AuthenticationException e) {  
  18.         System.out.println(e.getMessage());  
  19.     }  
  20.   
  21.     Assert.assertEquals(true, shiroSubject.isAuthenticated());   
  22.     shiroSubject.logout();  
  23.     ThreadContext.unbindSubject();  
  24. }</code>  




测试结果可以发现,只要其中一个realm通过就通过了.执行顺序是按shiro.ini中指定的顺序执行.先A后B.如果有realmC,realmD,但没有指定,不会执行.


6.Shiro默认提供的Realm

 



以后一般继承AuthorizingRealm(授权)即可;其继承了AuthenticatingRealm(即身份验证),而且也间接继承了CachingRealm(带有缓存实现)。其中主要默认实现如下:

  • org.apache.shiro.realm.text.IniRealm:[users]部分指定用户名/密码及其角色;[roles]部分指定角色即权限信息;
  • org.apache.shiro.realm.text.PropertiesRealm: user.username=password,role1,role2指定用户名/密码及其角色;role.role1=permission1,permission2指定角色及权限信息;
  • org.apache.shiro.realm.jdbc.JdbcRealm:通过sql查询相应的信息,
  1. 如“select password from users where username = ?”获取用户密码,
  2. “select password, password_salt from users where username = ?”获取用户密码及盐;
  3. “select role_name from user_roles where username = ?”获取用户角色;
  4. “select permission from roles_permissions where role_name = ?”获取角色对应的权限信息;
  5. 也可以调用相应的api进行自定义sql;


7.Authenticator及AuthenticationStrategy

Authenticator的职责是验证用户帐号,是Shiro API中身份验证核心的入口点:

[java]  view plain  copy
  1. <code>package org.apache.shiro.authc;  
  2.   
  3. public interface Authenticator {  
  4.   
  5.     /** 
  6.      * @throws AuthenticationException if there is any problem during the authentication process. 
  7.      *                                 See the specific exceptions listed below to as examples of what could happen 
  8.      *                                 in order to accurately handle these problems and to notify the user in an 
  9.      *                                 appropriate manner why the authentication attempt failed.  Realize an 
  10.      *                                 implementation of this interface may or may not throw those listed or may 
  11.      *                                 throw other AuthenticationExceptions, but the list shows the most common ones. 
  12.      * @see ExpiredCredentialsException 
  13.      * @see IncorrectCredentialsException 
  14.      * @see ExcessiveAttemptsException 
  15.      * @see LockedAccountException 
  16.      * @see ConcurrentAccessException 
  17.      * @see UnknownAccountException 
  18.      */  
  19.     public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)  
  20.             throws AuthenticationException;  
  21. }</code>  




如果验证成功,将返回AuthenticationInfo验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的AuthenticationException实现。  




SecurityManager接口继承了Authenticator,另外还有一个ModularRealmAuthenticator实现,其委托给多个Realm进行验证,验证规则通过AuthenticationStrategy接口指定,默认提供的实现:

  1. FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;
  2. AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,返回所有Realm身份验证成功的认证信息;
  3. AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。

ModularRealmAuthenticator默认使用AtLeastOneSuccessfulStrategy策略。

自定义AuthenticationStrategy实现,首先看其API

[java]  view plain  copy
  1. <code>//在所有Realm验证之前调用    
  2. AuthenticationInfo beforeAllAttempts(    
  3. Collection<? extends Realm> realms, AuthenticationToken token)     
  4. throws AuthenticationException;    
  5. //在每个Realm之前调用    
  6. AuthenticationInfo beforeAttempt(    
  7. Realm realm, AuthenticationToken token, AuthenticationInfo aggregate)     
  8. throws AuthenticationException;    
  9. //在每个Realm之后调用    
  10. AuthenticationInfo afterAttempt(    
  11. Realm realm, AuthenticationToken token,     
  12. AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)    
  13. throws AuthenticationException;    
  14. //在所有Realm之后调用    
  15. AuthenticationInfo afterAllAttempts(    
  16. AuthenticationToken token, AuthenticationInfo aggregate)     
  17. throws AuthenticationException;</code>  




因为每个AuthenticationStrategy实例都是无状态的,所有每次都通过接口将相应的认证信息传入下一次流程;通过如上接口可以进行如合并/返回第一个验证成功的认证信息。
自定义实现时一般继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy即可

测试案例:

修改shiro.ini

[java]  view plain  copy
  1. <code>authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator  
  2. securityManager.authenticator=$authenticator  
  3.   
  4. allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy  
  5. securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy  
  6.   
  7.   
  8. realmA=org.credo.test.realm.multi.RealmA  
  9. realmB=org.credo.test.realm.multi.RealmB  
  10.   
  11. securityManager.realms=$realmA,$realmB</code>  




Junit测试代码:

RealmB的getAuthenticationInfo方法返回值修改为:return new SimpleAuthenticationInfo(userName+”@qq.com”, passWord,getName());

其他不变,RealmA也不变.但验证过程用户名和密码都写正确的”credo”,”123”

[java]  view plain  copy
  1. <code>@Test  
  2.     public void testAuthenticator() {  
  3.         IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-multi-realm.ini");  
  4.   
  5.         SecurityManager securityManager = securityManagerFactory.getInstance();  
  6.         SecurityUtils.setSecurityManager(securityManager);  
  7.   
  8.         Subject shiroSubject = SecurityUtils.getSubject();  
  9.         UsernamePasswordToken normalToken = new UsernamePasswordToken("credo""aaa");  
  10.   
  11.         try {  
  12.             shiroSubject.login(normalToken);  
  13.         } catch (UnknownAccountException e) {  
  14.             System.out.println(e.getMessage());  
  15.         } catch (IncorrectCredentialsException e) {  
  16.             System.out.println(e.getMessage());  
  17.         } catch (AuthenticationException e) {  
  18.             System.out.println(e.getMessage());  
  19.         }  
  20.         // 得到一个PrincipalCollection,包含所有成功的.  
  21.         PrincipalCollection principalCollection = shiroSubject.getPrincipals();  
  22.         for(Object obj:principalCollection){  
  23.             System.out.println(obj.toString());  
  24.         }  
  25.         Assert.assertEquals(2, principalCollection.asList().size());  
  26.   
  27.         Assert.assertEquals(true, shiroSubject.isAuthenticated());  
  28.         shiroSubject.logout();  
  29.     }  
  30.   
  31.     @After  
  32.     public void tearDown() throws Exception {  
  33.         ThreadContext.unbindSubject();  
  34.     }</code>  




测试结果:

?
1
2
3
4
5
6
realm A
pass A
realm B
pass B
credo
credo@qq.com

包含credo和credo@qq.com,两个都通过了验证.都有两个信息.


转自https://blog.csdn.net/wudalang_gd/article/details/71512868

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值