ベースの環境
以下の作業は、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つの作業を行います。
- login-command の定義
login command の実装として security.CustomLoginCommand クラスを指定します。
<login-command class="security.CustomLoginCommand" server="all"/> - 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 件のコメント:
コメントを投稿