16.5. EJB 認証のために追加セキュリティーを提供する
デフォルトでは、アプリケーションサーバーにデプロイされた EJB にリモートコールを行う場合は、サーバーへの接続が認証され、この接続を介して受信されたすべての要求が、接続を認証したクレデンシャルを使用して実行されます。接続レベルでの認証は、基礎となる SASL (Simple Authentication and Security Layer) の機能に依存します。カスタム SASL メカニズムを記述する代わりに、サーバーに対する接続を開いて認証し、EJB を呼び出す前にセキュリティートークンを追加できます。このトピックでは、EJB 認証のために既存のクライアント接続で追加情報を渡す方法について説明します。
手順16.3 EJB 認証のためにセキュリティー情報を渡す
クライアントサイドインターセプターを作成する
このインターセプターは、org.jboss.ejb.client.EJBClientInterceptor
を実装する必要があります。インターセプターは、コンテキストデータマップを介して追加セキュリティートークンを渡すことが期待されます。このコンテキストデータマップは、EJBClientInvocationContext.getContextData()
への呼び出しを介して取得できます。追加セキュリティートークンを作成するクライアントサイドインターセプターコードの例は、以下のとおりです。public class ClientSecurityInterceptor implements EJBClientInterceptor { public void handleInvocation(EJBClientInvocationContext context) throws Exception { Object credential = SecurityActions.securityContextGetCredential(); if (credential != null && credential instanceof PasswordPlusCredential) { PasswordPlusCredential ppCredential = (PasswordPlusCredential) credential; Map<String, Object> contextData = context.getContextData(); contextData.put(ServerSecurityInterceptor.SECURITY_TOKEN_KEY, ppCredential.getAuthToken()); } context.sendRequest(); } public Object handleInvocationResult(EJBClientInvocationContext context) throws Exception { return context.getResult(); } }
クライアントインターセプターをアプリケーションに接続する方法については、 「アプリケーションでのクライアントサイドインターセプターの使用」を参照してください。サーバーサイドコンテナーインターセプターを作成および設定する
コンテナーインターセプタークラスは、単純な Plain Old Java Object (POJO) です。@javax.annotation.AroundInvoke
を使用して、Bean での呼び出し中に呼び出されるメソッドを指定します。コンテナーインターセプターの詳細については、 「コンテナーインターセプターについて」を参照してください。コンテナーインターセプターを作成する
このインターセプターは、コンテキストからセキュリティー認証トークンを取得し、認証のために JAAS (Java Authentication and Authorization Service) ドメインに渡します。コンテナーインターセプターコードの例は以下のとおりです。public class ServerSecurityInterceptor { private static final Logger logger = Logger.getLogger(ServerSecurityInterceptor.class); static final String SECURITY_TOKEN_KEY = ServerSecurityInterceptor.class.getName() + ".SecurityToken"; @AroundInvoke public Object aroundInvoke(final InvocationContext invocationContext) throws Exception { Principal userPrincipal = null; RealmUser connectionUser = null; String authToken = null; Map<String, Object> contextData = invocationContext.getContextData(); if (contextData.containsKey(SECURITY_TOKEN_KEY)) { authToken = (String) contextData.get(SECURITY_TOKEN_KEY); Connection con = SecurityActions.remotingContextGetConnection(); if (con != null) { UserInfo userInfo = con.getUserInfo(); if (userInfo instanceof SubjectUserInfo) { SubjectUserInfo sinfo = (SubjectUserInfo) userInfo; for (Principal current : sinfo.getPrincipals()) { if (current instanceof RealmUser) { connectionUser = (RealmUser) current; break; } } } userPrincipal = new SimplePrincipal(connectionUser.getName()); } else { throw new IllegalStateException("Token authentication requested but no user on connection found."); } } SecurityContext cachedSecurityContext = null; boolean contextSet = false; try { if (userPrincipal != null && connectionUser != null && authToken != null) { try { // We have been requested to use an authentication token // so now we attempt the switch. cachedSecurityContext = SecurityActions.securityContextSetPrincipalCredential(userPrincipal, new OuterUserPlusCredential(connectionUser, authToken)); // keep track that we switched the security context contextSet = true; SecurityActions.remotingContextClear(); } catch (Exception e) { logger.error("Failed to switch security context for user", e); // Don't propagate the exception stacktrace back to the client for security reasons throw new EJBAccessException("Unable to attempt switching of user."); } } return invocationContext.proceed(); } finally { // switch back to original security context if (contextSet) { SecurityActions.securityContextSet(cachedSecurityContext); } } } }
コンテナーインターセプターを設定する
サーバーサイドコンテナーインターセプターの設定方法については、 「コンテナーインターセプターの設定」を参照してください。
JAAS LoginModule を作成する
このカスタムモジュールは、既存の認証済み接続情報と追加セキュリティートークンを使用して認証を実行します。追加セキュリティートークンを使用し、認証を実行するコードの例は以下のとおりです。public class SaslPlusLoginModule extends AbstractServerLoginModule { private static final String ADDITIONAL_SECRET_PROPERTIES = "additionalSecretProperties"; private static final String DEFAULT_AS_PROPERTIES = "additional-secret.properties"; private Properties additionalSecrets; private Principal identity; @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { addValidOptions(new String[] { ADDITIONAL_SECRET_PROPERTIES }); super.initialize(subject, callbackHandler, sharedState, options); // Load the properties that contain the additional security tokens String propertiesName; if (options.containsKey(ADDITIONAL_SECRET_PROPERTIES)) { propertiesName = (String) options.get(ADDITIONAL_SECRET_PROPERTIES); } else { propertiesName = DEFAULT_AS_PROPERTIES; } try { additionalSecrets = SecurityActions.loadProperties(propertiesName); } catch (IOException e) { throw new IllegalArgumentException(String.format("Unable to load properties '%s'", propertiesName), e); } } @Override public boolean login() throws LoginException { if (super.login() == true) { log.debug("super.login()==true"); return true; } // Time to see if this is a delegation request. NameCallback ncb = new NameCallback("Username:"); ObjectCallback ocb = new ObjectCallback("Password:"); try { callbackHandler.handle(new Callback[] { ncb, ocb }); } catch (Exception e) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } return false; // If the CallbackHandler can not handle the required callbacks then no chance. } String name = ncb.getName(); Object credential = ocb.getCredential(); if (credential instanceof OuterUserPlusCredential) { OuterUserPlusCredential oupc = (OuterUserPlusCredential) credential; if (verify(name, oupc.getName(), oupc.getAuthToken())) { identity = new SimplePrincipal(name); if (getUseFirstPass()) { String userName = identity.getName(); if (log.isDebugEnabled()) log.debug("Storing username '" + userName + "' and empty password"); // Add the username and an empty password to the shared state map sharedState.put("javax.security.auth.login.name", identity); sharedState.put("javax.security.auth.login.password", oupc); } loginOk = true; return true; } } return false; // Attempted login but not successful. } private boolean verify(final String authName, final String connectionUser, final String authToken) { // For the purpose of this quick start we are not supporting switching users, this login module is validation an // additional security token for a user that has already passed the sasl process. return authName.equals(connectionUser) && authToken.equals(additionalSecrets.getProperty(authName)); } @Override protected Principal getIdentity() { return identity; } @Override protected Group[] getRoleSets() throws LoginException { Group roles = new SimpleGroup("Roles"); Group callerPrincipal = new SimpleGroup("CallerPrincipal"); Group[] groups = { roles, callerPrincipal }; callerPrincipal.addMember(getIdentity()); return groups; } }
カスタム LoginModule をチェーンに追加する
新しいカスタム LoginModule はチェーンの正しい場所に追加して正しい順序で呼び出されるようにする必要があります。この例では、SaslPlusLoginModule
は、password-stacking
オプションセットでロールをロードする LoginModule の前にチェーンする必要があります。管理 CLI を使用して LoginModule 順序を設定する
password-stacking
オプションを設定するRealmDirect
LoginModule の前にカスタムSaslPlusLoginModule
をチェーンする管理 CLI コマンドの例は以下のとおりです。[standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain:add(cache-type=default)
[standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain/authentication=classic:add
[standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain/authentication=classic/login-module=DelegationLoginModule:add(code=org.jboss.as.quickstarts.ejb_security_plus.SaslPlusLoginModule,flag=optional,module-options={password-stacking=useFirstPass})
[standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain/authentication=classic/login-module=RealmDirect:add(code=RealmDirect,flag=required,module-options={password-stacking=useFirstPass})
LoginModule 順序を手動で設定する
以下に、サーバー設定ファイルのsecurity
サブシステムで LoginModule 順序を設定する XML の例を示します。カスタムSaslPlusLoginModule
はRealmDirect
LoginModule より前に指定してユーザーロールがロードされ、password-stacking
オプションが設定される前にリモートユーザーを確認できるようにする必要があります。<security-domain name="quickstart-domain" cache-type="default"> <authentication> <login-module code="org.jboss.as.quickstarts.ejb_security_plus.SaslPlusLoginModule" flag="required"> <module-option name="password-stacking" value="useFirstPass"/> </login-module> <login-module code="RealmDirect" flag="required"> <module-option name="password-stacking" value="useFirstPass"/> </login-module> </authentication> </security-domain>
リモートクライアントを作成する
以下のコード例では、上記の JAAS LoginModule によりアクセスされるadditional-secret.properties
ファイルに以下のプロパティーが含まれることを前提とします。quickstartUser=7f5cc521-5061-4a5b-b814-bdc37f021acc
以下のコードは、EJB 呼び出しの前にセキュリティートークンを作成し、設定する方法を示しています。シークレットトークンはデモ目的のためにのみハードコーディングされています。このクライアントは、単に結果をコンソールに出力します。import static org.jboss.as.quickstarts.ejb_security_plus.EJBUtil.lookupSecuredEJB; public class RemoteClient { /** * @param args */ public static void main(String[] args) throws Exception { SimplePrincipal principal = new SimplePrincipal("quickstartUser"); Object credential = new PasswordPlusCredential("quickstartPwd1!".toCharArray(), "7f5cc521-5061-4a5b-b814-bdc37f021acc"); SecurityActions.securityContextSetPrincipalCredential(principal, credential); SecuredEJBRemote secured = lookupSecuredEJB(); System.out.println(secured.getPrincipalInformation()); } }