springboot+LDAP构建一个安全的web应用

官网上非常粗犷的提供了一篇关于springboot如何利用LDAP构建一个安全的应用的文档: https://spring.io/guides/gs/authenticating-ldap/

但是在实际尝试的时候,出现了非常多的疑惑点,包括:

  1. 什么是LDAP?
  2. 那四个新的依赖是干嘛的?
  3. 如何设置密码
  4. 等等

所以写下此篇文档前花了一些时间,去让自己明白,这,到底是什么以及怎么弄!

首先,我们来看看官网让我们装载的新的相关依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ldap</groupId>
    <artifactId>spring-ldap-core</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
</dependency>

我们就从这四个依赖入手,去明白这是怎么一回事~~~

1. 依赖:spring-boot-starter-security

我们之前的demo里面,创建的直接可以通过api获取数据的那些web应用,都是非常不安全的。

那么什么是安全的呢?

简单的来说,就是需要使用的时候,有安全验证,从各个访问的环节,尽可能的阻拦一些风险。

常用的方法就比如我们使用api的时候,需要带用户名密码,或者需要带登录后定时失效的token才可以访问等等。

这里有一篇很好的文章说明了我们要用的第一个依赖: spring-boot-starter-security

spring-boot-starter-security与应用安全

在当前项目中只要添加需要的 Controller 实现,一个添加了基本安全防护的 Web 应用就诞生了。spring-boot-starter-security 默认会提供一个基于 HTTP Basic 认证的安全防护策略,默认用户名为 user,访问密码则在当前 Web 应用启动的时候,打印到控制台,类似于:

2017-01-01 13:57:00.596 INFO 17966 --- [ost-startStop-1] b.a.s.Au-thenticationManagerConfiguration : Using default security password: 560ff91b-0ae7-492c-ad16-603e1adec54c 

如果我们希望对 HTTP Basic 认证的用户名和密码进行定制,可以通过如下配置项进行:

security.user.name={个人希望设置的用户名}\
security.user.password={个人希望使用的访问密码}

也就是说, spring-boot-starter-security 提供了一个非常基础的安全防护,让你可以通过默认密码或者自定义的用户名密码,来访问当前网页应用。

2. 依赖:spring-ldap-core 和 spring-security-ldap

1. 初识LDAP

去搜索这两个依赖的时候,都不约而同的指向了LDAP这个概念,所以这一节就需要弄明白LDAP到底是个什么东西.

先看看官方说明:

LDAP,Lightweight Directory Access Protocol,轻量级目录访问协议

无疑,这里又出现了新的疑问,什么是目录访问协议,这些又是什么!

根据网上大量的各种博客和资料,最终的总结如下:

  1. LDAP是一种特殊的服务器,可以存储数据
  2. 数据的存储是目录形式的,或者可以理解为树状结构(一层套一层)
  3. 一般存储关于用户、用户认证信息、组、用户成员,通常用于用户认证与授权

我们看看上图,就可以发现,LDAP可以通过设置这些o/ou/cn这些标志位,来创建一个树状结构的用户数据,还能把用户所属的公司、组织、部门、同部门的用户等都存起来。

这样的话,就很容易界定用户的类别和用户的权限,也方便进行用户查询

2. LDAP的标志位说明:依赖:unboundid

上一节的图中,我们可以看到一些o/ou/cn/dn这些标志位,或者说是字段,那么大概是什么意思呢?

首先,我们需要引入unboundid这个maven依赖:

<dependency>
   <groupId>com.unboundid</groupId>
   <artifactId>unboundid-ldapsdk</artifactId>
</dependency>

我们来看一下官方的解释:

UnboundID LDAP SDK提供了一套快速、强大、用户友好并且开源的Java API来与LDAP目录服务器交互。与其它基于Java的LDAP APIs相比,它具有更好的性能、更易于使用,功能更多。而且还是唯一个不断有活跃开发和增强的SDK。

官网地址:www.ldap.com/unboundid-l…

简单的来说,这玩意就是提供了一个更为友好的LDAP的交互配置的sdk,也就是我们所理解的LDAP的标志位。通过配置这些东西,就可以简单的通过一个配置文件,建立一套LDAP的目录数据。

大致的总结如下:

标识位 英文全称 中文说明
cn common name 通用名
ou organization unit 组织单位
o organization 组织名称
uid userid 对象id
dc domain component dns中的每个元素,例如com.cn中com和cn都是一个dc
dn distiguished name 可以看做dns,但组成dn的每个值都有自己的属性,如上例:dn为dc=com,dc=cn;同时,dn也可以表示某个目录,或者某个目录中的对象,如用户名等

那我们来理解一下官网例子中的配置信息:(很多,但是解读后有助于理解)

## 这是LDAP目录的第一层位置
## dns是:springframework.org
## 当前的dns的元素位置为springframework
dn: dc=springframework,dc=org
objectclass: top 
objectclass: domain
objectclass: extensibleObject
dc: springframework
## 这是LDAP第二层位置,隶属于上面的目录之下,产生了一个组织名为groups
## dns为:groups.springframework.org
## 组织单位为:groups
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups
## 这是LDAP第三层位置,是组织groups的下级组织:subgroups
## dns为:subgroups.groups.springframework.org
## 组织单位:subgroups
dn: ou=subgroups,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: subgroups
## 这是一个和groups同级别的组织:people,至此,第一级目录有了两个子集组织:people和groups
## dns为:people.springframework.org
## 组织单位:people
dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people
## 这是一个和groups同级别的组织:space cadets,至此,第一级目录有了三个子集组织:space cadets、people和groups
## dns为:space cadets.springframework.org
## 组织单位:space cadets
dn: ou=space cadets,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: space cadets
## 这是一个和groups同级别的组织:\"quoted people\",至此,第一级目录有了四个子集组织:\"quoted people\"、space cadets、people和groups
## dns为:\"quoted people\".springframework.org
## 组织单位:"quoted people"
dn: ou=\"quoted people\",dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: "quoted people"
## 这是一个和groups同级别的组织:otherpeople,至此,第一级目录有了五个子集组织:otherpeople、\"quoted people\"、space cadets、people和groups
## dns为:otherpeople.springframework.org
## 组织单位:otherpeople
dn: ou=otherpeople,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: otherpeople
## 描述用户信息:隶属于people这个组织下,有自己的用户名和密码
## dns为:ben.people.springframework.org
## 通用名:Ben Alex
## userid:ben
## 密码:$2a$10$c6bSeWPhg06xB1lvmaWNNe4NROmZiSpYhlocU/98HNr2MhIOiSt36(加密字符串)
dn: uid=ben,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Ben Alex
sn: Alex
uid: ben
userPassword: $2a$10$c6bSeWPhg06xB1lvmaWNNe4NROmZiSpYhlocU/98HNr2MhIOiSt36
## 描述用户信息:隶属于people这个组织下,有自己的用户名和密码
## dns为:bob.people.springframework.org
## 通用名:Bob Hamilton
## userid:bob
## 密码:bobspassword
dn: uid=bob,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Bob Hamilton
sn: Hamilton
uid: bob
userPassword: bobspassword
## 描述用户信息:隶属于otherpeople这个组织下,有自己的用户名和密码
## dns为:joe.otherpeople.springframework.org
## 通用名:Joe Smeth
## userid:joe
## 密码:joespassword
dn: uid=joe,ou=otherpeople,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Joe Smeth
sn: Smeth
uid: joe
userPassword: joespassword

此外,还有几个同样的用户,我们就不赘述了:

jerry、slashguy、quoteguy、space cadet

最后,还列举了一些group中的用户权限

## 这是一个隶属于groups的组织:developers
## dns为:developers.groups.springframework.org
## 组织单位:developer
## 这里面有两个独立用户:ben和bob
dn: cn=developers,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: developers
ou: developer
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
uniqueMember: uid=bob,ou=people,dc=springframework,dc=org
## 这是一个隶属于groups的组织:managers
## dns为:managers.groups.springframework.org
## 组织单位:managers
## 这里面有两个独立用户:ben和jerry
dn: cn=managers,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: managers
ou: manager
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
uniqueMember: cn=mouse, jerry,ou=people,dc=springframework,dc=org
## 这是一个隶属于subgroups的组织:submanagers
## dns为:submanagers.subgroups.groups.springframework.org
## 组织单位:managers
## 这里面有一个独立用户:ben
dn: cn=submanagers,ou=subgroups,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: submanagers
ou: submanager
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org

组织架构图:

springLDAP组织结构.jpg

综上所述,我们可以看到,通过LDAP的配置,我们可以创建自定义层级的各种组织架构,还可以添加各类用户,并为用户设置角色权限。可以说,这基本上实现了一个标准的用户角色权限与组织架构的基本功能。

3. LDAP工程实践

现在,我们开始往之前创建的RESTful应用中,添加LDAP的设置:

1. 添加依赖

先往pom.xml中添加maven依赖,并更新

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.ldap</groupId>
   <artifactId>spring-ldap-core</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
   <groupId>com.unboundid</groupId>
   <artifactId>unboundid-ldapsdk</artifactId>
</dependency>

2. 创建LDAP角色的配置文件: test-server.ldif

在resource中创建一个新的文件,并把角色权限和组织的配置信息放进去 image.png

dn: dc=springframework,dc=org
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: springframework

dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=subgroups,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: subgroups

dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people

dn: ou=space cadets,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: space cadets

dn: ou="quoted people",dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: "quoted people"

dn: ou=otherpeople,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: otherpeople

dn: uid=ben,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Ben Alex
sn: Alex
uid: ben
userPassword: $2a$10$c6bSeWPhg06xB1lvmaWNNe4NROmZiSpYhlocU/98HNr2MhIOiSt36

dn: uid=bob,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Bob Hamilton
sn: Hamilton
uid: bob
userPassword: bobspassword

dn: uid=joe,ou=otherpeople,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Joe Smeth
sn: Smeth
uid: joe
userPassword: joespassword

dn: cn=mouse, jerry,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Mouse, Jerry
sn: Mouse
uid: jerry
userPassword: jerryspassword

dn: cn=slash/guy,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: slash/guy
sn: Slash
uid: slashguy
userPassword: slashguyspassword

dn: cn=quote"guy,ou="quoted people",dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: quote"guy
sn: Quote
uid: quoteguy
userPassword: quoteguyspassword

dn: uid=space cadet,ou=space cadets,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Space Cadet
sn: Cadet
uid: space cadet
userPassword: spacecadetspassword

dn: cn=developers,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: developers
ou: developer
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
uniqueMember: uid=bob,ou=people,dc=springframework,dc=org

dn: cn=managers,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: managers
ou: manager
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
uniqueMember: cn=mouse, jerry,ou=people,dc=springframework,dc=org

dn: cn=submanagers,ou=subgroups,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: submanagers
ou: submanager
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org

3. 在 application.properties 中指定LDAP服务器使用上述配置文件

# Spring-LDAP user authentication
spring.ldap.embedded.ldif=classpath:test-server.ldif
spring.ldap.embedded.base-dn=dc=springframework,dc=org
spring.ldap.embedded.port=8388

4. 创建使用LDAP服务的java配置文件

image.png

package shenling.example.springbootJDBC.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().fullyAuthenticated()
                .and()
                .formLogin();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .ldapAuthentication()
                .userDnPatterns("uid={0},ou=people")
                .groupSearchBase("ou=groups")
                .contextSource()
                .url("ldap://localhost:8388/dc=springframework,dc=org") // 此处指定了LDAP服务器路径,端口号为我们自定义的8388
                .and()
                .passwordCompare()
                .passwordEncoder(new BCryptPasswordEncoder()) // 此处指定了可以使用encrypted的密码版本,和我们的用户信息配置有关系
                .passwordAttribute("userPassword");
    }

}

4. 运行结果

正常输入localhost后,会跳转到login页面: image.png

输入用户名/密码:ben/benspassword,登录成功 image.png

5. 使用非encrypted的密码登录失败

我们发现,使用bob等以明文形式存储密码的用户进行登录,会出现登录失败的问题:

image.png

控制台提示说密码不是encrypted的

2021-12-14 11:01:54.879  WARN 57415 --- [nio-8080-exec-3] o.s.s.c.bcrypt.BCryptPasswordEncoder     : Encoded password does not look like BCrypt

所以,我们注释掉了java文件中关于这个密码的这一行:

// .passwordEncoder(new BCryptPasswordEncoder())

即可登录成功 image.png

6. 延伸学习:单点登录

LDAP实际上也算是一种单点登录的方式,当然,我们在企业级应用中更为常见的应该是SSO或者OAuth这一类的配置,便于通过一个登录站点,可登录多个不同的外部服务。

那么,LDAP和OAuth这些主要区别在哪里呢?

参考:# cas 单点登录_5分钟明了单点登录SSO、OAuth、LDAP、CAS的流程与应用

  1. OAuth协议能广泛应用于互联网中,基于大企业的巨大用户量,能减少小网站的注册推广成本,并且能做到更加便捷的资源共享。
  2. LDAP协议适用于企业用户使用,通过LDAP协议,能较好地管理员工在公司各系统之间的授权与访问。
  3. CAS模型,作为权威机构开发的系统,具有很好的兼容性与安全性,广泛应用于各大高校等大型组织,能很好地完成大量系统的对接与大量人员的使用。

原文:https://juejin.cn/post/7041391214066663455