OpenLDAP 構築と Java によるディレクトリ認証の実践

LDAP の概要とアーキテクチャ

軽量ディレクトリアクセスプロトコル(LDAP)は、情報サービスとしてのディレクトリ機能を実現する標準的なプロトコルです。通常のデータベースとは異なり、読み取り・検索・ブラウジング操作に最適化されている点が特徴です。構造はツリー型で定義され、属性ベースの情報やフィルタリング能力を強力にサポートします。

LDAP サーバーは通常、複雑なトランザクション管理や大量データの更新には向いていませんが、高頻度な参照を伴うシステム(例えば、ユーザー認証やアドレス帳など)では、MySQL や PostgreSQL などのリレーショナルデータベースよりも優れたパフォーマンスを発揮します。情報はエントリー(Entry)という単位で保存され、組織単位やユーザー単位で階層化されます。

OpenLDAP の基本機能

OpenLDAP はオープンソース実装の代表的な存在であり、以下の主要機能を提供します。

  • クエリ操作: ディレクトリからの高速なデータ取得。RDBMS よりも参照性能が高い。
  • 更新操作: エントリーの追加・変更・削除が可能だが、更新頻度が高くないことが望ましい。
  • レプリケーション: マスターサーバーからスレーブサーバーへデータを複製し、可用性を向上させる分散構造に対応。
  • 認証・管理: クライアントの識別およびセッション制御を行う機能。

インストールとセットアップにおける課題

OpenLDAP の導入において最も注意すべき点は、バージョン間での設定ファイルの違いです。旧バージョン(2.3 など)では slapd.conf が中心でしたが、新バージョン(2.4 以降)ではディレクトリ構造(slapd.d)を基盤とした設定 menjadi 推奨されています。

ネットワーク設定とドメイン

LDAP サービスを正しく起動させるためには、ホスト名の解決が必須です。設定ファイル(suffix など)で指定されたドメイン構成が、実際の OS 側のホスト名と一致している必要があります。

Linux 環境では /etc/hosts に以下の形式を追加することで解決します。

127.0.1.1       hostname.example.com    hostname

これにより、slapd.conf または cn=config 内の suffix 設定(例:dc=example,dc=com)と整合性が取れます。

バックエンドストレージについて

デフォルトでは Berkeley DB (BDB/HDB) が採用されます。しかし、大規模なデータ量や既存の管理運用フローとの親和性を考慮し、PostgreSQL や MySQL をバックエンドとして利用することも可能です。その場合、特定のモジュール(moduleload など)を設定して外部データベースへの接続パスを指示する必要があります。

スキーマとオブジェクトクラスの定義

LDIF ファイルを使って情報をインポートする際、objectClass アトリビュートは定義されたスキーマに準拠している必要があります。inetOrgPersonorganizationalUnit などは、core.schemanis.schema などで定義されており、これを明示的に含めておくことで正常に解析されます。

よくあるトラブルシューティング

ケースセンシティブな認証設定

一般的に OpenLDAP の uid フィールドは大文字小文字を区別しません(caseIgnoreMatch)。しかし、セキュリティ要件によっては厳密なマッチングが必要になる場合があります。旧バージョンではスキーマを編集してマッチングルールを変更できましたが、新バージョンではコア部分にハードコードされているため、設定ファイルのみでの対応は困難です。

代替案として、独自の属性(例:userPrincipalId)を作成し、それにカスタムスキーマを適用してケースセンシティブな照合ルールを定義するのが現実的な解決策となります。

権限エラー (Invalid credentials)

新しいバージョンの OpenLDAP で「認証エラー」が発生する場合、設定データの再生成が必要なことがあります。これは旧型のコンフィグファイルを新しいディレクトリ形式(slapd.d)に変換しないまま起動しようとすると発生します。初期設定をクリアし、スキーマを再読み込みさせる手順を踏むことを推奨します。

クライアントツール

直接コマンドラインで操作せずに、以下のような管理ツールを使用して確認することが可能です。

  • グラフィカルクライアント: LdapAdmin や LDAP Browser などのデスクトップアプリ。
  • Web ベース: phpLDAPadmin などを使用すればブラウザから簡単にエンリティの検索や編集が行えます。

Java による LDAP 認証の実装

アプリケーション側で LDAP サーバーを検証するには、JNDI API を利用した Java 実装が一般的です。以下に、ユーザーのDN検索を行い、パスワード検証を行う簡易クラスを示します。ここでは接続プールの再利用ではなく、メソッド単位の確実なクローズを重視した設計例です。

package com.example.security;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class LdapAuthService {
    private final String ldapUrl;
    private final String baseDn;
    private final String bindDn;
    private final String bindPassword;
    private static final String FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";

    public LdapAuthService(String host, String port, String dn, String adminDn, String adminPass) {
        this.ldapUrl = String.format("ldap://%s:%s/", host, port);
        this.baseDn = dn;
        this.bindDn = adminDn;
        this.bindPassword = adminPass;
    }

    /**
     * 管理者権限で接続してユーザーを探索し、その後ユーザー権限で再接続を試みる
     */
    public boolean authenticateUser(String targetUid, String password) {
        LdapContext context = null;
        
        // ステップ 1: ユーザーの完全な Distinguished Name (DN) を取得するために管理アカウントで接続
        try {
            Map env = createEnvironment(bindDn, bindPassword, false);
            context = new InitialLdapContext(env.toArray(), null);

            // ステップ 2: uid でユーザーを検索
            String searchFilter = "(uid=" + targetUid + ")";
            SearchResult result = findUserByUid(context, searchFilter);

            if (result == null || result.getAttributes() == null) {
                return false;
            }

            // 見つかったユーザーの DN を抽出
            String userDn = result.getNameInNamespace();
            
            // ステップ 3: 取得した DN とパスワードで実際にバインドテストを行う
            System.out.println("Found DN: " + userDn);
            Map userEnv = createEnvironment(userDn, password, true);
            userEnv.put(Context.INITIAL_CONTEXT_FACTORY, FACTORY);
            
            LdapContext userContext = new InitialLdapContext(userEnv.toArray(), null);
            userContext.close(); // 接続切れチェック
            
            return true;

        } catch (NamingException e) {
            System.err.println("Authentication failed for " + targetUid);
            return false;
        } finally {
            if (context != null) {
                try { context.close(); } catch (NamingException ignore) {}
            }
        }
    }

    private Map createEnvironment(String principal, String credential, boolean simpleAuth) {
        Hashtable env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, FACTORY);
        env.put(Context.PROVIDER_URL, ldapUrl + baseDn);
        if (simpleAuth) {
            env.put(Context.SECURITY_AUTHENTICATION, "simple");
        } else {
            env.put(Context.SECURITY_AUTHENTICATION, "none"); // 検索時だけ必要なら調整可
        }
        env.put(Context.SECURITY_PRINCIPAL, principal);
        env.put(Context.SECURITY_CREDENTIALS, credential);
        // プロキシ認証防止のため、不要なオプションは除外
        return env;
    }

    private SearchResult findUserByUid(LdapContext ctx, String filter) throws NamingException {
        SearchControls controls = new SearchControls();
        controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
        controls.setReturningOnlyAttrs(new String[]{"dn"});
        
        var results = ctx.search("", filter, controls);
        if (results.hasMore()) {
            return (SearchResult) results.next();
        }
        return null;
    }

    public static void main(String[] args) {
        LdapAuthService service = new LdapAuthService(
            "192.168.1.10", 
            "389", 
            "dc=company,dc=local",
            "cn=admin,dc=company,dc=local",
            "secret_password"
        );
        
        boolean isSuccess = service.authenticateUser("john_doe", "password123");
        System.out.println("Login result: " + (isSuccess ? "Success" : "Failed"));
    }
}

その他の選択肢

開発言語固有の統合や Javaプラットフォームでの高性能な利用を希望する場合は、OpenDJ (現在は Forgerock 製品の一部) も検討対象となります。これらは LDAPv3 に準拠しつつ、Java ベースのネイティブサポートや高度なクラスタリング機能を備えています。

タグ: openldap JNDI java-authentication directory-service ldap-config

6月26日 00:34 投稿