Spring LDAP 官文阅读理解 纯干货

缘起

公司项目需要使用LDAP服务, 遂研究了2天. 昨天下午想写个LDAP分页, 百度万千, 结果都是抄来抄去的不可用的代码. 感觉博客的作者自己都没弄懂Spring LDAP的API分类就瞎抄一气. 我极为光火. 遂花了一晚上阅读了Spring LDAP 2.3.2 的官方文档. 豁然开朗, 并且写了基于最新的spring-ldap2.3.2分页的demo. 关于参考文档,见【1-3】

本文并不想仔细分析其中的API,其实这一点直接去看官文就很容易看懂了. 英文不好的童鞋直接看中文翻译的1-5章. 秒懂. 比百度搜的抄来抄去作者都不知所谓的博文好上百倍.

分析

Spring LDAP 封装了LDAP原生的JNDI操作. Spring LDAP 的API 分成2类

  1. 实体类不带注解的
  2. 实体类带注解的

不带注解的需要自己写Mapper(不论是AttributesMapper还是较为方便的ContextMapper), 而为了解决需要额外定义Mapper的代码繁琐问题,Spring LDAP 引入了ODM(Object Directory Mapping)注解. 该注解就是提供了实体类属性和LDAP目录结构之间的映射,这和JPA注解是类似的. 本质上就是实现了Mapper的功能. 所以不难知道, 后者在增删改查的时候是不需要传入Mapper或者有类似于做Mapper的事情的举动的. 这里只说两个问题

  1. 在使用ODM注解的时候, 具体遇到的问题和自己的理解
  2. 分页的正确姿势

可能下面的写的比较乱,但是绝对是使用spring ldap 2.3.2的干货. 因为下面要讲的一些东西都是我demo中试出来的, 用语言不好表达,所以很多博客或者官文干脆寥寥数言就带过 但是如果理解不到位的话, 就很难真正把Spring LDAP的API用好的. 所以下面的语言会比较啰嗦.

使用ODM注解时遇到的问题

这里首先说一句,ODM注解虽然代码简单,但是操作并没有不带注解的简明直接. 所以其实我更喜欢用不带注解的方式.

  1. Q: 注解@Entry的属性objectClasses意味着什么?

    A: 意味着如果查到的条目中没有包含这里写的objectClasses全部值的话, 就不会被查询出. 相当于过滤. 当然, 官网上也有解释.

  2. Q: 注解@Entry的属性base意味着什么?

    A: 这个必须要和另一个注解 @DnAttribute 结合在一起讲.

    项目的DIT树基本结构如下

    @DnAttribute 有2个属性,一个叫value,一个叫index. 写了这个注解的属性是一定要参与条目dn值的构建的,所以不能为null, 不然创建条目的时候报错. value表示它在dn中的名称. index就是它的索引位置(从@Entry的base属性开始,第一个index为0). 这样讲依旧好抽象,我们来举个栗子吧!

    比如(具体代码参见DEMO【1】)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Entry(base = "o=xxx,ou=study", objectClasses = { "inetOrgPerson" })
    public class Person {
    @DnAttribute(value = "cn", index = 2)
    // @Attribute(name = "cn")
    @Transient
    private String ouu;

    @DnAttribute(value = "cn", index = 1)
    // @Attribute(name = "cn")
    @Transient
    private String ouu2;
    @DnAttribute(value = "ou", index = 0)
    @Attribute(name = "ou")
    // @Transient // 注意,这是spring ldap 包下的Transitent, 不要写错了!!!不然看不到效果的
    private String ouu1;
    ...

    则你在创建对象的时候, 即

    1
    ldapTemplate.create(person);

    的时候, 会读取person入参的ouu(比如说study5)、ouu1(比如说vvv)、ouu2(比如说cfs)的值,然后拼成该条目的dn值

    1
    cn=study5,cn=cfs,ou=vvv,o=xxx,ou=study,o=myorg,dc=yfs,dc=com

    注意,上面的dn值其实分成了3部分

    1. cn=study5,cn=cfs,ou=vvv, 这就是 上面三个@DnAttribute属性. ouu2映射成了0号位置的ou, 其实就是从右到左, 截掉下面的2和3开始从零算起.
    2. o=xxx,ou=study 这其实就是@Entry注解中的base值
    3. o=myorg,dc=yfs.dc=com 这是整个ldap连接的base dn,简记P, 在配置文件中写的. 注意,一旦这个P配置好了,则API中所有的操作要写的dn入参都是相对于P来写的, 不要再拼上P,不然报NameNotFound.

    所以就知道了为什么@DnAttribute注解的属性不能为null了. 不然的话, 无法构建dn值,那ldap怎么知道你要把这个条目放在ldap的哪个位置呢? 这里多说一句,属性的属性名是不能乱写的(不能写ouu、ouu1、ouu2,不然会报type undefined的, 所以才要使用@Attribute进行映射), 例如上面的ou、cn,因为这些都是在ldap服务器的scheme约束文件中写的. 可以自定义自己的属性名,甚至objectclass.

    而且要注意, 你上传一个cn=study5,cn=cfs,ou=vvv,o=xxx,ou=study,o=myorg,dc=yfs,dc=com这样的条目的话, 则必须要保证中间的条目,例如

    cn=cfs,ou=vvv,o=xxx,ou=study,o=myorg,dc=yfs,dc=com

    ou=vvv,o=xxx,ou=study,o=myorg,dc=yfs,dc=com

    o=xxx,ou=study,o=myorg,dc=yfs,dc=com

    ou=study,o=myorg,dc=yfs,dc=com

    o=myorg,dc=yfs,dc=com

    都已近建立好了,不然报NameNotFoud. 这其实并不难理解的, 因为条目中有很多属性,你不可能就给ldap服务器一个子条目,就让ldap服务器猜测所有父条目的属性一路把父条目给你自动创建出来.

    还要说一句,@Entry注解中的base属性并不意味着查询节点的时候从”base拼上ldap连接配置的base dn”这个地址搜索起, base和搜索的起点完全是不搭嘎的两个东西. base只是用来计算创建的条目的dn值的. 而且必须要有@DnAttribute 注解的属性, 不然报 Unable to determine id for entry com.yfs.po.Person异常. 而LdapTemplate中的find方法如不指定搜索起点(即base入参),则都是从ldap连接中的base dn开始搜起. 和@Entry中的base完全无关.

    其实很正常, 因为LDAP只有一个根条目,你总会在根条目下面创建子条目啊.

    例如我们可以

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Entry( objectClasses = { "inetOrgPerson" })
    public class Person {

    @Id
    // @JsonIgnore // @JsonIgnore是为了将person传给前端时不报错,因为Name类型的无法自动解析成json格式。但是这里没导入json的包
    private Name dn;

    @DnAttribute(value = "ou", index = 0)
    // @Attribute(name = "ou")
    @Transient // 注意,这是spring ldap 包下的Transitent, 不要写错了!!!不然看不到效果的
    private String ouu1;
    @Attribute(name = "cn")
    private String commonName;
    @Attribute(name = "sn")
    private String suerName;

    注意,我们没有填写注解 @Entry 的base属性, 然后

    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    public void test1() {
    Person person = new Person();
    person.setCommonName("xin");
    person.setSuerName("xin");
    person.setOuu1("vvv");
    personRepo.create(person);
    }

    则就可以

    我们就在根条目下面创建了vvv条目. 所以@DnAttribute 属性的好处是显然的. 那就是可以在创建和更新条目的时候spring-ldap框架自动计算条目的dn值,则就知道这个条目在DIT中的位置. 特别是更新,就可以更新完毕一update则条目自动移位置了.

    最后我们来讲讲上面出现的@Transient注解. 首先这个注解是

    1
    org.springframework.ldap.odm.annotations.Transient;

    即ldap包下的, 还有一个

    1
    org.springframework.data.annotation.Transient

    不要导包的时候导错了,导致最后看不到@Transient的效果. 被这个注解注解的属性将不会成为条目的属性. 首先,比较好理解的@Attribute注解的含义大家都知道(N多博客总算不至于把这个都写错). 就是被注解的属性创建条目以及读取条目的时候,条目中的属性自动映射到成员变量. 但是学过JPA的童鞋都知道,JPA也有@Transient注解, 一旦被注解就不再会被存储到数据库中,举一反三,被此注解的成员变量就不会成为条目中的属性. 也就是创建条目的时候,此属性不会成为条目中的属性. 读取的时候自然没有被读取条目中的属性映射到此属性. 譬如一个同时被@DnAttribute 和 @Transient 注解的属性的作用就仅仅是参与dn的构建(因此不会为null),但是不会参与条目的属性的事情. 但是也有例外,例外就是条目的rdn值一定会参与属性的构建,而不论它有没有被@Transient注解,干说太抽象. 下面举一个栗子.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Entry(base = "o=xxx,ou=study", objectClasses = { "inetOrgPerson" })
    public class Person {

    @Id
    // @JsonIgnore // @JsonIgnore是为了将person传给前端时不报错,因为Name类型的无法自动解析成json格式。但是这里没导入json的包
    private Name dn;
    @DnAttribute(value = "cn", index = 2)
    // @Attribute(name = "cn")
    @Transient
    private String ouu;

    @DnAttribute(value = "cn", index = 1)
    // @Attribute(name = "cn")
    @Transient
    private String ouu2;
    @DnAttribute(value = "ou", index = 0)
    @Attribute(name = "ou")
    // @Transient // 注意,这是spring ldap 包下的Transitent, 不要写错了!!!不然看不到效果的
    private String ouu1;
    @Attribute(name = "cn")
    private String commonName;
    @Attribute(name = "sn")
    private String suerName;
    ... 省略 gettor、settor
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    public void test1() {
    Person person = new Person();
    person.setCommonName("xin");
    person.setSuerName("xin");
    person.setOuu("study6");
    person.setOuu1("vvv");
    person.setOuu2("cfs");
    personRepo.create(person);
    }

    前提前面说了,需要想创建的子条目的所有父条目都已经创建好了,不然报NameNodeNotFound。则最后效果是

    可以清晰的看到成功创建了study6条目 有ou=vvv属性. 这是因为 ouu1 被映射为ou,并且没有被 @Transient注解,所以自动成为了条目属性,而ouu2被cn,ouu2为cfs,但是条目属性中并没有cn=yfs属性. 这是因为ouu2成员上被注解了@Transient. 最后虽然ouu被映射为了cn,值为study6,而且也打上了@Transient注解,但是因为study6是rdn(也就是dn最左边的值),所以尽管你注解了@Transient, 依旧会成为属性的.

    如果打开第八行注释,注掉第九行的话, 结果不变. 如果打开13行注释,注掉第14行的话,则变成

    可见,ouu2(值为cfs)也变成了新建条目的属性.

    如果继续注掉17行,打开18行注释的话,则

    可以发现 ou=vvv 属性已经不见了. 因为他被transient掉了.

分页的正确姿势

分页网上基本抄来抄去就是那段代码,而且注释都一样抄. 就不能自己理解一下吗? 官网上给了现成的demo啊!

具体代码参见DEMO【1】中的searhByPage方法,这里讲一下我对他的理解. LDAP分页其实就是我要找第5页,每页3条数据的结果集. 则ldap很笨的,会从第一页开始给你找,然后每找到一页就会执行你传入的回调, 我们只需要在一页一页找的过程中加入一个计数器,到了第5页就终止查找(如果要找的页数超过最大页数的话, 也只返回最后一页的数据),然后返回即可. 这就是分页的原理.

参考

【1】https://docs.spring.io/spring-ldap/docs/current/reference/ Spring LDAP 2.3.2 官方文档

【2】https://www.jianshu.com/p/77517e26a357 Spring LDAP 2.3.2 官方文档 1-5 章翻译

【3】https://www.jianshu.com/p/835c2db4a1c4 Spring LDAP 2.3.2 官方文档 6-10 章(不完整)翻译

DEMO

【1】https://github.com/yfsyfs/backend/tree/master/springboot-ldap