LDAP统一认证服务解决方案(转)
原文链接:https://javaforall.cn/126568.html
LDAP是什么
- 首先LDAP是一种通讯协议,LDAP支持TCP/IP。协议就是标准,并且是抽象的。在这套标准下,AD(Active Directory)是微软出的一套实现。 那AD是什么呢?暂且把它理解成是个数据库。也有很多人直接把LDAP说成数据库(可以把LDAP理解成存储数据的数据库)。像是其他数据库一样,LDAP也是有client端和server端。server端是用来存放资源,client端用来操作增删改查等操作。
- 而我们通常说的LDAP是指运行这个数据库的服务器。
- 可以简单理解AD =LDAP服务器+LDAP应用。
LDAP与数据库有什么区别?
LDAP是一个数据库,但是又不是一个数据库。说他是数据库,因为他是一个数据存储的东西。但是说他不是数据库,是因为他的作用没有数据库这么强大,而是一个目录。
为了理解,给一个例子就是电话簿(黄页)。我们用电话簿的目的是为了查找某个公司的电话,在这个电话簿中附带了一些这个公司的基本信息,比如地址,经营范围,联系方式等。
其实这个例子就是一个LDAP在现实生活中的表现。电话簿的组织结构是一条一条的信息组成,信息按照行业,类比进行了分类。每条记录都分成了若干的区域,其中涵盖了我们要的信息。这就是一个Directory。一个树状的结构,每个叶子都是由一条一条的分成若干区域的记录。LDAP就是这么一个东西。
从概念上说,LDAP分成了DN, OU等。OU就是一个树,DN就可以理解为是叶子,叶子还可以有更小的叶子。但是LDAP最大的分层按照IBM的文档是4层。
还是上面这个例子,电话簿由电话公司进行维护,因此写是由他们去写,去组织。写完了,组织好了,就完成了,以后再写,再组织的次数是有限的。而其作用是为了查找。LDAP也是类似,目的不是为了写,主要是为了查找。这就回答了有同志问,有人要写有人要读的并发怎么解决的问题。LDAP的用途不是针对这个来设计的,如果你有这样的需求,解决办法就应该是数据库,而不是LDAP。这就是另外一个例子,Access和SQL Server。Access就是一个数据库产品,但是主要用于家庭,功能和性能都比较弱。SQL Server就是一个专业的数据库系统,功能强大。LDAP是一个轻量级的产品,主要目的是为了查,因此在架构和优化主要是针对读,而不是写。但并不是说LDAP不能满足,只是说强项不在这里。
LDAP作为一个统一认证的解决方案,主要的优点就在能够快速响应用户的查找需求。比如用户的认证,这可能会有大量的并发。如果用数据库来实现,由于数据库结构分成了各个表,要满足认证这个非常简单的需求,每次都需要去搜索数据库,合成过滤,效率慢也没有好处。虽然可以有Cache,但是还是有点浪费。LDAP就是一张表,只需要用户名和口令,加上一些其他的东西,非常简单。从效率和结构上都可以满足认证的需求。这就是为什么LDAP成为现在很人们的统一认证的解决方案的优势所在。
当然LDAP也有数据写入的借口,是可以满足录入的要求的。这里就不多说了。
LDAP这种数据库有什么特殊的呢?
我们知道,像MySQL数据库,数据都是按记录一条条记录存在表中。而LDAP数据库,是树结构的,数据存储在叶子节点上。看看下面的比喻:
假设你要树上的一个苹果(一条记录),你怎么告诉园丁它的位置呢?
当然首先要说明是哪一棵树(dc,相当于MYSQL的DB),
然后是从树根到那个苹果所经过的所有“分叉”(ou)
,最后就是这个苹果的名字(uid,相当于MySQL表主键id)。
好了!这时我们可以清晰的指明这个苹果的位置了,就是那棵“歪脖树
”的东边那个分叉上的靠西边那个分叉的再靠北边的分叉上的半红半绿的……,晕了!你直接爬上去吧!
就这样就可以描述清楚“树结构”上的一条记录了。 说一下LDAP里如何定义一个记录的位置吧。
树(dc=ljheee)
分叉(ou=bei,ou=xi,ou= dong)
苹果(cn=redApple)
好了,redApple的位置出来了: dn:cn=honglv,ou=bei,ou=xi,ou=dong,dc=ljheee
其中dn标识一条记录,描述了一条数据的详细路径。 咦!有人疑问,为什么ou会有多个值?你想想,从树根到达苹果的位置,可能要经过好几个树杈,所有ou可能有多个值。关于dn后面一长串,分别是cn,ou,dc;中间用逗号隔开。
总结一下LDAP树形数据库如下:
dn :一条记录的详细位置
dc :一条记录所属区域 (哪一颗树)
ou :一条记录所属组织 (哪一个分支)
cn/uid:一条记录的名字/ID (哪一个苹果名字)
LDAP目录树的最顶部就是根,也就是所谓的“基准DN"。
- 为什么要用LDAP目录树来存储数据,用MySQL不行吗,为什么非要搞出一个树形的数据库呢?
- 这是因为用树形结构存储数据,查询效率更高(具体为什么,可以看一下关系型数据库索引的实现原理——B树/B+树)。在某些特定的场景下,使用树形数据库更理想。比如:需要储存大量的数据,而且数据不是经常更改,需要很快速的查找。
- 把它与传统的关系型数据库相比,LDAP除了快速查找的特点,它还有很多的运用场景,比如域验证等。
LDAP场景描述
一、多平台认证的烦恼
小明的公司有很多IT系统, 比如邮箱、SVN、Jenkins , JIRA,VPN, WIFI… 等等 。
新人入职时需要在每个系统中申请一遍账号,每个系统对用户名和密码的要求还不一样, 实在是烦人。
这还不算, 按照公司的策略, 这些密码每隔三个月还得更改一次,每次都是一次大折腾。
离职的时候, 各个账号又都得删除一遍,太折磨了。
能不能让这些系统用同一套用户名和密码呢? 申请一次,到处使用!
“嗯,这其实是一个用户统一认证的问题” 小明做了一个总结。
怎么去实现? 当然是开发一套系统了, 关键是要把账号统一起来用Mysql 数据库来保管, 然后用自己擅长的SpringMVC对外提供JSON接口, 别的系统比如SVN想做用户认证的时候,调用一下这个接口,把用户名和密码传过来,系统就会判断认证是否成功。
被这么一个美好的前景激励着,小明像打了鸡血,充满激情地、迅速地把这个系统开发出来了。
二、用户中心的推广
他先找了SVN的管理员,结果栽了跟头,人家根本不买账,理由很简单: “你这个系统稳定性、性能怎么样? 还有,你这接口是自己定义的,也不是业界标准,我甚至得开发代码和你做集成, 太麻烦。 对了, 你怎么不用LDAP啊?”
LDAP ? 这是什么鬼? 小明没放在心上, 又去找邮箱和VPN的负责人, 都被残忍地拒绝了, 甚至连理由都一样。
最后的希望集中在Jenkins身上, 管理Jenkins的是自己的哥们张大胖, 中午吃饭的时候小明向基友哭诉了自己的悲惨遭遇,希望能博得一点同情。
“我觉得你的想法很好啊,我们就缺你这样的实干家, 你说说接口是什么样的?” 大胖路见不平,决定为好基友两肋插刀。
“其实我这里提供了一个HTTP+JSON的接口, 你的Jenkins调用一下就行了” 小明满怀期望。
“这个… 虽然我没有仔细研究过, 但是Jenkins 好像只支持自定义的用户认证,还没有LDAP。” 大胖的刀还没拔出来就放回去了。
看来推广又要失败了。
干嘛不用LDAP?
“这个LDAP是什么东西,你们的系统为什么都要支持它?” 小明愤愤不平地问道。
“LDAP是Lightweight Directory Access Protocol,即轻量级目录访问协议, 用这个协议可以访问提供目录服务的产品,例如OpenLDAP。”
“目录服务?”
“对, 比如公司有个员工列表名单, 对于一个员工,你能查到他的电话,工位,部门等各种信息, 这就是一个目录啊。”
“听起来很适合保存一个公司员工的账号和密码啊” 小明说。
“是啊,这个目录服务啊,存储数据的方式有点特殊,完全不像我们熟知的关系数据库, 数据都在表中,一行一行的,一目了然,这个OpenLDAP是以树的方式存储的。 比如一个人的信息是这样的:
小明说:“ 有点古怪,不过这很像文件系统的目录树, 每个目录都有属性,可以存储信息,比如用户名和秘密,但是查询的时候还得一层一层的来,多麻烦, 为啥不用关系型数据库,直接一个select 不就出来了 ”
张大胖说: “我对LDAP研究不深, 但是我知道LDAP速度快, 非常快,比当今最快的数据库还要快。“
“怎么可能,现在的关系数据库多强悍啊。”
“其实LDAP主要的应用场景是查询多而修改极少,查询和修改的比率是10:1 甚至更高, 那就充分发挥LDAP的优势了,因为没有事务处理,那数据库的速度可是比不上。 还有LDAP能存储海量的数据,还可以轻松地在各个系统之间复制,可用性超高。 ”
小明想想确实是这样,公司员工信息变化本来就很少,我们把用户名密码存进去, 三个月才改一次, 查询的操作远远高于修改,如果LDAP专注于优化查询,又没有事务处理, 就像一个缓存一样, 肯定要更快了, 怪不得很多软件都支持LDAP做用户认证,这是个重要原因啊。
小明有点沮丧,觉得自己在没有充分调查研究的情况下,又造了一个轮子。
既然如此,那就搭建一个支持LDAP的目录服务器吧, 小明这一次吸取了教训,先说服了领导,在领导的支持下,进行了跨部门的沟通,经过艰苦的努力,各个系统终于搞成了统一认证, 现在的结构是这样的:
小明的努力没有白费, 除了学到技术外,还得到了公司的认可,年底的时候给他发了一个领导力的奖,奖励他勇于走出自己的工作岗位、跨部门的与同事沟通,用自己的专业能力带来大家完成了用户的统一认证,极大提高了工作的效率。
Spring LDAP的使用
- Spring LDAP,是Spring的一个组件,实现对LDAP的操作。
- 在编程操作MySQL时,我们除了用JDBC,可能都会选用一些框架,比如JbdcTemplate。
- JdbcTemplate的实现是通过传入sql语句和RowMapper,query返回目标列表,或是传入sql和参数,执行update方法。JdbcTemplate的优点是简化了与数据库连接的代码,以及避免了一些常见的错误。
- 同样的,Spring LDAP框架也提供了类似的特性——LdapTemplate。
- 优点都是相通的,Spring LdapTemplate的优点是简化了与LDAP交互的代码。
- 按之前Spring配置JavaBean的方式,在xml文件配置LdapTemplate及其属性值即可,本文将演示使用Springboot 用Java代码的方式定义LdapTemplate,完成Spring ldap连接数据库服务及进行相关操作。
首先引入LDAP依赖
<!-- spring ldapTemplate操作 --> <dependency> <groupId>com.sun</groupId> <artifactId>ldapbp</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.springframework.ldap</groupId> <artifactId>spring-ldap-core</artifactId> <version>2.3.2.RELEASE</version> </dependency>
- 先介绍一些Spring-ldap,因为网上有很多教程,在给出的工程依赖中有用spring-ldap的,也有spring-ldap-core的,而且还有版本问题。笔者使用目前最新的spring-ldap-2.3.2.RELEASE。推荐直接使用,这个最新版本。
- spring-ldap框架,是Spring集成ldap操作的总和,包含spring-ldap-core,spring-ldap-core-tiger,spring-ldap-ldif-core,spring-ldap-odm等jar,而通常我们在工程中只需要引入spring-ldap-core即可,它提供了绝大部分功能。而且截至目前,spring-ldap的<version>2.3.2.RELEASE</version>不在maven的中央仓库,不好获取。但spring-ldap-core在。
- 另外,Spring LDAP 2.0对jdk版本要求是1.6,并且开始支持ODM,并后来引入Spring ldap pool连接池。
- 据本人尝试,这些版本之间,变化差异很大。在新版本中可能有些关键的核心类,都会被移动到不同的package下;一些老版本完成的愚钝功能,可能在新版本中有了更好的实现或支持,所以在新版本中,一些“愚钝”实现可能会被移除。
- 比如LdapTemplate,原先在org.springframework.ldap包,在最新版本被移至core包。在spring-ldap-core的<version>2.0.2.RELEASE</version>版本中支持类似于JPA方式的LdapRepository,但在2.3.2最新版本中,完全被移除,但是新版本增强的LdapTemplate,使得LdapTemplate功能更强大,可以完全替代LdapRepository。
下面是用Java代码的方式定义LdapTemplate,完成用Spring ldap连接LDAP服务器
import com.xxx.xxx.sim.ldap.constants.LdapConstans; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.ldap.pool.factory.PoolingContextSource; import org.springframework.ldap.pool.validation.DefaultDirContextValidator; import org.springframework.ldap.transaction.compensating.manager.TransactionAwareContextSourceProxy; import java.util.HashMap; import java.util.Map; /** * LDAP 的自动配置类 * * 完成连接 及LdapTemplate生成 */ @Configuration public class LdapConfiguration { private LdapTemplate ldapTemplate; @Bean public LdapContextSource contextSource() { LdapContextSource contextSource = new LdapContextSource(); Map<String, Object> config = new HashMap(); contextSource.setUrl(LdapConstans.url); contextSource.setBase(LdapConstans.BASE_DC); contextSource.setUserDn(LdapConstans.username); contextSource.setPassword(LdapConstans.password); // 解决 乱码 的关键一句 config.put("java.naming.ldap.attributes.binary", "objectGUID"); contextSource.setPooled(true); contextSource.setBaseEnvironmentProperties(config); return contextSource; } @Bean public LdapTemplate ldapTemplate() { if (null == ldapTemplate) ldapTemplate = new LdapTemplate(contextSource()); return ldapTemplate; } }
- 完成LdapTemplate的bean定义,是最关键的一步。因为后续的操作,对于LDAP目录树的CRUD操作,全都靠它完成。
- 通过上面的代码,在IOC容器完成bean的定义,我们在外部就可以注入使用LdapTemplate了。
下面给出用LdapTemplate完成CRUD功能:
import com.xxx.xxx.sim.ldap.attribute.LdapDeptAttributeMapper; import com.xxx.xxx.ldap.attribute.LdapUserAttributeMapper; import com.xxx.xxx.xxx.ldap.module.dto.LdapUser; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DistinguishedName; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.filter.AndFilter; import org.springframework.ldap.filter.EqualsFilter; import java.util.List; public class ConfigTest extends BaseTest { @Autowired private LdapTemplate ldapTemplate; /** * 获取所有 internal人员 * ou=Internal,ou=People */ @Test public void listUsers(){ AndFilter filter = new AndFilter(); filter.and(new EqualsFilter("objectClass", "person")); //查询所有内部人员 List<LdapUser> users = ldapTemplate.search("ou=internal,ou=People", filter.encode(), new LdapUserAttributeMapper()); for (LdapUser user: users ) { System.out.println(user); } // Assert.assertEquals(123, users.size()); } /** * 根据userid 查找单个人员 */ @Test public void findUser(){ //uid=123,ou=internal,ou=People,o=xxx.com,o=xxx DirContextAdapter obj = (DirContextAdapter) ldapTemplate.lookup("uid=123,ou=internal,ou=People");//BASE_DC 不用填 System.out.println(obj); } /** * 根据部门编号o,查找部门 */ @Test public void findDept(){ //o=abcd,ou=Organizations,ou=People,o=xxx.com,o=xxx DirContextAdapter obj = (DirContextAdapter) ldapTemplate.lookup("o=abcd,ou=Organizations");//BASE_DC 不用填 System.out.println(obj); } @Test public void listDepts(){ AndFilter filter = new AndFilter(); filter.and(new EqualsFilter("objectClass", "organization")); //search是根据过滤条件进行查询,第一个参数是父节点的dn,可以为空,不为空时查询效率更高 List depts = ldapTemplate.search("", filter.encode(), new LdapDeptAttributeMapper()); System.out.println(depts.size()); // Assert.assertEquals(305600, depts.size()); } }
- 在ldap中,有两个”查询”概念,search和lookup。search是ldaptemplate对每一个entry进行查询,lookup是通过DN直接找到某个条目。
- 在Ldap中,新增与删除叫做绑定bind和解绑unBind。这些方法LdapTemplate全部提供,并且还提供各种条件过滤等方法,不如findAll(),list()等。
我们注意到,findAll(),list()肯定是返回一个java.util.List<T>,包括,
//查询所有internal人员 List<LdapUser> users = ldapTemplate.search("ou=internal,ou=People", filter.encode(), new LdapUserAttributeMapper());
也是返回列表,列表里装的是查询出来的结果。但是上一篇文章用JNDI方式查询出来的是 Attributes attrs = ctx.getAttributes("uid=123,ou=Internal,ou=People");//获取到一个人员
Spring-ldap是基于JNDI实现的封装,那是哪里实现的把Attributes转成我们需要的Java Bean对象呢? 答案在new LdapUserAttributeMapper(),这个接口实现了查询结果到对象的转化。
import com.xxx.csb.sim.ldap.module.dto.LdapUser; import org.springframework.ldap.core.AttributesMapper; import javax.naming.NamingException; import javax.naming.directory.Attributes; /** * 将ldap返回的结果,转成指定对象 */ public class LdapUserAttributeMapper implements AttributesMapper { /** * 将单个Attributes转成单个对象 * @param attrs * @return * @throws NamingException */ public Object mapFromAttributes(Attributes attrs) throws NamingException { LdapUser user = new LdapUser(); if(attrs.get("uid") != null){ user.setUsername( attrs.get("uid").get().toString()); } if(attrs.get("cn") != null){ user.setUserCn( attrs.get("cn").get().toString()); } if(attrs.get("mobile") != null){ user.setMobile( attrs.get("mobile").get().toString()); } if(attrs.get("mail") != null){ user.setMail( attrs.get("mail").get().toString()); } if(attrs.get("employeeNumber") != null){ user.setUserNumber( attrs.get("employeeNumber").get().toString()); } if(attrs.get("type") != null){ user.setUserType( attrs.get("type").get().toString()); } if(attrs.get("py") != null){ user.setPinyin(attrs.get("py").get().toString()); } if(attrs.get("alias") != null){ user.setAlias(attrs.get("alias").get().toString()); } if(attrs.get("departmentNumber") != null){ user.setDeptId(attrs.get("departmentNumber").get().toString()); } if(attrs.get("departmentName") != null){ user.setDeptName(attrs.get("departmentName").get().toString()); } if(attrs.get("jobname") != null){ user.setPositionName(attrs.get("jobname").get().toString()); } if(attrs.get("modifyTimestamp") != null){ user.setModifyTimestamp(attrs.get("modifyTimestamp").get().toString()); } return user; } }
可以看到转化的过程非常繁琐,无非就是拿JNDI查询到的Attributes,不停的获取属性值,再设置到Java对象中;attrs.get(“uid”).get().toString()然后set。
那好了,在每次查询的时候,要查询到多少列,在这个AttributesMapper转化方法中就要写多少个,判断及赋值。而且,如果因为业务不同,要查询不同的列,那AttributesMapper接口的实现必须重新写。那有没有支持复用的方式呢?答案是肯定的。