🍃

Spring BootでLDAP認証を行う

2022/10/23に公開

Spring Boot で LDAP認証を利用してみます。
Spring Security LDAPを利用することで簡単に実装できます。

今回確認に利用したプロジェクト全体は、下記に配置しています。

プロジェクト

Spring Bootの最新(2022年10月16日時点)となる2.7.4で確認します。

  • Java 17
  • Spring Boot 2.7.4
  • Gradle 7.5

build.gradleは下記のようになります。

build.gradle
plugins {
    id 'org.springframework.boot' version '2.7.4'
    id 'io.spring.dependency-management' version '1.0.14.RELEASE'
    id 'java'
}

group = 'com.github.onozaty'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.ldap:spring-ldap-core'
    implementation 'org.springframework.security:spring-security-ldap'

    implementation 'com.unboundid:unboundid-ldapsdk'
}

tasks.named('test') {
    useJUnitPlatform()
}

spring-boot-starter-security にプラスして、LDAPでの認証を行うために spring-ldap-corespring-security-ldap を追加する形になります。

unboundid-ldapsdkは、確認用のLDAPサーバとして利用しています。
(別途LDAPサーバがあって、そちらと通信する形ならば不要)

UnboundID LDAP の設定

UnboundID LDAP で利用するLDIFファイルを作成します。
パスワードはBCryptでハッシュ化します。

src/main/resources/test-server.ldif
dn: dc=example, dc=com
objectClass: top
objectClass: domain
objectClass: extensibleObject
dc: example

dn: ou=groups,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: groups

dn: cn=admin,ou=groups,dc=example,dc=com
cn: admin
objectClass: groupOfUniqueNames
objectClass: top
uniqueMember: uid=taro,ou=people,dc=example,dc=com

dn: cn=user,ou=groups,dc=example,dc=com
cn: user
objectClass: groupOfUniqueNames
objectClass: top
uniqueMember: uid=taro,ou=people,dc=example,dc=com
uniqueMember: uid=hanako,ou=people,dc=example,dc=com

dn: ou=people,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: people

dn: uid=taro,ou=people,dc=example,dc=com
cn: Taro, Yamada
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
sn: Taro
uid: taro
userPassword: $2a$08$cf6mjdn16R09lrCdQQb5NuGzi1m6g5JeTakFAfsk6dbtepoh0ckh.

dn: uid=hanako,ou=people,dc=example,dc=com
cn: Hanako, Yamada
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
sn: Hanako
uid: hanako
userPassword: $2a$08$qe6uyixO/SwhfIfQGwdOM.yt8OVcGSRESInc5dodO2vcSL51oFtJC

権限による制御も確認したかったので、adminuserの2つのグループを用意し、下記の2つのユーザを用意しています。

ユーザ パスワード グループ
taro taropassword admin、user
hanako hanakopassword user

application.propertiesにて、LDAPサーバの情報を指定します。

src/main/resources/application.properties
spring.ldap.embedded.base-dn=dc=example,dc=com
spring.ldap.embedded.ldif=classpath:test-server.ldif
spring.ldap.embedded.port=8389

簡単なWebページの作成

アクセスの制御を確認するため、下記の3つのHTMLを作成します。

  • src/main/resources/static/index.html トップページ
  • src/main/resources/static/admin/admin.html admin向けのページ
  • src/main/resources/static/user/user.html user向けのページ
src/main/resources/static/index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Top</title>
  </head>
  <body>
    <ul>
      <li><a href="./admin/admin.html">Admin</a></li>
      <li><a href="./user/user.html">User</a></li>
      <li><a href="./logout">Logout</a></li>
    </ul>
  </body>
</html>
src/main/resources/static/admin/admin.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Admin</title>
  </head>
  <body>
    <ul>
      <li><a href="../">Top</a></li>
      <li><a href="../user/user.html">User</a></li>
      <li><a href="../logout">Logout</a></li>
    </ul>
  </body>
</html>
src/main/resources/static/user/user.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>User</title>
  </head>
  <body>
    <ul>
      <li><a href="../">Top</a></li>
      <li><a href="../admin/admin.html">Admin</a></li>
      <li><a href="../logout">Logout</a></li>
    </ul>
  </body>
</html>

Spring Security の設定

Spring Security の設定を行います。

SpringのGetting Started Guidesにある Authenticating a User with LDAP だと、WebSecurityConfigurerAdapterを使った実装になっていますが、WebSecurityConfigurerAdapterは既に非推奨になっているので注意が必要です。(Spring Security 5.7に対応した更新が漏れている?)

Spring Security側のリファレンスだと最新の情報になっているので、そちらを参考に記載します。

src/main/java/com/example/ldap/WebSecurityConfig.java
package com.example.ldap;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.ldap.LdapPasswordComparisonAuthenticationManagerFactory;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
public class WebSecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

    // https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html
    // デフォルトのフォーム認証を利用
    http.formLogin();
    http.logout(
        // GETでもログアウトが受け付けられるように
        logout -> logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout")));

    // https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
    http.authorizeHttpRequests(
        authz -> authz
            // admin配下は要ADMINロール
            .mvcMatchers("/admin/**").hasRole("ADMIN")
            // user配下は要USERロール
            .mvcMatchers("/user/**").hasRole("USER")
            // それ以外のリクエストも全て認証必要(ロールは何でもOK)
            .anyRequest().authenticated());

    return http.build();
  }

  // https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/ldap.html
  @Bean
  public BaseLdapPathContextSource contextSource() {
    return new DefaultSpringSecurityContextSource(
        "ldap://localhost:8389/dc=example,dc=com");
  }

  @Bean
  public LdapAuthoritiesPopulator authorities(BaseLdapPathContextSource contextSource) {

    // 権限の情報をgroupsから取得
    String groupSearchBase = "ou=groups";

    DefaultLdapAuthoritiesPopulator authorities =
        new DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase);
    // uniqueMemberが一致するものが対象
    authorities.setGroupSearchFilter("uniqueMember={0}");

    return authorities;
  }

  @Bean
  public AuthenticationManager authenticationManager(
      BaseLdapPathContextSource contextSource, LdapAuthoritiesPopulator authorities) {

    LdapPasswordComparisonAuthenticationManagerFactory factory =
        new LdapPasswordComparisonAuthenticationManagerFactory(
              contextSource,
              new BCryptPasswordEncoder());

    // ユーザ名とパスワードのフィールドを指定
    factory.setUserDnPatterns("uid={0},ou=people");
    factory.setPasswordAttribute("userPassword");

    factory.setLdapAuthoritiesPopulator(authorities);

    return factory.createAuthenticationManager();
  }
}

動作確認

Admin の taro でログインします。
AdminなのでAdminのページも問題無く表示されます。

User の hanako でログインします。
Adminのページに遷移すると、権限が無いので 403 Forbidden となります。

参考

Discussion