JAVA操作LDAP总结

一、LDAP概念

LDAP的全称为Lightweight Directory Access Protocol(轻量级目录访问协议), 基于X.500标准, 支持 TCP/IP。

LDAP目录为数据库,通过LDAP服务器(相当于DBMS)处理查询和更新, 以树状的层次结构来存储数据,相对关系型数据库, LDAP主要是优化数据读取的性能,适用于比较少改变、跨平台的信息。

 

二、Softerra LDAP Administrator

         下载安装软件,并配置LDAP

                                

 

DN:Distinguished Name 唯一标识一条记录的路径,Base DN为基准DN,指定LDAP search的起始DN,即从哪个DN下开始搜索,RDN为叶子结点本身的名字。

DC:Domain Component 一条记录所属区域

OU:Organization Unit 组织单元

CN/UID:一条记录的名字/ID

 

三、在JAVA中应用LDAP

         1、spring配置文件

        <bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate">
          <constructor-arg ref="ldapcontextSource" />
    </bean>

         <bean id="ldapcontextSource" class="org.springframework.ldap.core.support.LdapContextSource">

                   <property name="url" value="${LdapConnHost}" /> <!—例: LdapConnHost=ldap://10.190.123.123 -->

                   <property name="userDn" value="${LdapConnUser}" /><!—例: LdapConnUser=cn=root -->

                   <property name="password" value="${LdapConnPwd}" /><!—例: LdapConnPwd=111111 -->

                   <property name="base" value="${LdapBaseDn}" /> <!—Base DN 例: LdapBaseDn=DC=FJTIC -->

                   <property name="pooled" value="false" />

         </bean>

 

        

         2、JAVA代码

  1 package com.test.dao;
  2 
  3 import java.beans.PropertyDescriptor;
  4 import java.lang.reflect.Field;
  5 import java.lang.reflect.InvocationTargetException;
  6 import java.lang.reflect.Method;
  7 import java.util.ArrayList;
  8 import java.util.List;
  9 import javax.naming.Name;
 10 import javax.naming.NamingEnumeration;
 11 import javax.naming.NamingException;
 12 import javax.naming.directory.Attribute;
 13 import javax.naming.directory.Attributes;
 14 import javax.naming.directory.BasicAttribute;
 15 import javax.naming.directory.BasicAttributes;
 16 import javax.naming.directory.ModificationItem;
 17 import org.apache.commons.beanutils.BeanUtils;
 18 import org.springframework.beans.factory.annotation.Autowired;
 19 import org.springframework.dao.EmptyResultDataAccessException;
 20 import org.springframework.ldap.core.ContextMapper;
 21 import org.springframework.ldap.core.DirContextAdapter;
 22 import org.springframework.ldap.core.DirContextOperations;
 23 import org.springframework.ldap.core.DistinguishedName;
 24 import org.springframework.ldap.core.LdapTemplate;
 25 import org.springframework.ldap.core.support.AbstractContextMapper;
 26 import org.springframework.ldap.filter.AndFilter;
 27 import org.springframework.ldap.filter.EqualsFilter;
 28 import org.springframework.ldap.filter.OrFilter;
 29 import org.springframework.stereotype.Component;
 30 import com.surekam.model.Person;
 31 import com.surekam.utils.DateCl;
 32 import com.surekam.utils.StringUtil;
 33 
 34 @Component
 35 public class UserDAOL {
 36     @Autowired
 37     private LdapTemplate ldapTemplate;
 38 
 39     /**
 40      *
 41      * @Description: 添加
 42      *
 43      */
 44     public boolean addPerson(Person person) {
 45         boolean flag = false;
 46         Name dn = buildUDn(person.getUid());
 47         Attributes buildAddAttributes = buildAddAttributes(person);
 48         ldapTemplate.bind(dn, null, buildAddAttributes);
 49         flag = true;
 50         return flag;
 51     }
 52 
 53     /**
 54      *
 55      * @Description: 修改
 56      *
 57      */
 58     public boolean updatePerson(Person person) {
 59         boolean flag = false;
 60         Name dn = buildUDn(person.getUid());
 61         ModificationItem[] modificationItem = buildModifyAttributes(person);
 62         ldapTemplate.modifyAttributes(dn, modificationItem);
 63         flag = true;
 64         return flag;
 65     }
 66 
 67     /**
 68      *
 69      * 多条件获取用户
 70      * 
 71      */
 72     public List<Person> getPersonByOu(String ou, String level) {
 73         AndFilter filter = new AndFilter();
 74         OrFilter orFilter1 = new OrFilter();
 75         if ("处级干部".equals(level)) {
 76             orFilter1.or(new EqualsFilter("cuadministrativelevels", "aa"));
 77             orFilter1.or(new EqualsFilter("cuadministrativelevels", "bb"));
 78             orFilter1.or(new EqualsFilter("cuadministrativelevels", "cc"));
 79             orFilter1.or(new EqualsFilter("cuadministrativelevels", "dd"));
 80         }
 81         if ("普通员工".equals(level)) {
 82             orFilter1.or(new EqualsFilter("cuadministrativelevels", "ee"));
 83             orFilter1.or(new EqualsFilter("cuadministrativelevels", "ff"));
 84             orFilter1.or(new EqualsFilter("cuadministrativelevels", "gg"));
 85         }
 86         OrFilter orFilter2 = new OrFilter();
 87         orFilter2.or(new EqualsFilter("departmentnumber", ou));
 88         orFilter2.or(new EqualsFilter("cutransferdnumber", ou));
 89         filter.and(orFilter2);
 90         filter.and(orFilter1);
 91         System.out.println(filter.toString());
 92         List<Person> list = ldapTemplate.search("cn=users,dc=hq", filter
 93                 .encode(), new PersonContextMapper());
 94         return list;
 95     }
 96 
 97 
 98     /**
 99      *
100      * 生成用户DN
101      * 
102      * @return uid=123455,cn=users,dc=hq
103      */
104     private Name buildUDn(String urdn) {
105         DistinguishedName dn = new DistinguishedName("");
106         dn.add("dc", "hq");
107         dn.add("cn", "users");
108         dn.add("uid", urdn);
109         return dn;
110     }
111 
112     /**
113      *
114      * 组织查询结果
115      * 
116      */
117     public static class PersonContextMapper implements ContextMapper {
118         public Object mapFromContext(Object ctx) {
119             if (ctx == null)
120                 return new Person();
121             DirContextAdapter context = (DirContextAdapter) ctx;
122             Person per = new Person();
123             Attributes attrs = context.getAttributes();
124             NamingEnumeration results = attrs.getAll();
125             Class c = per.getClass();
126             while (results.hasMoreElements()) {
127                 try {
128                     Attribute attr = (Attribute) results.next();
129                     String value = attr.get().toString();
130                     if (StringUtil.isNotEmpty(value)) {
131                         String fieldName = attr.getID();
132                         if ("objectclass".equals(fieldName.toLowerCase())) {
133                             continue;
134                         }
135                         Field field = c.getDeclaredField(fieldName
136                                 .toLowerCase());
137                         Class fieldClazz = field.getType();
138                         /*
139                          * 如果属性条数大于1,那就是多值属性 attr.getAttributeDefinition()
140                          * 获取多值属性的方法没找到
141                          */
142                         if (fieldClazz.isAssignableFrom(List.class)) { // 属性值数大于1
143                             NamingEnumeration values = attr.getAll(); // 获取所有值
144                             // LDAP中的多值属性只会是String类型
145                             List<String> list = new ArrayList<String>();
146                             while (values.hasMoreElements()) {
147                                 list.add(values.next().toString());
148                             }
149                             BeanUtils.setProperty(per, fieldName.toLowerCase(),
150                                     list);
151                         } else {
152                             // 普通属性
153                             BeanUtils.setProperty(per, fieldName.toLowerCase(),
154                                     value);
155                         }
156                     }
157                 } catch (IllegalAccessException e) {
158                     e.printStackTrace();
159                 } catch (InvocationTargetException e) {
160                     e.printStackTrace();
161                 } catch (NamingException e) {
162                     e.printStackTrace();
163                 } catch (SecurityException e) {
164                     // TODO Auto-generated catch block
165                     e.printStackTrace();
166                 } catch (NoSuchFieldException e) {
167                     // TODO Auto-generated catch block
168                     e.printStackTrace();
169                 }
170             }
171             per.setDn(context.getNameInNamespace());
172             return per;
173         }
174     }
175 
176     /**
177      *
178      * 组织添加数据数据
179      * 
180      */
181     @SuppressWarnings("unchecked")
182     private Attributes buildAddAttributes(Person p) {
183         Attributes attrs = new BasicAttributes();
184         BasicAttribute ocattr = new BasicAttribute("objectclass");
185         ocattr.add("top");
186         ocattr.add("person");
187         ocattr.add("organizationalPerson");
188         ocattr.add("inetOrgPerson");
189         ocattr.add("FJTicPerson");
190         attrs.put(ocattr);
191         Class c = p.getClass();
192         Field[] fields = c.getDeclaredFields();
193         for (int i = 0; i < fields.length; i++) {
194             try {
195                 Class fieldClazz = fields[i].getType();
196                 String fieldName = fields[i].getName(); // 获得属性名
197                 String fieldVlue = BeanUtils.getProperty(p, fieldName); // 获得属性值
198                 /*
199                  * 判断属性是否要过滤,例如修改时间之类的字段LDAP是没有的 判断属性值是否为空,在这里过滤了所有null和""
200                  * 增加操作中不存在主动设置某个值为空的情况 所以只需要处理有值属性
201                  */
202                 if (checkfieldName(fieldName) || StringUtil.isEmpty(fieldVlue))
203                     continue;
204                 /*
205                  * 多值属性的处理 如果多值属性为空,那么增加的时候就不会增加值进去
206                  */
207                 if (fieldClazz.isAssignableFrom(List.class)) { // 集合属性
208                     BasicAttribute ocattr1 = new BasicAttribute(fieldName);
209                     PropertyDescriptor pd = new PropertyDescriptor(fieldName, c);
210                     Method getMethod = pd.getReadMethod();// 获得get方法
211                     List list = (List) getMethod.invoke(p);// 执行get方法返回一个Object
212                     for (Object object : list) {
213                         ocattr1.add(object);
214                     }
215                     attrs.put(ocattr1);
216                 } else {
217                     attrs.put(fieldName, fieldVlue);
218                 }
219             } catch (Exception e) {
220                 e.printStackTrace();
221             }
222         }
223         return attrs;
224 
225     }
226 
227     /**
228      *
229      * 组织修改数据
230      * 
231      */
232     @SuppressWarnings("unchecked")
233     private ModificationItem[] buildModifyAttributes(Person p) {
234         ArrayList<ModificationItem> attrs = new ArrayList<ModificationItem>();
235         Class c = p.getClass();
236         Field[] fields = c.getDeclaredFields();
237         for (Field field : fields) {
238             try {
239                 Class fieldClazz = field.getType();
240                 String fieldName = field.getName(); // 获得属性名
241                 String fieldValue = BeanUtils.getProperty(p, fieldName);
242                 /*
243                  * 判断属性是否要过滤,例如修改时间之类的字段LDAP是没有的 判断属性值是否为空,在这里过滤了所有null和""
244                  * 要置空的属性通过识别特殊属性值:delAtr 在后面做重新置空操作
245                  */
246                 if (checkfieldName(fieldName) || StringUtil.isEmpty(fieldValue))
247                     continue;
248                 BasicAttribute basicAttr = new BasicAttribute(fieldName);
249                 /*
250                  * 多值属性的处理 如果传递一个空的list,那么修改的时候就会清空多值属性 (new ArrayList<String>())
251                  */
252                 if (fieldClazz.isAssignableFrom(List.class)) { // 如果是集合属性
253                     PropertyDescriptor pd = new PropertyDescriptor(fieldName, c);
254                     Method getMethod = pd.getReadMethod();// 获得get方法
255                     List list = (List) getMethod.invoke(p);// 执行get方法返回一个Object
256                     for (Object object : list) {
257                         basicAttr.add(object);
258                     }
259                 } else {
260                     /*
261                      * 判断删除标记来对值进行置空 传递过来的对象中有些属性没有做修改就传递了"" 有些是要修改为 ""
262                      * 所以在上面要过滤所有 "" 属性,避免将不修改的参数全都置空了 然后通过识别要修改参数的特有值来判断是否主动置空
263                      * 如果add一个""进去,那么在LDAP中依然会显示
264                      * 如果不给值,由BasicAttribute自动授予空值,那么在LDAP中就不显示了
265                      */
266                     if ("delAtr".equals(fieldValue)) {
267                         basicAttr.add(""); // 置空属性
268                     } else {
269                         basicAttr.add(fieldValue);// 有值属性
270                     }
271                 }
272                 // 替换条目
273                 attrs.add(new ModificationItem(
274                         DirContextAdapter.REPLACE_ATTRIBUTE, basicAttr));
275             } catch (Exception e) {
276                 e.printStackTrace();
277             }
278         }
279         return attrs.toArray(new ModificationItem[attrs.size()]);
280     }
281 
282     /**
283      *
284      * 过滤默认值字段
285      * 
286      */
287     private static boolean checkfieldName(String fieldName) {
288         String[] check = new String[] { "id", "status", "createtime",
289                 "updatetime", "dn" };
290         for (int i = 0; i < check.length; i++) {
291             if (check[i].equalsIgnoreCase(fieldName))
292                 return true;
293         }
294         return false;
295     }
296 }
spring版
  1 package com.smnpc.util;
  2 
  3 import java.util.Hashtable;
  4 import java.util.Vector;
  5 
  6 import javax.naming.Context;
  7 import javax.naming.NamingEnumeration;
  8 import javax.naming.NamingException;
  9 import javax.naming.directory.Attribute;
 10 import javax.naming.directory.Attributes;
 11 import javax.naming.directory.BasicAttribute;
 12 import javax.naming.directory.BasicAttributes;
 13 import javax.naming.directory.DirContext;
 14 import javax.naming.directory.InitialDirContext;
 15 import javax.naming.directory.ModificationItem;
 16 import javax.naming.directory.SearchControls;
 17 import javax.naming.directory.SearchResult;
 18 import javax.naming.ldap.LdapContext;
 19 
 20 /**
 21  * Java通过Ldap操作AD的增删该查询
 22  * 
 23  * @author guob
 24  */
 25 
 26 public class LdapbyUser {
 27     DirContext dc = null;
 28     String root = "dc=example,dc=com"; // LDAP的根节点的DC
 29 
 30     /**
 31      * 
 32      * @param dn类似于"CN=RyanHanson,dc=example,dc=com"
 33      * @param employeeID是Ad的一个员工号属性
 34      */
 35     public LdapbyUser(String dn, String employeeID) {
 36         init();
 37         // add();//添加节点
 38         // delete("ou=hi,dc=example,dc=com");//删除"ou=hi,dc=example,dc=com"节点
 39         // renameEntry("ou=new,o=neworganization,dc=example,dc=com","ou=neworganizationalUnit,o=neworganization,dc=example,dc=com");//重命名节点"ou=new,o=neworganization,dc=example,dc=com"
 40         // searchInformation("dc=example,dc=com", "",
 41         // "sAMAccountName=guob");//遍历所有根节点
 42         modifyInformation(dn, employeeID);// 修改
 43         // Ldapbyuserinfo("guob");//遍历指定节点的分节点
 44         close();
 45     }
 46 
 47     /**
 48      * 
 49      * Ldap连接
 50      * 
 51      * @return LdapContext
 52      */
 53     public void init() {
 54         Hashtable env = new Hashtable();
 55         String LDAP_URL = "ldap://xxxx:389"; // LDAP访问地址
 56         String adminName = "example\\user"; // 注意用户名的写法:domain\User或
 57         String adminPassword = "userpassword"; // 密码
 58         env.put(Context.INITIAL_CONTEXT_FACTORY,
 59                 "com.sun.jndi.ldap.LdapCtxFactory");
 60         env.put(Context.PROVIDER_URL, LDAP_URL);
 61         env.put(Context.SECURITY_AUTHENTICATION, "simple");
 62         env.put(Context.SECURITY_PRINCIPAL, adminName);
 63         env.put(Context.SECURITY_CREDENTIALS, adminPassword);
 64         try {
 65             dc = new InitialDirContext(env);// 初始化上下文
 66             System.out.println("认证成功");// 这里可以改成异常抛出。
 67         } catch (javax.naming.AuthenticationException e) {
 68             System.out.println("认证失败");
 69         } catch (Exception e) {
 70             System.out.println("认证出错:" + e);
 71         }
 72     }
 73 
 74     /**
 75      * 添加
 76      */
 77     public void add(String newUserName) {
 78         try {
 79             BasicAttributes attrs = new BasicAttributes();
 80             BasicAttribute objclassSet = new BasicAttribute("objectClass");
 81             objclassSet.add("sAMAccountName");
 82             objclassSet.add("employeeID");
 83             attrs.put(objclassSet);
 84             attrs.put("ou", newUserName);
 85             dc.createSubcontext("ou=" + newUserName + "," + root, attrs);
 86         } catch (Exception e) {
 87             e.printStackTrace();
 88             System.out.println("Exception in add():" + e);
 89         }
 90     }
 91 
 92     /**
 93      * 删除
 94      * 
 95      * @param dn
 96      */
 97     public void delete(String dn) {
 98         try {
 99             dc.destroySubcontext(dn);
100         } catch (Exception e) {
101             e.printStackTrace();
102             System.out.println("Exception in delete():" + e);
103         }
104     }
105 
106     /**
107      * 重命名节点
108      * 
109      * @param oldDN
110      * @param newDN
111      * @return
112      */
113     public boolean renameEntry(String oldDN, String newDN) {
114         try {
115             dc.rename(oldDN, newDN);
116             return true;
117         } catch (NamingException ne) {
118             System.err.println("Error: " + ne.getMessage());
119             return false;
120         }
121     }
122 
123     /**
124      * 修改
125      * 
126      * @return
127      */
128     public boolean modifyInformation(String dn, String employeeID) {
129         try {
130             System.out.println("updating...\n");
131             ModificationItem[] mods = new ModificationItem[1];
132             /* 修改属性 */
133             // Attribute attr0 = new BasicAttribute("employeeID", "W20110972");
134             // mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
135             // attr0);
136             /* 删除属性 */
137             // Attribute attr0 = new BasicAttribute("description",
138             // "陈轶");
139             // mods[0] = new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
140             // attr0);
141             /* 添加属性 */
142             Attribute attr0 = new BasicAttribute("employeeID", employeeID);
143             mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, attr0);
144             /* 修改属性 */
145             dc.modifyAttributes(dn + ",dc=example,dc=com", mods);
146             return true;
147         } catch (NamingException e) {
148             e.printStackTrace();
149             System.err.println("Error: " + e.getMessage());
150             return false;
151         }
152     }
153 
154     /**
155      * 关闭Ldap连接
156      */
157     public void close() {
158         if (dc != null) {
159             try {
160                 dc.close();
161             } catch (NamingException e) {
162                 System.out.println("NamingException in close():" + e);
163             }
164         }
165     }
166 
167     /**
168      * @param base
169      *            :根节点(在这里是"dc=example,dc=com")
170      * @param scope
171      *            :搜索范围,分为"base"(本节点),"one"(单层),""(遍历)
172      * @param filter
173      *            :指定子节点(格式为"(objectclass=*)",*是指全部,你也可以指定某一特定类型的树节点)
174      */
175     public void searchInformation(String base, String scope, String filter) {
176         SearchControls sc = new SearchControls();
177         if (scope.equals("base")) {
178             sc.setSearchScope(SearchControls.OBJECT_SCOPE);
179         } else if (scope.equals("one")) {
180             sc.setSearchScope(SearchControls.ONELEVEL_SCOPE);
181         } else {
182             sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
183         }
184         NamingEnumeration ne = null;
185         try {
186             ne = dc.search(base, filter, sc);
187             // Use the NamingEnumeration object to cycle through
188             // the result set.
189             while (ne.hasMore()) {
190                 System.out.println();
191                 SearchResult sr = (SearchResult) ne.next();
192                 String name = sr.getName();
193                 if (base != null && !base.equals("")) {
194                     System.out.println("entry: " + name + "," + base);
195                 } else {
196                     System.out.println("entry: " + name);
197                 }
198 
199                 Attributes at = sr.getAttributes();
200                 NamingEnumeration ane = at.getAll();
201                 while (ane.hasMore()) {
202                     Attribute attr = (Attribute) ane.next();
203                     String attrType = attr.getID();
204                     NamingEnumeration values = attr.getAll();
205                     Vector vals = new Vector();
206                     // Another NamingEnumeration object, this time
207                     // to iterate through attribute values.
208                     while (values.hasMore()) {
209                         Object oneVal = values.nextElement();
210                         if (oneVal instanceof String) {
211                             System.out.println(attrType + ": "
212                                     + (String) oneVal);
213                         } else {
214                             System.out.println(attrType + ": "
215                                     + new String((byte[]) oneVal));
216                         }
217                     }
218                 }
219             }
220         } catch (Exception nex) {
221             System.err.println("Error: " + nex.getMessage());
222             nex.printStackTrace();
223         }
224     }
225 
226     /**
227      * 查询
228      * 
229      * @throws NamingException
230      */
231     public void Ldapbyuserinfo(String userName) {
232         // Create the search controls
233         SearchControls searchCtls = new SearchControls();
234         // Specify the search scope
235         searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
236         // specify the LDAP search filter
237         String searchFilter = "sAMAccountName=" + userName;
238         // Specify the Base for the search 搜索域节点
239         String searchBase = "DC=example,DC=COM";
240         int totalResults = 0;
241         String returnedAtts[] = { "url", "whenChanged", "employeeID", "name",
242                 "userPrincipalName", "physicalDeliveryOfficeName",
243                 "departmentNumber", "telephoneNumber", "homePhone", "mobile",
244                 "department", "sAMAccountName", "whenChanged", "mail" }; // 定制返回属性
245 
246         searchCtls.setReturningAttributes(returnedAtts); // 设置返回属性集
247 
248         // searchCtls.setReturningAttributes(null); // 不定制属性,将返回所有的属性集
249 
250         try {
251             NamingEnumeration answer = dc.search(searchBase, searchFilter,
252                     searchCtls);
253             if (answer == null || answer.equals(null)) {
254                 System.out.println("answer is null");
255             } else {
256                 System.out.println("answer not null");
257             }
258             while (answer.hasMoreElements()) {
259                 SearchResult sr = (SearchResult) answer.next();
260                 System.out
261                         .println("************************************************");
262                 System.out.println("getname=" + sr.getName());
263                 Attributes Attrs = sr.getAttributes();
264                 if (Attrs != null) {
265                     try {
266 
267                         for (NamingEnumeration ne = Attrs.getAll(); ne
268                                 .hasMore();) {
269                             Attribute Attr = (Attribute) ne.next();
270                             System.out.println("AttributeID="
271                                     + Attr.getID().toString());
272                             // 读取属性值
273                             for (NamingEnumeration e = Attr.getAll(); e
274                                     .hasMore(); totalResults++) {
275                                 String user = e.next().toString(); // 接受循环遍历读取的userPrincipalName用户属性
276                                 System.out.println(user);
277                             }
278                             // System.out.println(" ---------------");
279                             // // 读取属性值
280                             // Enumeration values = Attr.getAll();
281                             // if (values != null) { // 迭代
282                             // while (values.hasMoreElements()) {
283                             // System.out.println(" 2AttributeValues="
284                             // + values.nextElement());
285                             // }
286                             // }
287                             // System.out.println(" ---------------");
288                         }
289                     } catch (NamingException e) {
290                         System.err.println("Throw Exception : " + e);
291                     }
292                 }
293             }
294             System.out.println("Number: " + totalResults);
295         } catch (Exception e) {
296             e.printStackTrace();
297             System.err.println("Throw Exception : " + e);
298         }
299     }
300 
301     /**
302      * 主函数用于测试
303      * 
304      * @param args
305      */
306     public static void main(String[] args) {
307         new LdapbyUser("CN=RyanHanson", "bbs.it-home.org");
308     }
309 }
普通实现(转载)

 

转载于:https://www.cnblogs.com/Nadim/p/4681003.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值