1. 概要

Jakarta EE 8 Security APIは、Javaコンテナのセキュリティ問題を処理するための新しい標準であり、移植可能な方法です。

この記事では、APIの3つのコア機能を見ていきます。

  1. HTTP認証メカニズム
  2. アイデンティティストア
  3. セキュリティコンテキスト

まず、提供されている実装を構成する方法を理解し、次にカスタム実装を実装する方法を理解します。

2. Mavenの依存関係

Jakarta EE 8セキュリティAPIを設定するには、サーバー提供の実装または明示的な実装のいずれかが必要です。

2.1. サーバー実装の使用

Jakarta EE 8準拠のサーバーは、すでにJakarta EE 8セキュリティAPIの実装を提供しているため、必要なのは JakartaEEWebプロファイルAPIMavenアーティファクトのみです。

<dependencies>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-web-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

2.2. 明示的な実装の使用

まず、Jakarta EE 8 SecurityAPIのMavenアーティファクトを指定します。

<dependencies>
    <dependency>
        <groupId>javax.security.enterprise</groupId>
        <artifactId>javax.security.enterprise-api</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

次に、実装を追加します。たとえば、 Soteria –リファレンス実装です。

<dependencies>
    <dependency>
        <groupId>org.glassfish.soteria</groupId>
        <artifactId>javax.security.enterprise</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

3. HTTP認証メカニズム

Jakarta EE 8より前は、web.xmlファイルを介して宣言的に認証メカニズムを構成していました。

このバージョンでは、Jakarta EE 8 Security APIが、代わりに新しいHttpAuthenticationMechanismインターフェースを設計しました。 したがって、Webアプリケーションは、このインターフェイスの実装を提供することにより、認証メカニズムを構成できるようになりました。

幸い、コンテナには、サーブレット仕様で定義されている3つの認証方法(基本HTTP認証、フォームベース認証、カスタムフォームベース認証)のそれぞれの実装がすでに用意されています。

また、各実装をトリガーするための注釈も提供します。

  1. @BasicAuthenticationMechanismDefinition
  2. @FormAuthenticationMechanismDefinition
  3. @CustomFormAuthenrticationMechanismDefinition

3.1. 基本HTTP認証

上記のように、Webアプリケーションは、CDIBean@BasicAuthenticationMechanismDefinitionアノテーションを使用するだけで基本HTTP認証を構成できます。

@BasicAuthenticationMechanismDefinition(
  realmName = "userRealm")
@ApplicationScoped
public class AppConfig{}

この時点で、サーブレットコンテナは、提供されているHttpAuthenticationMechanismインターフェイスの実装を検索してインスタンス化します。

不正な要求を受信すると、コンテナはWWW-Authenticate応答ヘッダーを介して適切な認証情報を提供するようにクライアントに要求します。

WWW-Authenticate: Basic realm="userRealm"

次に、クライアントは、 Authorization リクエストヘッダーを介して、コロン「:」で区切られ、Base64でエンコードされたユーザー名とパスワードを送信します。

//user=baeldung, password=baeldung
Authorization: Basic YmFlbGR1bmc6YmFlbGR1bmc=

クレデンシャルを提供するために表示されるダイアログは、サーバーからではなくブラウザからのものであることに注意してください。

3.2. フォームベースのHTTP認証

@FormAuthenticationMechanismDefinitionアノテーションは、サーブレット仕様で定義されているフォームベースの認証をトリガーします。

次に、ログインページとエラーページを指定するか、デフォルトの妥当なページ /loginおよび/login-errorを使用するオプションがあります。

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(
    loginPage = "/login.html",
    errorPage = "/login-error.html"))
@ApplicationScoped
public class AppConfig{}

loginPage、を呼び出した結果、サーバーはフォームをクライアントに送信する必要があります。

<form action="j_security_check" method="post">
    <input name="j_username" type="text"/>
    <input name="j_password" type="password"/>
    <input type="submit">
</form>

次に、クライアントは、コンテナによって提供される事前定義されたバッキング認証プロセスにフォームを送信する必要があります。

3.3. カスタムフォームベースのHTTP認証

Webアプリケーションは、アノテーション @CustomFormAuthenticationMechanismDefinition:を使用して、カスタムフォームベースの認証実装をトリガーできます。

@CustomFormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.xhtml"))
@ApplicationScoped
public class AppConfig {
}

ただし、デフォルトのフォームベースの認証とは異なり、カスタムログインページを構成し、 SecurityContext.authenticate()メソッドをバッキング認証プロセスとして呼び出しています。

ログインロジックを含むバッキングLoginBeanも見てみましょう。

@Named
@RequestScoped
public class LoginBean {

    @Inject
    private SecurityContext securityContext;

    @NotNull private String username;

    @NotNull private String password;

    public void login() {
        Credential credential = new UsernamePasswordCredential(
          username, new Password(password));
        AuthenticationStatus status = securityContext
          .authenticate(
            getHttpRequestFromFacesContext(),
            getHttpResponseFromFacesContext(),
            withParams().credential(credential));
        // ...
    }
     
    // ...
}

カスタムlogin.xhtmlページを呼び出した結果、クライアントは受信したフォームを LoginBean’ s login()メソッドに送信します。

//...
<input type="submit" value="Login" jsf:action="#{loginBean.login}"/>

3.4. カスタム認証メカニズム

HttpAuthenticationMechanism インターフェースは、3つのメソッドを定義します。 最も重要なのはvalidateRequest()で、これは実装を提供する必要があります。

ほとんどの場合、他の2つのメソッド、 secureResponse()および cleanSubject()、のデフォルトの動作で十分です。

実装例を見てみましょう。

@ApplicationScoped
public class CustomAuthentication 
  implements HttpAuthenticationMechanism {

    @Override
    public AuthenticationStatus validateRequest(
      HttpServletRequest request,
      HttpServletResponse response, 
      HttpMessageContext httpMsgContext) 
      throws AuthenticationException {
 
        String username = request.getParameter("username");
        String password = response.getParameter("password");
        // mocking UserDetail, but in real life, we can obtain it from a database
        UserDetail userDetail = findByUserNameAndPassword(username, password);
        if (userDetail != null) {
            return httpMsgContext.notifyContainerAboutLogin(
              new CustomPrincipal(userDetail),
              new HashSet<>(userDetail.getRoles()));
        }
        return httpMsgContext.responseUnauthorized();
    }
    //...
}

ここで、実装は検証プロセスのビジネスロジックを提供しますが、実際には、 IdentityStoreHandler b yを呼び出してを介してIdentityStoreに委任することをお勧めします。 ]検証

また、CDI対応にする必要があるため、実装に@ApplicationScopedアノテーションを付けました。

クレデンシャルの有効な検証と、最終的なユーザーロールの取得の後、実装はコンテナに通知する必要があります

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

3.5. サーブレットセキュリティの実施

Webアプリケーションは、サーブレット実装で@ServletSecurityアノテーションを使用してセキュリティ制約を適用できます

@WebServlet("/secured")
@ServletSecurity(
  value = @HttpConstraint(rolesAllowed = {"admin_role"}),
  httpMethodConstraints = {
    @HttpMethodConstraint(
      value = "GET", 
      rolesAllowed = {"user_role"}),
    @HttpMethodConstraint(     
      value = "POST", 
      rolesAllowed = {"admin_role"})
  })
public class SecuredServlet extends HttpServlet {
}

このアノテーションには、httpMethodConstraintsvalueの2つの属性があります。 httpMethodConstraints は、1つ以上の制約を指定するために使用されます。各制約は、許可された役割のリストによってHTTPメソッドへのアクセス制御を表します。

次に、コンテナは、 url-pattern およびHTTPメソッドごとに、接続されたユーザーがリソースにアクセスするための適切な役割を持っているかどうかを確認します。

4. アイデンティティストア

この機能はによって抽象化されます IdentityStoreインターフェースであり、資格情報を検証し、最終的にグループメンバーシップを取得するために使用されます。 つまり、認証、承認、またはその両方の機能を提供できます。

IdentityStore は、呼び出されたIdentityStoreHandlerインターフェイスを介してHttpAuthenticationMecanismによって使用されることを目的としており、推奨されています。 IdentityStoreHandler のデフォルトの実装は、サーブレットコンテナによって提供されます。

アプリケーションは、 IdentityStore の実装を提供するか、データベースとLDAPのコンテナーによって提供される2つの組み込み実装のいずれかを使用できます。

4.1. ビルトインアイデンティティストア

Jakarta EE準拠のサーバーは、2つのIDストア(データベースとLDAP )の実装を提供する必要があります。

データベースIdentityStoreの実装は、構成データを@DataBaseIdentityStoreDefinitionアノテーションに渡すことによって初期化されます。

@DatabaseIdentityStoreDefinition(
  dataSourceLookup = "java:comp/env/jdbc/securityDS",
  callerQuery = "select password from users where username = ?",
  groupsQuery = "select GROUPNAME from groups where username = ?",
  priority=30)
@ApplicationScoped
public class AppConfig {
}

構成データとして、外部データベースへのJNDIデータソース、呼び出し元とそのグループをチェックするための2つのJDBCステートメント、最後に複数ストアの場合に使用される優先度パラメーターが構成されます。

優先度の高いIdentityStoreは、後でIdentityStoreHandlerによって処理されます。

データベースと同様に、 LDAP IdentityStore実装は、構成データを渡すことにより、@LdapIdentityStoreDefinitionを介して初期化されます。

@LdapIdentityStoreDefinition(
  url = "ldap://localhost:10389",
  callerBaseDn = "ou=caller,dc=baeldung,dc=com",
  groupSearchBase = "ou=group,dc=baeldung,dc=com",
  groupSearchFilter = "(&(member=%s)(objectClass=groupOfNames))")
@ApplicationScoped
public class AppConfig {
}

ここでは、外部LDAPサーバーのURL、LDAPディレクトリで呼び出し元を検索する方法、および呼び出し元のグループを取得する方法が必要です。

4.2. カスタムIdentityStoreの実装

IdentityStore インターフェースは、次の4つのデフォルトメソッドを定義します。

default CredentialValidationResult validate(
  Credential credential)
default Set<String> getCallerGroups(
  CredentialValidationResult validationResult)
default int priority()
default Set<ValidationType> validationTypes()

priority()メソッドは、この実装がIdentityStoreHandlerによって処理される反復の順序の値を返します。優先度の低いIdentityStoreが最初に処理されます。

デフォルトでは、 IdentityStore は、資格情報の検証(ValidationType.VALIDATE)とグループの取得( ValidationType.PROVIDE_GROUPS )の両方を処理します。 この動作をオーバーライドして、1つの機能のみを提供できるようにすることができます。

したがって、IdentityStoreを資格情報の検証にのみ使用するように構成できます。

@Override
public Set<ValidationType> validationTypes() {
    return EnumSet.of(ValidationType.VALIDATE);
}

この場合、 validate()メソッドの実装を提供する必要があります。

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map<String, UserDetails> users = new HashMap<>();

    @Override
    public int priority() {
        return 70;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.VALIDATE);
    }

    public CredentialValidationResult validate( 
      UsernamePasswordCredential credential) {
 
        UserDetails user = users.get(credential.getCaller());
        if (credential.compareTo(user.getLogin(), user.getPassword())) {
            return new CredentialValidationResult(user.getLogin());
        }
        return INVALID_RESULT;
    }
}

または、 IdentityStore を構成して、グループの取得にのみ使用できるようにすることもできます。

@Override
public Set<ValidationType> validationTypes() {
    return EnumSet.of(ValidationType.PROVIDE_GROUPS);
}

次に、 getCallerGroups()メソッドの実装を提供する必要があります。

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map<String, UserDetails> users = new HashMap<>();

    @Override
    public int priority() {
        return 90;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.PROVIDE_GROUPS);
    }

    @Override
    public Set<String> getCallerGroups(CredentialValidationResult validationResult) {
        UserDetails user = users.get(
          validationResult.getCallerPrincipal().getName());
        return new HashSet<>(user.getRoles());
    }
}

IdentityStoreHandlerは実装がCDIBeanであることを想定しているため、ApplicationScopedアノテーションで装飾します。

5. セキュリティコンテキストAPI

Jakarta EE 8セキュリティAPIは、SecurityContextインターフェイスを介してプログラムによるセキュリティへのアクセスポイントを提供します。 これは、コンテナによって適用される宣言型セキュリティモデルが十分でない場合の代替手段です。

SecurityContext インターフェースのデフォルトの実装は、実行時にCDI Beanとして提供される必要があるため、次のように挿入する必要があります。

@Inject
SecurityContext securityContext;

この時点で、ユーザーを認証し、認証されたユーザーを取得し、ユーザーの役割のメンバーシップを確認し、5つの利用可能な方法でWebリソースへのアクセスを許可または拒否できます。

5.1. 発信者データの取得

Jakarta EEの以前のバージョンでは、 Principal を取得するか、コンテナーごとに異なる役割のメンバーシップを確認していました。

サーブレットコンテナでHttpServletRequestgetUserPrincipal()および isUserInRole()メソッドを使用しますが、同様のメソッド getCallerPrincipal()また、EJBコンテナでは EJBContextisCallerInRole()メソッドが使用されます。

新しいJakartaEE8セキュリティAPIは、このによって標準化し、SecurityContextインターフェイスを介して同様のメソッドを提供します。

Principal getCallerPrincipal();
boolean isCallerInRole(String role);
<T extends Principal> Set<T> getPrincipalsByType(Class<T> type);

getCallerPrincipal()メソッドは、認証された呼び出し元のコンテナー固有の表現を返しますが、 getPrincipalsByType()メソッドは、指定されたタイプのすべてのプリンシパルを取得します。

アプリケーション固有の呼び出し元がコンテナーの呼び出し元と異なる場合に役立ちます。

5.2. Webリソースアクセスのテスト

まず、保護されたリソースを構成する必要があります。

@WebServlet("/protectedServlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "USER_ROLE"))
public class ProtectedServlet extends HttpServlet {
    //...
}

次に、この保護されたリソースへのアクセスを確認するには、 hasAccessToWebResource()メソッドを呼び出す必要があります:

securityContext.hasAccessToWebResource("/protectedServlet", "GET");

この場合、ユーザーがロールUSER_ROLE。にいる場合、メソッドはtrueを返します。

5.3. プログラムによる発信者の認証

アプリケーションは、 authenticate()を呼び出すことにより、プログラムで認証プロセスをトリガーできます。

AuthenticationStatus authenticate(
  HttpServletRequest request, 
  HttpServletResponse response,
  AuthenticationParameters parameters);

次に、コンテナに通知が送信され、アプリケーション用に構成された認証メカニズムが呼び出されます。  AuthenticationParameters パラメーターは、 HttpAuthenticationMechanism:へのクレデンシャルを提供します

withParams().credential(credential)

AuthenticationStatusSUCCESSおよびSEND_FAILURE値は、成功および失敗した認証を設計し、SEND_CONTINUEは認証プロセスの進行中のステータスを通知します。

6. 例の実行

これらの例を強調するために、JakartaEE8をサポートするOpenLibertyサーバーの最新の開発ビルドを使用しました。 これは、 liberty-maven-plugin のおかげでダウンロードおよびインストールされ、アプリケーションをデプロイしてサーバーを起動することもできます。

例を実行するには、対応するモジュールにアクセスして、次のコマンドを呼び出します。

mvn clean package liberty:run

その結果、Mavenはサーバーをダウンロードし、アプリケーションをビルド、デプロイ、実行します。

7. 結論

この記事では、新しいJakarta EE 8SecurityAPIの主な機能の構成と実装について説明しました。

まず、デフォルトの組み込み認証メカニズムを構成する方法と、カスタム認証メカニズムを実装する方法を示すことから始めました。 後で、組み込みのIDストアを構成する方法とカスタムストアを実装する方法を確認しました。 そして最後に、SecurityContext。のメソッドを呼び出す方法を見ました。

いつものように、この記事のコード例はGitHubから入手できます。