最近因工作需要自己花时间学习了ldap的API并在项目中使用了,其中ldap开发有自己的API,现在的java自带的API也包含了相关的API
ldap自身的API:https://www.novell.com/documentation/developer/jldap/jldapenu/api/
jdk自带的api在javax.naming.*包下面
其中openldap有些比较坑的地方,页面上创建Entry和修改Entry的时候的字段名不一致,然后使用java开发的时候又不一致
其中添加用户的时候需要添加属性(objectClass=posixAccount)
添加组的时候需要添加属性(objectClass=posixGroup)
其中在使用java添加的时候javaAPI中需要添加的属性与openldap页面添加的属性名对应关系有
User Name 对应属性 uid
Password 对应属性 userPassword
如果不一致会报如下错误error code 17 - User Name :AttributeDescription contains in appropriate characters
添加组有gid,添加用户有uid,通过页面添加的时候可以发现他们的id应该是自增的,但是你查出来之后,设置属性必须使用字符串设置进去,否则会报Malformed gidNumber 错误,当然这个属性名也是页面上的
下面是通过javaAPI写的示例
package com.java.ldap; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.Hashtable; import java.util.List; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.BasicAttribute; import javax.naming.directory.BasicAttributes; import javax.naming.directory.DirContext; import javax.naming.directory.ModificationItem; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.LdapContext; public class LdapTest { private static final String BASE_DN = "dc=csair,dc=com"; private static final String USER_DN_BASE = "ou=users,dc=csair,dc=com"; private static final String GROUP_DN_BASE = "ou=group,dc=csair,dc=com"; public static void main(String[] args) throws Exception { LdapContext connectLdap = connectLdap(); getMaxId(connectLdap); SearchControls searchCtls = new SearchControls(); searchCtls.setSearchScope(SearchControls.OBJECT_SCOPE); NamingEnumeration<SearchResult> search = connectLdap.search("cn=athenatest111,ou=group,dc=abc,dc=com", "(objectClass=posixGroup)", null, searchCtls); // NamingEnumeration<SearchResult> search = connectLdap.search("cn=caifan,ou=users,dc=abc,dc=com", "(objectClass=posixAccount)", null,searchCtls); while (search.hasMore()) { SearchResult result = search.next(); //System.out.println(result.getAttributes().get("memberUid").get(2));//.get("description") System.out.println(result.getAttributes().get("gidnumber").get());//.get("description") System.out.println(result.getAttributes().get("objectClass")); System.out.println(result.getName()); } getAllGroups(connectLdap); } public static void getAllGroups(LdapContext context) throws Exception { SearchControls searchCtls = new SearchControls(); searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); //SHIRO-115 - prevent potential code injection: String searchFilter = "(&(objectClass=posixGroup))"; NamingEnumeration answer = context.search("ou=group,dc=abc,dc=com", searchFilter, null, searchCtls); while (answer.hasMoreElements()) { SearchResult searchResult = (SearchResult) answer.next(); String group = searchResult.getName().substring(3); //组名 String groupName = ""; Attribute groupNameAttr = searchResult.getAttributes().get("description"); System.out.println(group + "::" + groupNameAttr + "::" + searchResult.getAttributes().get("memberUid")); //组成员 List<String> userIds = new ArrayList<>(); Attribute memberUidAttr = searchResult.getAttributes().get("memberUid"); if (memberUidAttr != null) { NamingEnumeration memberUid = memberUidAttr.getAll(); while (memberUid.hasMore()) { String userId = (String) memberUid.next(); userIds.add(userId); } } } } public static Integer getMaxId(LdapContext context) throws Exception { SearchControls searchCtls = new SearchControls(); searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); List<Integer> idList = new ArrayList<>(); //NamingEnumeration<SearchResult> search = context.search("ou=group,dc=abc,dc=com", "(objectClass=posixGroup)", null,searchCtls); NamingEnumeration<SearchResult> search = context.search("ou=users,dc=abc,dc=com", "(objectClass=posixAccount)", null,searchCtls); while (search.hasMore()) { SearchResult result = search.next(); //System.out.println(result.getAttributes().get("memberUid").get(2));//.get("description") //System.out.println(result.getAttributes().get("gidnumber").get());//.get("description") idList.add(Integer.parseInt(result.getAttributes().get("uidnumber").get().toString())); //System.out.println(result.getAttributes().get("uidnumber").get()); } Collections.sort(idList); for(Integer id : idList) { System.out.println(id); } System.out.println("maxID:" + idList.get(idList.size() -1)); return idList.get(0); } public static LdapContext connectLdap() throws Exception { // 连接Ldap需要的信息 String ldapFactory = "com.sun.jndi.ldap.LdapCtxFactory"; String ldapUrl = "ldap://ip:389";// url String ldapAccount = "cn=admin,dc=abc,dc=com"; // 用户名 String ldapPwd = "password";//密码 Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, ldapFactory); // LDAP server env.put(Context.PROVIDER_URL, ldapUrl); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, ldapAccount); env.put(Context.SECURITY_CREDENTIALS, ldapPwd); env.put("java.naming.referral", "follow"); LdapContext ctxTDS = new InitialLdapContext(env, null); return ctxTDS; } public void addUser(String cn, String givenName, String password, List<String> group) throws NamingException { LdapContext ldapContext = null; try { ldapContext = connectLdap(); Integer uidNumber = getMaxId(ldapContext, USER_DN_BASE) + 1; String md5 = generateMD5(password); Attributes attributes = new BasicAttributes(); Attribute passwordAttribute = new BasicAttribute("userPassword", md5); Attribute objectClass = new BasicAttribute("objectClass", true); objectClass.add("inetOrgPerson"); objectClass.add("posixAccount"); objectClass.add("top"); Attribute cnAttr = new BasicAttribute("cn",cn); Attribute givenNameAttr = new BasicAttribute("givenName",givenName); Attribute homeDirectoryAttr = new BasicAttribute("homeDirectory", "/home/users/" + cn); Attribute loginShellAttr = new BasicAttribute("loginShell", "/bin/sh"); Attribute snAttr = new BasicAttribute("sn", cn); Attribute uidAttr = new BasicAttribute("uid", cn); Attribute uidNumberAttr = new BasicAttribute("uidNumber", uidNumber + ""); SearchControls searchCtls = new SearchControls(); searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); if(group.size() > 0) { NamingEnumeration<SearchResult> searchGroup = ldapContext.search("cn=" + group.get(0) + "," + GROUP_DN_BASE, "(objectClass=posixGroup)", null, searchCtls); while (searchGroup.hasMore()) { String gid = searchGroup.next().getAttributes().get("gidnumber").get().toString(); Attribute gidAttr = new BasicAttribute("gidNumber", gid); attributes.put(gidAttr); } //添加用户到组 group.forEach(groupName -> { try { moveUser2Group(cn, groupName, 0); } catch (Exception e) { } }); } attributes.put(cnAttr); attributes.put(givenNameAttr); attributes.put(passwordAttribute); attributes.put(objectClass); attributes.put(loginShellAttr); attributes.put(snAttr); attributes.put(homeDirectoryAttr); attributes.put(uidAttr); attributes.put(uidNumberAttr); ldapContext.createSubcontext("cn=" + cn + "," + USER_DN_BASE, attributes); } catch (Exception e) { e.printStackTrace(); } finally { if(ldapContext != null) { ldapContext.close(); } } } /** * * 其中givenName sn UserName在修改的时候可以添加成多个值,添加的时候是通过其他值进行转换的,其中User Name sn在修改的时候最后至少保留一个,否则报错 * @param cn 用户唯一标识 * @param givenName 添加的givenName属性 * @param addGroup 添加的组 * @param subGroup 减少的组 */ public void modifyUser(String cn, String givenName, List<String> addGroup, List<String> subGroup) { LdapContext ldapContext = null; try { ldapContext = connectLdap(); Attributes attributes = new BasicAttributes(); SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.OBJECT_SCOPE); NamingEnumeration<SearchResult> search = ldapContext.search("cn=" + cn + "," + USER_DN_BASE, "(objectClass=posixAccount)", null, searchControls); while (search.hasMore()) { SearchResult searchResult = search.next(); Attributes beforeAttrs = searchResult.getAttributes(); Attribute givenNameAttrs = beforeAttrs.get("givenName"); if(givenNameAttrs != null && givenNameAttrs.get() != null && !givenNameAttrs.get().toString().trim().equals(givenName.trim())) { givenNameAttrs.clear(); givenNameAttrs.add(givenName); attributes.put(givenNameAttrs); } else { givenNameAttrs = new BasicAttribute("givenName", givenName); attributes.put(givenNameAttrs); } Attribute uidAttrs = beforeAttrs.get("uid"); if(addGroup != null && !addGroup.isEmpty()) { if(addGroup.get(0).trim().length() > 0) { NamingEnumeration<SearchResult> searchResults = ldapContext.search("cn=" + addGroup.get(0) + "," + GROUP_DN_BASE, "(objectClass=posixGroup)", null, searchControls); Attribute gidAttribute = null; while (searchResults.hasMore()) { SearchResult sr = searchResults.next(); gidAttribute = sr.getAttributes().get("gidNumber"); attributes.put(new BasicAttribute("gidNumber", gidAttribute.get())); } addGroup.forEach(add -> { try { if (add.trim().length() > 0) { moveUser2Group(cn, add, 0); } } catch (Exception e) { } }); } } if (subGroup != null && subGroup.size() > 0) { subGroup.forEach(sub -> { try { if(sub.trim().length() > 0) { moveUser2Group(cn, sub, 1); } } catch (Exception e) { } }); } ldapContext.modifyAttributes("cn=" + cn + "," + USER_DN_BASE, DirContext.REPLACE_ATTRIBUTE, attributes); } } catch (Exception e) { } finally { if(ldapContext != null) { try { ldapContext.close(); } catch (NamingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } private void modifyList(Attribute attrs, List<String> addValue, List<String> subValue) { if(attrs != null ) { if(addValue != null && addValue.size() > 0) { for (String add : addValue) { if (!attrs.contains(add)) { attrs.add(add); } } } if(subValue != null && subValue.size() > 0) { for (String sub : subValue) { if (attrs.contains(sub)) { attrs.remove(sub); } } } } } public void deleteUser(String username) throws Exception { LdapContext context = null; try { context = connectLdap(); context.destroySubcontext("cn=" + username + "," + USER_DN_BASE); } catch (NamingException e) { } finally { if(context != null) { context.close();; } } } /** * 添加用户到指定的组 * @param userId * @param groupName * @param type 0:添加 1:移除 * @throws Exception */ private void moveUser2Group (String userId, String groupName, Integer type) throws Exception { LdapContext context = connectLdap(); String searchFilter = "(&(objectClass=posixGroup))"; SearchControls searchCtrl = new SearchControls(); searchCtrl.setSearchScope(SearchControls.OBJECT_SCOPE); NamingEnumeration answer = context.search("cn=" + groupName + "," + GROUP_DN_BASE, searchFilter, null, searchCtrl); SearchResult searchResult; while (answer.hasMore()) { searchResult = (SearchResult) answer.next(); Attribute attribute = searchResult.getAttributes().get("memberUid"); if(attribute == null) { attribute = new BasicAttribute("memberUid"); } if (type == 0 && !attribute.contains(userId)) { attribute.add(attribute.size(), userId); } else if(type == 1 && attribute.contains(userId)) { attribute.remove(userId); } ModificationItem[] item = new ModificationItem[1]; item[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute); context.modifyAttributes("cn=" + groupName + "," + GROUP_DN_BASE, item); } } private Integer getMaxId(LdapContext context, String dn) throws Exception { SearchControls searchCtls = new SearchControls(); searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); List<Integer> idList = new ArrayList<>(); NamingEnumeration<SearchResult> search = null; if(GROUP_DN_BASE.equals(dn)) { search = context.search(GROUP_DN_BASE, "(objectClass=posixGroup)", null,searchCtls); while (search.hasMore()) { SearchResult result = search.next(); idList.add(Integer.parseInt(result.getAttributes().get("gidnumber").get().toString())); } } else if(USER_DN_BASE.equals(dn)) { search = context.search(USER_DN_BASE, "(objectClass=posixAccount)", null,searchCtls); while (search.hasMore()) { SearchResult result = search.next(); idList.add(Integer.parseInt(result.getAttributes().get("uidnumber").get().toString())); } } Collections.sort(idList); return idList.get(idList.size() -1); } public static String generateMD5(String password) { //声明StringBuffer对象来存放最后的值 StringBuffer sb=new StringBuffer(); try { //1.初始化MessageDigest信息摘要对象,并指定为MD5不分大小写都可以 MessageDigest md=MessageDigest.getInstance("md5"); //2.传入需要计算的字符串更新摘要信息,传入的为字节数组byte[], //将字符串转换为字节数组使用getBytes()方法完成 //指定时其字符编码 为utf-8 md.update(password.getBytes("utf-8")); //3.计算信息摘要digest()方法 //返回值为字节数组 byte [] hashCode=md.digest(); //4.将byte[] 转换为找度为32位的16进制字符串 //遍历字节数组 for(byte b:hashCode){ //对数组内容转化为16进制, sb.append(Character.forDigit(b>>4&0xf, 16)); //换2次为32位的16进制 sb.append(Character.forDigit(b&0xf, 16)); } } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return sb.toString(); } }
后面通过ldapAPI写示例
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import com.novell.ldap.util.DN; /** * 通过LDAPConnection获取词条 * @author admin * */ public class LdapUtils { private static String ldapHost = "IP"; private static int ldapPort = 389; private static String ldapBindDN = "cn=admin,dc=abc,dc=com"; private static String ldapPassword = "password"; private static LDAPConnection connection = null; public static void main(String[] args) throws Exception { DN dn = new DN(ldapBindDN); int countResults = 0; openConnection(); //LDAPSearchResults results = connection.search(dn.getParent().toString(), LDAPConnection.SCOPE_BASE, "objectClass=*", null, false);//获取当前DN级别LDAPEntry //LDAPSearchResults results1 = connection.search(dn.getParent().toString(), LDAPConnection.SCOPE_ONE, "objectClass=*", null, false);//获取当前DN级别的子集LDAPEntry LDAPSearchResults results2 = connection.search(dn.getParent().toString(), LDAPConnection.SCOPE_SUB, "objectClass=*", null, false);//获取所有DN级别的子集 System.out.println("===results1======"); while(results2.hasMore()) { System.out.println(results2.next()); countResults++; } System.out.println(countResults); connection.disconnect(); } /** * 添加LDAPEntry * @param connection * @param dn * @param attrList * @throws LDAPException */ public void addLDAPEntry(LDAPConnection connection, String dn, List<LDAPAttribute> attrList) throws LDAPException { LDAPAttributeSet ldapAttributeSet = new LDAPAttributeSet(); ldapAttributeSet.addAll(attrList); LDAPEntry ldapEntry = new LDAPEntry(dn, ldapAttributeSet); connection.add(ldapEntry); connection.disconnect(); } /** * 修改LDAP属性 * @param connection * @param dn * @param attrList * @param type LDAPModification。REPLACE DELETE * @throws LDAPException */ public static void modifyLDAPEntryAttr(LDAPConnection connection, String dn, List<LDAPAttribute> attrList, int type) throws LDAPException { LDAPModification ldapModification; if(attrList != null && attrList.size() > 0) { for(LDAPAttribute attr : attrList) { ldapModification = new LDAPModification(type, attr); connection.modify(dn, ldapModification); } } } /** * 连接LDAP */ public static void openConnection() { if (connection == null) { try { connection = new LDAPConnection(); connection.connect(ldapHost, ldapPort); connection.bind(LDAPConnection.LDAP_V3, ldapBindDN, ldapPassword.getBytes("UTF8")); } catch (Exception e) { System.out.println("连接LDAP出现错误:\n" + e.getMessage()); System.exit(1); } } } /** * 根据查询scope 获取所有的子LDAPEntry scope:LDAPConnection.SCOPE_BASE(查询当前级LdapEnttry) LDAPConnection.SCOPE_ONE(查询当前子集LDAPEntry) LDAPConnection.SCOPE_SUB(查询所有的子集LDAPEntry,如有子目录则递归) * @param dn * @param connection * @param searchFilter 搜索条件 通常为"objectClass=*" * @param attrs 属性 * @return * @throws LDAPException */ public static Map<String, Object> getLDAPEntry(String dn, LDAPConnection connection, int scope, String searchFilter, String[] attrs) throws LDAPException { Map<String, Object> resultsMap = new HashMap<>(); List<LDAPEntry> entryList = new ArrayList<>(); int resultsCounts = 0; LDAPSearchResults ldapSearchResults = connection.search(dn, scope, searchFilter, attrs, false); LDAPEntry ldapEntry; while(ldapSearchResults.hasMore()) { ldapEntry = ldapSearchResults.next(); entryList.add(ldapEntry); resultsCounts++; } resultsMap.put("ldapEntryCounts", resultsCounts); resultsMap.put("ldapEntryList", entryList); return resultsMap; } /** * 根据连接和DN获取LDAPEntry * @param connection * @param ldapBindDN * @return * @throws Exception */ public static LDAPEntry getLDAPEntry(LDAPConnection connection, String ldapBindDN) throws Exception { return connection.read(ldapBindDN); } /** * @param connection * @param userDN * @param groupDN * @return * 添加用户到组 * @throws LDAPException */ public static boolean addUser2Group(LDAPConnection connection, String userDN, String groupDN, List<LDAPAttribute> userAttributes, List<LDAPAttribute> groupAtrributes) { try { modifyLDAPEntryAttr(connection,userDN, userAttributes, LDAPModification.ADD); } catch (LDAPException e) { try { modifyLDAPEntryAttr(connection,userDN, userAttributes, LDAPModification.DELETE); } catch (LDAPException e1) { e1.printStackTrace(); } System.out.println(e.getMessage()); return false; } try { modifyLDAPEntryAttr(connection,userDN, userAttributes, LDAPModification.ADD); } catch (LDAPException e) { try { modifyLDAPEntryAttr(connection,userDN, userAttributes, LDAPModification.DELETE); } catch (LDAPException e1) { e1.printStackTrace(); } System.out.println(e.getMessage()); return false; } return true; } }
有疑问欢迎加入:513650703群聊,有更好的意欢迎提出分享。
相关推荐
Java LDAP+CAS单点登录是一种常见的企业级身份验证和授权解决方案。这个技术组合允许用户只需登录一次,就可以访问多个相互独立的应用系统,提高了用户体验并增强了安全性。以下是对这个主题的详细解释: **LDAP...
Java连接LDAP(Lightweight Directory Access Protocol)是一种常见的任务,用于在分布式环境中管理和访问目录服务信息。这个主题涉及几个关键知识点,包括Java LDAP API、SSL安全连接以及如何通过代码操作LDAP目录...
Java LDAP(轻量级目录访问协议)操作是Java开发者在处理目录服务时常见的任务,尤其在需要进行身份验证、用户管理或企业应用集成时。...结合`LDAPTest`代码,你可以更深入地学习如何在Java中有效地与LDAP服务器交互。
Novell LDAP开发包是专为Java开发者设计的一个工具集,用于与 Lightweight Directory Access Protocol (LDAP) 服务进行交互。LDAP是一种开放的标准协议,用于存储和检索分布式目录信息,广泛应用于企业身份验证、...
Java操作LDAP(Lightweight Directory Access Protocol)是一种常见的方式,用于在分布式环境中管理和访问目录信息。在Java中,我们可以使用各种库来实现与LDAP服务器的交互,这些库提供了丰富的API,使得开发人员...
Java操作LDAP(轻量级目录访问协议)是企业级应用中常见的任务,特别是在管理AD(活动目录)域用户时。本项目提供了一种便捷的方式,允许开发者在Java环境中创建、管理和更新AD域中的用户信息,并且可以直接在...
在企业级应用开发中,LDAP(Lightweight Directory Access Protocol,轻量目录访问协议)是一种常见的用于管理组织机构中的用户账户信息的标准协议。通过LDAP,开发者可以方便地对用户进行认证、授权等操作。本文将...
针对标题"ldap常用类封装下载"和描述"ibm,novell,sun等大厂商ldap服务器,使用本包可以方便的操作和维护以及查询",我们可以理解这个压缩包提供了一些预封装的Java类,便于开发者更高效地与LDAP服务器进行交互。...
3. **UnboundID LDAP SDK**:这是一套全面的Java LDAP开发工具包,提供了丰富的类和方法,便于开发人员构建、测试和调试LDAP应用。它支持异步操作,性能优化,并包含许多实用工具。 4. **OpenLDAP软件**:虽然不是...
- **开发者**:在开发过程中,测试和调试LDAP相关的代码时,该工具可作为辅助调试工具。 - **安全专家**:进行安全性审查,确保目录服务符合安全策略和合规性要求。 综上所述,`ldapbrowser`是一个强大的、基于Java...
本资源提供的“LDAP类库”是为了简化开发人员对LDAP的操作,使其能够更加高效、便捷地与LDAP服务器进行交互。类库通常包含了各种预定义的方法和函数,用于执行常见的LDAP操作,如搜索、添加、删除、修改条目,以及...
标题中的"ldap.jar.zip"表明这是一个包含ldap.jar文件的压缩包,主要用于Java开发环境中。...总之,"ldap.jar.zip"是一个包含Java LDAP实现的压缩包,对于需要与LDAP服务器交互的Java应用来说,是一个重要的开发资源。
Java和SpringBoot是开发此类解决方案的常用工具,因为它们提供了强大的支持来集成 LDAP 与 AD 域服务。下面我们将深入探讨如何使用Java和SpringBoot实现基于LDAP的AD域账号验证。 首先,我们需要了解LDAP的基本结构...
开发人员可以使用各种编程语言的 LDAP 库,如Java的JNDI,Python的ldap3库,或.NET Framework的System.DirectoryServices组件,来与LDAP服务器交互。 **八、LDAP培训内容** 一个完整的LDAP应用开发培训可能包括: ...
Tcp服务端与客户端的JAVA实例源代码 2个目标文件 摘要:Java源码,文件操作,TCP,服务器 Tcp服务端与客户端的JAVA实例源代码,一个简单的Java TCP服务器端程序,别外还有一个客户端的程序,两者互相配合可以开发出超多...
Spring LDAP为开发人员提供了高级抽象层,简化了与LDAP服务器的交互过程。通过使用Spring LDAP,开发人员可以避免直接处理复杂的LDAP API,而是通过更简洁、面向对象的方式进行数据的检索、更新和管理。 ### 关键...
在IT行业中,尤其是在企业级应用开发中,与Active Directory(AD)域进行交互是一项常见的任务。AD是一个由微软提供的服务,用于集中管理网络资源,包括用户账户、密码、权限等。Java开发人员经常使用Java Naming ...
1. **用户认证与授权**: Spring-Ldap 可用于实现基于 LDAP 的用户身份验证和权限管理,这在大型企业或组织中非常常见。 2. **数据存储**: 对于需要持久化 LDAP 数据的应用,Spring-Ldap 提供了方便的数据存取接口。...
C#和Java是两种常用的语言,它们都有库和API支持与LDAP服务器进行交互。** 在C#中,我们可以使用`System.DirectoryServices`命名空间来连接到LDAP服务器。这个命名空间提供了`DirectoryEntry`类,它是与目录服务...
《com.sun.jndi.ldap.jar:Maven...对于需要与LDAP服务器交互的Java应用来说是至关重要的。虽然在Maven中央仓库中可能难以找到,但通过上述方法,开发者依然可以成功地将其集成到自己的项目中,享受其提供的强大功能。