- はじめに
- Security API とは
- AuthenticationMechanism
- CustomFormAuthenticationMechanismDefinition
- IdentityStore API
- DatabaseIdentityStoreDefinition
- 認証テーブルの作成
- まとめ
はじめに
Java EE 8 で導入された Security API(JSR-375) ですが、取りまとまった情報があまり無いため、ここにまとめます。
Jakarta EE とのバージョン対比は以下のようになります。
Version | EE Version |
---|---|
Jakarta Security 1.0 | Jakarta EE 8 |
Jakarta Security 2.0 | Jakarta EE 9 |
Jakarta Security 3.0 | Jakarta EE 10 |
Jakarta EE 10 では、OpenID Connect や OAuth2 の認証メカニズムが追加されたり、URL 別で認証方法を定義できるようになるなど多くの機能追加が予定されています。
RI は Soteria になります。
Security API とは
アプリケーションの認証方法は、旧来アプリケーションサーバで独自に実現されていましが、Java EE 8 時代に JSR-375 として共通 API が導入されました。
Security API では現在以下の機能が提供されています。
- 認証 API(AuthenticationMechanism)
- IdentityStore API
- SecurityContext API
AuthenticationMechanism は、ベーシック認証であったり、フォーム認証であったり、といった認証の手段を扱います。
IdentityStore では、データベースであったり、LDAPであったり、といった認証対象のIDストアを扱います。 クレデンシャルを検証してグループ・メンバーシップを取得するために使用されます。
SecurityContext はセキュリティへのアクセスポイントを提供します。 以前はばらばらだったセキュリティ機能へのアクセス手段がこの SecurityContext を経由して統一できます。 CDI や EL式を経由して SecurityContext に統一的にアクセスすることができます。
以下のようなAPIについては、現在は提供されていません。 今後のバージョンアップで追加されていくものとなります。
- パスワードエイリアス API
- ロール/パーミッション割当 API
- 認可インタセプター API
AuthenticationMechanism
AuthenticationMechanism は、認証の手段を扱います。これにはビルトインのサポートがあり、以下のアノテーションで簡単に機能実現できます。
- BasicAuthenticationMechanismDefinition
- FormAuthenticationMechanismDefinition
- CustomFormAuthenticationMechanismDefinition
BasicAuthenticationMechanismDefinition
はその名の通り、BASIC認証を扱います。
FormAuthenticationMechanismDefinition
は、Servlet で旧来からある、j_username
と j_password
を使った以下のようなフォームを扱うものです。
<form method="POST" action="j_security_check"> <input type="text" name="j_username"> <input type="password" name="j_password" autocomplete="off"> </form>
たいていの場合は CustomFormAuthenticationMechanismDefinition
を使うことになると思うので、CustomFormAuthenticationMechanismDefinition
を JSF と共に使うケースに絞って話を進めます。
ここでは深入りしませんが、上記の認証方式は、以下のインターフェースの実装を提供し、独自に認証を定義することもできるようになっています。
package jakarta.security.enterprise.authentication.mechanism.http; public interface HttpAuthenticationMechanism { AuthenticationStatus validateRequest( HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpMessageContext) throws AuthenticationException; default AuthenticationStatus secureResponse(HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpMessageContext) throws AuthenticationException { return SUCCESS; } default void cleanSubject(HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpMessageContext) { httpMessageContext.cleanClientSubject(); } }
CustomFormAuthenticationMechanismDefinition
CDI ビーンとして以下のように定義することで、カスタムフォーム認証が有効になります。
@CustomFormAuthenticationMechanismDefinition( loginToContinue = @LoginToContinue( loginPage="/login.xhtml", errorPage="/login.xhtml", useForwardToLogin = false ) ) @ApplicationScoped public class AuthenticationMechanismConfig { }
@LoginToContinue
アノテーションにて、ログインページやエラーページのURLを指定します。
useForwardToLogin
では、ログインページへのページ遷移にリダイレクトかフォワードの何れを使うかを指定します。詳しくは JavaDoc を参照ください。
@RememberMe
を合わせて利用することで、Cookie を使って、一定期間の再認証をスキップさせることもできます。
JSF のバッキングビーンは以下のように SecurityContext.authenticate()
を介して認証を行います。
@Model public class LoginModel { @NotNull private String username; @NotNull private String password; @Inject private SecurityContext securityContext; @Inject private FacesContext facesContext; @Inject private ExternalContext externalContext; public void login() throws Exception { Credential credential = new UsernamePasswordCredential(username, new Password(password)); externalContext.invalidateSession(); AuthenticationStatus status = securityContext.authenticate( (HttpServletRequest) externalContext.getRequest(), (HttpServletResponse) externalContext.getResponse(), AuthenticationParameters.withParams() .newAuthentication(true) .credential(credential)); switch (status) { case SEND_CONTINUE, NOT_DONE -> facesContext.responseComplete(); case SEND_FAILURE -> facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Authentication failed", null)); case SUCCESS -> { externalContext.redirect(externalContext.getRequestContextPath() + "/private/home.xhtml"); } } } // getter/setter }
認証成功時には AuthenticationStatus.SUCCESS
が得られます。
なお、現在は、web.xml
または web-fragment.xml
にて security-constraint
を定義する必要があります。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" version="5.0"> <security-constraint> <web-resource-collection> <web-resource-name>auth</web-resource-name> <url-pattern>/private/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> <role-name>user</role-name> </auth-constraint> </security-constraint> </web-app>
web-fragment.xml
の場合も定義は同様です。
<?xml version="1.0" encoding="UTF-8"?> <web-fragment xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-fragment_5_0.xsd" version="5.0"> <security-constraint> <web-resource-collection> <web-resource-name>auth</web-resource-name> <url-pattern>/private/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> <role-name>user</role-name> </auth-constraint> </security-constraint> </web-fragment>
/private/*
以下のURLへのアクセスには認証が必要になり、@CustomFormAuthenticationMechanismDefinition
に従って認証処理が行われます。
login.xml
は以下のような感じになります。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:jsf="http://xmlns.jcp.org/jsf"> <head jsf:id="idHead"> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>Login</title> </head> <body jsf:id="idBody"> <h2>Login to continue</h2> <h:messages /> <form jsf:id="form"> <p> <strong>Username </strong> <input jsf:id="username" type="text" jsf:value="#{loginModel.username}" /> </p> <p> <strong>Password </strong> <input jsf:id="password" type="password" jsf:value="#{loginModel.password}" /> </p> <p> <input type="submit" value="Login" jsf:action="#{loginModel.login}" /> </p> </form> </body> </html>
認証の方法は定義できたので、次に IdentityStore を見ていきます。
IdentityStore API
IdentityStore では、どのように クレデンシャルを検証しするかを定義します。 現在は以下のものがビルトインでサポートされています。
- DatabaseIdentityStoreDefinition
- LdapIdentityStoreDefinition
ここでは、一般的に用いられる DatabaseIdentityStoreDefinition
について見ていきます。
独自に実装する場合は、以下のインターフェースを、アプリケーションスコープの CDI ビーンとして実装することでカスタマイズできます。
package jakarta.security.enterprise.identitystore; public interface IdentityStore { Set<ValidationType> DEFAULT_VALIDATION_TYPES = EnumSet.of(VALIDATE, PROVIDE_GROUPS); default CredentialValidationResult validate(Credential credential) { try { return CredentialValidationResult.class.cast( MethodHandles.lookup() .bind(this, "validate", methodType(CredentialValidationResult.class, credential.getClass())) .invoke(credential)); } catch (NoSuchMethodException e) { return NOT_VALIDATED_RESULT; } catch (Throwable e) { throw new IllegalStateException(e); } } default Set<String> getCallerGroups(CredentialValidationResult validationResult) { return emptySet(); } default int priority() { return 100; } default Set<ValidationType> validationTypes() { return DEFAULT_VALIDATION_TYPES; } enum ValidationType { VALIDATE, PROVIDE_GROUPS } }
DatabaseIdentityStoreDefinition
以下のような CDI ビーン にアノテーションとして @DatabaseIdentityStoreDefinition
を付与することで、データベースでの認証が有効になります。
@DatabaseIdentityStoreDefinition( dataSourceLookup = java:app/App/DefaultDataSource, callerQuery = IdentityStoreConfig.CALLER_QUERY, groupsQuery = IdentityStoreConfig.GROUPS_QUERY, hashAlgorithmParameters = "${identityStoreConfig.hashAlgorithmParameters}" ) @Named @ApplicationScoped public class IdentityStoreConfig { public static final String CALLER_QUERY = """ select users.password from users where users.name = ?"""; public static final String GROUPS_QUERY = """ select groups.name from users join users_groups on users.id = users_groups.users_id join groups on users_groups.groups_id = groups.id where users.name = ?"""; public static Map<String, String> HASH_PARAMS = Map.of( "Pbkdf2PasswordHash.Iterations","3072", "Pbkdf2PasswordHash.Algorithm", "PBKDF2WithHmacSHA512", "Pbkdf2PasswordHash.SaltSizeBytes", "64"); public String[] getHashAlgorithmParameters() { return HASH_PARAMS.entrySet().stream() .map(e -> e.getKey() + "=" + e.getValue()).toArray(String[]::new); } }
callerQuery
でユーザ(Security API では Caller と呼ぶ)のパスワード取得SQL、groupsQuery
では Caller のグループを取得するSQLを定義します。
アノテーションの中ではEL式が使えるので、ハッシュアルゴリズムは、@Named の CDIビーンから供給できます。
認証テーブルの作成
DatabaseIdentityStoreDefinition で使う SQL に対応した エンティティ は以下のように定義できます。
@Entity(name = "USERS") public class User extends BaseEntity<User> { @NotNull @Size(min = 2, max = 25) private String name; @NotNull private String password; @ManyToMany private Set<Group> groups; // ... }
@Entity(name = "GROUPS") public class Group extends BaseEntity<Group> { @NotNull private String name; // ... }
初期データ登録は以下のように行なえます。
@Startup @Singleton public class UserInitializeService { @Inject private EntityManager em; @Inject private Pbkdf2PasswordHash passwordHash; @PostConstruct public void init() { TypedQuery<Long> query = em.createQuery("SELECT COUNT(e) FROM USERS e", Long.class); if (query.getSingleResult() == 0) { var adminGroup = Group.of("admin"); var userGroup = Group.of("user"); em.persist(adminGroup); em.persist(userGroup); passwordHash.initialize(IdentityStoreConfig.HASH_PARAMS); var admin = User.of("admin", passwordHash.generate("admin".toCharArray()), adminGroup, userGroup); var user = User.of("user", passwordHash.generate("user".toCharArray()), userGroup); em.persist(admin); em.persist(user); } } }
Pbkdf2PasswordHash
にて、パスワードのハッシュが生成できます。
まとめ
Jakarta EE Security API の簡単な利用方法について説明しました。
具体的な実装例は以下を参照してください。
以下のサンプルが確認できます。
ログイン画面
ログイン後