2010年4月11日日曜日

GAE/J + BlazeDS 環境の Security 設定

今回は、GAE/J + BlazeDS 環境で Security の設定をしてみようと思います。

ベースの環境
以下の作業は、GAE/J + BlazeDS + Flash Builder 環境の構築で構築した環境をベースとします。


参考資料
Administering BlazeDS applications - Security - Configuring security


本日のメニュー

本日は、以下のメニューを一通りこなしたいと思います。
  • custom authentication に対応する。
  • 認証ロジックは、サンプルとして固定のものを使用する。(username = foo, password = bar)
  • role は使用しない。
  • Session 単位の認証のみ行い、FlexClient 単位の認証は行わない。
  • security-constraint は destination 単位ではなく service 単位でかける。

カスタム LoginCommand の作成

login command の実装クラスは、flex.messaging.security.LoginCommand インタフェースを実装している必要があります。

今回は、flex.messaging.security.LoginCommand インタフェースを implements した抽象クラスである flex.messaging.security.AppServerLoginCommand を継承してカスタムの LoginCommand を作成しようと思います。

CustomLoginCommand.java
package security;

import java.security.Principal;

import flex.messaging.security.AppServerLoginCommand;

public class CustomLoginCommand extends AppServerLoginCommand {

 @Override
 public Principal doAuthentication(final String username, Object credentials) {
  String password = extractPassword(credentials);
  boolean isValidUser = "foo".equals(username) && "bar".equals(password);
  return isValidUser ? new CustomPrincipal(username) : null;
 }

 @Override
 public boolean logout(Principal principal) {
  return true;
 }
}

CustomPrincipal.java
package security;

import java.io.Serializable;
import java.security.Principal;

class CustomPrincipal implements Principal, Serializable {
 private String name;
 public CustomPrincipal(String name) { this.name = name; }
 public String getName() { return null; }
 public int hashCode() { return name.hashCode(); }
 public boolean equals(Object obj) { return name.equals(obj); }
}

services-config.xml ファイルの設定

services-config.xml ファイルでは、以下の2つの作業を行います。
  1. login-command の定義
    login command の実装として security.CustomLoginCommand クラスを指定します。
    <login-command class="security.CustomLoginCommand" server="all"/>
  2. security-constraint の定義
    今回は role を使用しないので、security-constraint は id のみの指定となります。
    <security-constraint id="security_constraint"/>

services-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<services-config>

  <services>
    <default-channels><channel ref="amf" /></default-channels>
    <service-include file-path="remoting-config.xml" />
    <service-include file-path="messaging-config.xml" />
  </services>

  <channels>
    <channel-definition id="amf" class="mx.messaging.channels.AMFChannel">
      <endpoint url="http://{server.name}:{server.port}/{context.root}/messagebroker/amf" class="flex.messaging.endpoints.AMFEndpoint" />
      <properties>
        <polling-enabled>true</polling-enabled><!-- default is true (though the document doesn't say so) -->
      </properties>
    </channel-definition>
  </channels>

  <security>
    <login-command class="security.CustomLoginCommand" server="all"/>
    <security-constraint id="security_constraint"/>
  </security>

  <system>
    <!-- Workaround for GAE : java.lang.management.ManagementFactory is a restricted class. -->
    <manageable>false</manageable>
  </system>

</services-config>


各 service に対する security-constraint の設定

security-constraint は destination 毎に設定することもできますが、今回は service 単位で設定します。具体的には remoting service と messaging service に設定します。

設定方法は、各 <service> 要素の直下に以下のようにデフォルトの security-constraint を指定します。
<default-security-constraint ref="security_constraint"/>

以下、各 service の定義ファイル:
remoting-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<service id="remoting-service" class="flex.messaging.services.RemotingService">

  <default-security-constraint ref="security_constraint"/>
  
  <adapters>
    <adapter-definition id="java-object" class="flex.messaging.services.remoting.adapters.JavaAdapter" default="true" />
  </adapters>

  <destination id="echoServiceDestination">
    <properties>
      <source>server.EchoService</source>
      <scope>application</scope>
    </properties>
  </destination>

</service>

messaging-config.xml
<service id="message-service" class="flex.messaging.services.MessageService">

  <default-security-constraint ref="security_constraint"/>
  
  <adapters>
    <adapter-definition id="actionscript" class="flex.messaging.services.messaging.adapters.ActionScriptAdapter" default="true" />
  </adapters>

  <destination id="messageServiceDestination">
  </destination>

</service>


疎通確認用アプリケーションの開発(クライアント側)

GAE/J + BlazeDS + Flash Builder 環境の構築の Flex プロジェクトに、SecuredClient.mxml ファイルを作成します。
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
      xmlns:s="library://ns.adobe.com/flex/spark"
      xmlns:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%" creationComplete="init()" currentState="BeforeLogin">
 <s:layout>
  <s:VerticalLayout horizontalAlign="center" paddingLeft="24" paddingBottom="24" paddingRight="24" paddingTop="24" />
 </s:layout>
 <s:states>
  <s:State name="BeforeLogin"/>
  <s:State name="AfterLogin"/>
 </s:states>
 <fx:Declarations>
  <mx:Producer id="producer" destination="messageServiceDestination" fault="messagingFaultHandler(event);"/>
 </fx:Declarations>
 <fx:Script>
  <![CDATA[
   import mx.binding.utils.ChangeWatcher;
   import mx.controls.Alert;
   import mx.messaging.ChannelSet;
   import mx.messaging.config.ServerConfig;
   import mx.messaging.events.MessageAckEvent;
   import mx.messaging.events.MessageEvent;
   import mx.messaging.events.MessageFaultEvent;
   import mx.messaging.messages.AsyncMessage;
   import mx.rpc.AsyncResponder;
   import mx.rpc.AsyncToken;
   import mx.rpc.events.FaultEvent;
   import mx.rpc.events.ResultEvent;
   
   private var cs:ChannelSet;
   
   // Define an AsyncToken object.
   private var token:AsyncToken;
   
   // Initialize component
   private function init():void {
    cs = ServerConfig.getChannelSet("messageServiceDestination");
    if (cs.authenticated == true) {
     currentState = "AfterLogin";
    }
   }
   
   // Login
   private function login(username:String, password:String):void {
    if (cs.authenticated == false) {
     token = cs.login(username, password);
     token.addResponder(new AsyncResponder(LoginResultEvent, LoginFaultEvent));
    }
   }
   
   // Handle successful login.
   private function LoginResultEvent(event:ResultEvent, token:Object=null):void  {
    if (event.result != "success") return;
    currentState="AfterLogin"
   }
   
   // Handle login failure.
   private function LoginFaultEvent(event:FaultEvent, token:Object=null):void {
    if (event.fault.faultCode != "Client.Authentication") return;
    Alert.show("Login Failed!");
   }
   
   // Logout and handle success or failure.
   private function logout():void {
    // Add result and fault handlers.
    token = cs.logout();
    token.addResponder(new AsyncResponder(LogoutResultEvent,LogoutFaultEvent));
   }
   
   // Handle successful login.
   private function LogoutResultEvent(event:ResultEvent, token:Object=null):void  {
    if (event.result != "success") return;
    currentState="BeforeLogin"
   }
   
   // Handle login failure.
   private function LogoutFaultEvent(event:FaultEvent, token:Object=null):void {
    if (event.fault.faultCode != "Client.Authentication") return;
    Alert.show("Logout Failed!");
   }
   
   // Handle a message fault.
   private function messagingFaultHandler(event:MessageFaultEvent):void {
    Alert.show(event.message.faultString);
   }
   
   // Send the message in response to a Button click.
   private function sendMessage():void {
    producer.send(new AsyncMessage("This is only a test."));
   }
   
  ]]>
 </fx:Script>
 <mx:Form includeIn="BeforeLogin">
  <mx:FormItem label="username">
   <mx:TextInput id="userNameTi" focusIn="IME.enabled = false"/>
  </mx:FormItem>
  <mx:FormItem label="password">
   <mx:TextInput displayAsPassword="true" id="passwordTi" focusIn="IME.enabled = false" keyDown="IME.enabled = false"/>
  </mx:FormItem>
 </mx:Form>
 <s:Label includeIn="AfterLogin" text="Hi, {userNameTi.text}. You have logged in."/>
 <s:HGroup includeIn="BeforeLogin,AfterLogin">
  <s:Button label="Login" id="loginBtn" includeIn="BeforeLogin" click="login(userNameTi.text, passwordTi.text)"/>
  <s:Button includeIn="AfterLogin" label="Logout" id="logoutBtn" click="logout()"/>
  <s:Button includeIn="BeforeLogin,AfterLogin" label="Send Message" id="sendMessageButton" click="sendMessage();"/>
 </s:HGroup>
</s:Application>

以上。


注意点

独自の Principal を作成する場合、確実に serialize できるようにしましょう。その際、例外が発生しなかったからといって安心してはいけません。おいらのように、悲しい思いをすることになります。

下記のように、HttpFlexSession は writeObject() メソッドを独自に持って serialize を制御しています。
/**
 * Implements Serializable; only the Principal needs to be serialized as all
 * attribute storage is delegated to the associated HttpSession.
 *
 * @param stream The stream to read instance state from.
 */
private void writeObject(ObjectOutputStream stream)
{
    try
    {
        Principal principal = super.getUserPrincipal();
        if (principal != null && principal instanceof Serializable)
            stream.writeObject(principal);
    }
    catch (IOException e)
    {
        // Principal was Serializable and non-null; if this happens there's nothing we can do.
        // The user will need to reauthenticate if necessary.
    }
    catch (LocalizedException ignore)
    {
        // This catch block added for bug 194144.
        // On BEA WebLogic, writeObject() is sometimes invoked on invalidated session instances
        // and in this case the checkValid() invocation in super.getUserPrincipal() throws.
        // Ignore this exception.
    }
}

そして、writeObject 時に IOException を握りつぶしています。

ご注意ください。

参考: 作業ログ : BlazeDS のカスタム LoginCommand がなぜかうまく動かなくて調べた時のログ

付録
login-command 要素に server 属性を追加し忘れた時の例外。
javax.servlet.UnavailableException: Attribute 'server' must be specified for element 'login-command'.
 at flex.messaging.MessageBrokerServlet.init(MessageBrokerServlet.java:170)
 at org.mortbay.jetty.servlet.ServletHolder.initServlet(ServletHolder.java:440)
 at org.mortbay.jetty.servlet.ServletHolder.doStart(ServletHolder.java:263)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
 at org.mortbay.jetty.servlet.ServletHandler.initialize(ServletHandler.java:685)
 at org.mortbay.jetty.servlet.Context.startContext(Context.java:140)
 at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1250)
 at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:517)
 at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:467)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
 at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:130)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
 at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:130)
 at org.mortbay.jetty.Server.doStart(Server.java:224)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
 at com.google.appengine.tools.development.JettyContainerService.startContainer(JettyContainerService.java:185)
 at com.google.appengine.tools.development.AbstractContainerService.startup(AbstractContainerService.java:146)
 at com.google.appengine.tools.development.DevAppServerImpl.start(DevAppServerImpl.java:219)
 at com.google.appengine.tools.development.DevAppServerMain$StartAction.apply(DevAppServerMain.java:162)
 at com.google.appengine.tools.util.Parser$ParseResult.applyArgs(Parser.java:48)
 at com.google.appengine.tools.development.DevAppServerMain.(DevAppServerMain.java:113)
 at com.google.appengine.tools.development.DevAppServerMain.main(DevAppServerMain.java:89)

0 件のコメント:

コメントを投稿