ベースの環境
以下の作業は、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 件のコメント:
コメントを投稿