::Java Platform::/::Spring::
|
2009. 7. 13. 07:35
:: Spring BlazeDS Integration 1.0.0 Release(정식) 적용하기 (2) - Spring Security 연동 ::
무한삽질 끝에 Spring BlazeDS Integration 1.0에 핵심 기술 세가지(Remoting , Security , Message)중
Spring Security와의 연동도 해결했습니다.
Spring을 처음접할때 DI 다음으로 저를 놀라게한 기술이 Security인데 역시나 Doc은 부실하지만
예제가 탄탄해 쉽게 이해할 수 있었습니다.
Spring Security를 간단히 설명하자면 JSP코딩을 해보신 분들은 아시겠지만 매 페이지마다 세션 또는
쿠키에서 if문으로 수도없이 권한을 체크해보신적이 있을겁니다.
그러한 권한을 Spring이 관리해주면서 JSP에서는 간단한 태그 몇개로 이를 해결하도록한 기술인데
이를 Spring측에서 BlazeDS와도 연동을 할 수 있게 해주었습니다. 그냥 이뻐죽겠습니다 아주^^
플렉스에서는 Spring Security를 적용한 JSP페이지 만큼 소스코드를 줄일수는 없겠지만
권한 체크를 한곳에 집중시킬 수 있고 메소드별로 권한설정을 줄 수 있기에 상당히 유용하다고 봅니다.
자 이제 적용을 해보면서 차근차근 설명해보도록 하겠습니다.
우선 web.xml부터 가겠습니다.
security를 적용하기에 앞서 우선 security을 선언할 xml을 선언합니다.
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/applicationContext.xml
/WEB-INF/applicationContext-security.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/applicationContext.xml
/WEB-INF/applicationContext-security.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
여기서 주의 할 점은 xml을 contextConfigLocation에 추가하지 않고 아래와 같이
<servlet>
<servlet-name>MessageBrokerServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/config/web-application-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>MessageBrokerServlet</servlet-name>
<url-pattern>/messagebroker/*</url-pattern>
</servlet-mapping>
<servlet-name>MessageBrokerServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/config/web-application-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>MessageBrokerServlet</servlet-name>
<url-pattern>/messagebroker/*</url-pattern>
</servlet-mapping>
서블릿 부분에 선언한 xml에 기입하면 적용되지 않습니다.
서블릿에 작성하고도 적용가능한 방법을 아시는분은 꼭 좀 알려주세요~T.T
그 후 Spring Security 필터를 추가 합니다.
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
여기까지가 web.xml에 security를 사용하기위해 필수적으로 들어갈 부분입니다.
자이제 applicationContext-security.xml 를 작성해보도록 하겠습니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd">
<http auto-config="true" session-fixation-protection="none"/>
<authentication-provider>
<user-service>
<user name="john" password="john" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="guest" password="guest" authorities="ROLE_GUEST" />
</user-service>
</authentication-provider>
</beans:beans>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd">
<http auto-config="true" session-fixation-protection="none"/>
<authentication-provider>
<user-service>
<user name="john" password="john" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="guest" password="guest" authorities="ROLE_GUEST" />
</user-service>
</authentication-provider>
</beans:beans>
내용은 간단합니다. 기존에 jsp에서 Spring Security를 사용할때보다 추가해야될 부분이라고는
<http/> 에 session-fixation-protection="none" 이 전부입니다.
Doc에는 이와 다른 방법으로 적혀있는데 예제에는 위와 같아서 예제부분을 따라갔습니다.
<authentication-provider>는 기존 Spring Security 방식으로 jdbc를 이용해서 DB의 id와
password를 비교할 수도 있습니다.
<authentication-provider>
<jdbc-user-service data-source-ref = "dataSource"
users-by-username-query = "select id as username , password , 1 as enabled from raisondetre where id = ?"
authorities-by-username-query = "select id as username , 'ROLE_ADMIN' as authority from raisondetre where id = ?"/>
</authentication-provider>
<jdbc-user-service data-source-ref = "dataSource"
users-by-username-query = "select id as username , password , 1 as enabled from raisondetre where id = ?"
authorities-by-username-query = "select id as username , 'ROLE_ADMIN' as authority from raisondetre where id = ?"/>
</authentication-provider>
이제 Spring BlazeDS Integration에 Security를 적용해보면 우선 기존에 서블릿에 선언한
xml에 <flex:message-broker>등을 작성했다면 이내용을 모두 contextConfigLocation에
선언한 applicationContext.xml로 가져옵니다.
그렇게 하지않으면 Security를 적용해도 권한을 찾을 수 없다고 에러를 뿜어냅니다.
자이제 Security를 적용한 applicationContext에 전체코드 나갑니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flex="http://www.springframework.org/schema/flex"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/flex
http://www.springframework.org/schema/flex/spring-flex-1.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">
<flex:message-broker>
<flex:secured />
</flex:message-broker>
<bean id="test" class="test.Test">
<flex:remoting-destination destination-id="blaze"/>
<security:intercept-methods>
<security:protect method="getStr" access="ROLE_ADMIN" />
</security:intercept-methods>
</bean>
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
</beans>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flex="http://www.springframework.org/schema/flex"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/flex
http://www.springframework.org/schema/flex/spring-flex-1.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">
<flex:message-broker>
<flex:secured />
</flex:message-broker>
<bean id="test" class="test.Test">
<flex:remoting-destination destination-id="blaze"/>
<security:intercept-methods>
<security:protect method="getStr" access="ROLE_ADMIN" />
</security:intercept-methods>
</bean>
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
</beans>
추가된 부분은 정말 간단합니다.
메시지 브로커 아래에 secured를 추가하고
Security를 적용할 Service bean에 <security:intercept-methods>태그를 추가하고
로그인을 해야지만 호출할수 있는 메소드명을 적어줍니다.
<security:protect method> 태그는 여러개를 추가할 수 있고 method 속성에 메소드명
외에도 method="get*" 과 같이 get으로 시작하는 메소드전부 Spring AOP 방식으로 적용가능합니다.
자이제 mxml로 가서 security 부분을 적용해보겠습니다.
파일로도 첨부하겠지만 우선 기존에 적용한 BlazeDS_Hello.mxml에 Security를 적용한 소스 전체 나갑니다.
스크롤 압박 주의!
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" applicationComplete="creationCompleteHandler();">
<mx:RemoteObject id="ro" destination="blaze" fault="faultHandler(event)">
<mx:method name="getStr" result="resultHandler(event)"/>
</mx:RemoteObject>
<mx:Script>
<![CDATA[
import mx.rpc.remoting.mxml.RemoteObject;
import mx.controls.Alert;
import mx.rpc.events.ResultEvent;
import mx.rpc.events.FaultEvent;
import mx.messaging.ChannelSet;
import mx.messaging.channels.AMFChannel;
import mx.rpc.events.FaultEvent;
import mx.rpc.AsyncToken;
import mx.rpc.AsyncResponder;
[Bindable]
private var isPending:Boolean = true;
/**
* 에러가 났을 경우 이벤트 발생
* */
/*private function faultHandler(event:FaultEvent):void{
Alert.show(event.message.toString());
}*/
/**
* 제대로 실행 되었을 경우 이벤트 발생
* */
private function resultHandler(event:ResultEvent):void{
Alert.show(event.result.toString());
}
/**
* 버튼 클릭 후 RemoteObject Call
* */
private function remote_object_call():void{
// remoteObjecc에서 java method call
ro.getStr();
}
private function creationCompleteHandler():void
{
var channel:AMFChannel = new AMFChannel( "my-amf" , "http://localhost:8080/CairngormTest/messagebroker/amf" );
var channelSet:ChannelSet = new ChannelSet();
channelSet.addChannel( channel );
ro.channelSet = channelSet;
}
private function faultHandler(event:FaultEvent):void
{
if( event.fault.faultString.indexOf( "Access is denied" ) >= 0 )
{
Alert.show( "로그인을 해주세요." );
}
else
{
Alert.show( "서버와의 연결에 실패하였습니다." );
}
}
private function login():void
{
var asyncToken:AsyncToken = ro.channelSet.login( userId.text , password.text );
asyncToken.addResponder(
new AsyncResponder(
function( event:ResultEvent , token:Object = null ):void {
if ( event.result.authorities.indexOf( "ROLE_ADMIN" ) >= 0 )
{
isPending = false;
userId.text = "";
password.text = "";
Alert.show( "로그인 성공" );
}
else
{
Alert.show( "해당메뉴를 사용하실 권한이 없습니다." );
}
},
function( event:FaultEvent , token:Object = null ):void
{
var faultString:String = "로그인에 실패하셨습니다.\n실패이유 : ";
if( event.fault.faultString.indexOf( "Bad credentials" ) >= 0 )
{
faultString += "ID 또는 PASSWORD를 틀리셨습니다.";
}
else if( event.fault.faultString.indexOf( "Invalid credential format." ) >= 0 )
{
faultString += "ID 와 PASSWORD를 입력해주세요.";
}
else
{
faultString += "서버와의 연결에 실패하였습니다.";
}
Alert.show( faultString );
}
)
);
}
private function logout():void
{
var asyncToken:AsyncToken = ro.channelSet.logout();
isPending = true;
asyncToken.addResponder(
new AsyncResponder(
function( event:ResultEvent , token:Object = null ):void {
if ( event.result.indexOf( "success" ) >= 0 )
{
Alert.show( "로그아웃 성공" );
}
else
{
Alert.show( "로그아웃에 실패하였습니다." );
}
},
function( event:FaultEvent , token:Object = null ):void
{
Alert.show("Logout Failed: "+event.fault.faultString);
var faultString:String = "로그아웃에 실패하셨습니다.\n실패이유 : ";
if( event.fault.faultString.indexOf( "Send failed" ) >= 0 )
{
faultString += "로그아웃 정보를 서버에 전송하는데 실패하였습니다.\n이미 로그아웃 된 상태입니다.";
}
else
{
faultString += "서버와의 연결에 실패하였습니다.";
}
Alert.show( faultString );
}
)
);
}
]]>
</mx:Script>
<mx:Button label="서비스 시작" click="remote_object_call()"/>
<mx:Form>
<mx:FormItem label="User Id">
<mx:TextInput id="userId"/>
</mx:FormItem>
<mx:FormItem label="Password">
<mx:TextInput id="password" displayAsPassword="true"/>
</mx:FormItem>
<mx:FormItem direction="horizontal">
<mx:Button label="Login" click="login()" enabled="{this.isPending}"/>
<mx:Button label="Logout" click="logout()" enabled="{!this.isPending}"/>
</mx:FormItem>
</mx:Form>
</mx:Application>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" applicationComplete="creationCompleteHandler();">
<mx:RemoteObject id="ro" destination="blaze" fault="faultHandler(event)">
<mx:method name="getStr" result="resultHandler(event)"/>
</mx:RemoteObject>
<mx:Script>
<![CDATA[
import mx.rpc.remoting.mxml.RemoteObject;
import mx.controls.Alert;
import mx.rpc.events.ResultEvent;
import mx.rpc.events.FaultEvent;
import mx.messaging.ChannelSet;
import mx.messaging.channels.AMFChannel;
import mx.rpc.events.FaultEvent;
import mx.rpc.AsyncToken;
import mx.rpc.AsyncResponder;
[Bindable]
private var isPending:Boolean = true;
/**
* 에러가 났을 경우 이벤트 발생
* */
/*private function faultHandler(event:FaultEvent):void{
Alert.show(event.message.toString());
}*/
/**
* 제대로 실행 되었을 경우 이벤트 발생
* */
private function resultHandler(event:ResultEvent):void{
Alert.show(event.result.toString());
}
/**
* 버튼 클릭 후 RemoteObject Call
* */
private function remote_object_call():void{
// remoteObjecc에서 java method call
ro.getStr();
}
private function creationCompleteHandler():void
{
var channel:AMFChannel = new AMFChannel( "my-amf" , "http://localhost:8080/CairngormTest/messagebroker/amf" );
var channelSet:ChannelSet = new ChannelSet();
channelSet.addChannel( channel );
ro.channelSet = channelSet;
}
private function faultHandler(event:FaultEvent):void
{
if( event.fault.faultString.indexOf( "Access is denied" ) >= 0 )
{
Alert.show( "로그인을 해주세요." );
}
else
{
Alert.show( "서버와의 연결에 실패하였습니다." );
}
}
private function login():void
{
var asyncToken:AsyncToken = ro.channelSet.login( userId.text , password.text );
asyncToken.addResponder(
new AsyncResponder(
function( event:ResultEvent , token:Object = null ):void {
if ( event.result.authorities.indexOf( "ROLE_ADMIN" ) >= 0 )
{
isPending = false;
userId.text = "";
password.text = "";
Alert.show( "로그인 성공" );
}
else
{
Alert.show( "해당메뉴를 사용하실 권한이 없습니다." );
}
},
function( event:FaultEvent , token:Object = null ):void
{
var faultString:String = "로그인에 실패하셨습니다.\n실패이유 : ";
if( event.fault.faultString.indexOf( "Bad credentials" ) >= 0 )
{
faultString += "ID 또는 PASSWORD를 틀리셨습니다.";
}
else if( event.fault.faultString.indexOf( "Invalid credential format." ) >= 0 )
{
faultString += "ID 와 PASSWORD를 입력해주세요.";
}
else
{
faultString += "서버와의 연결에 실패하였습니다.";
}
Alert.show( faultString );
}
)
);
}
private function logout():void
{
var asyncToken:AsyncToken = ro.channelSet.logout();
isPending = true;
asyncToken.addResponder(
new AsyncResponder(
function( event:ResultEvent , token:Object = null ):void {
if ( event.result.indexOf( "success" ) >= 0 )
{
Alert.show( "로그아웃 성공" );
}
else
{
Alert.show( "로그아웃에 실패하였습니다." );
}
},
function( event:FaultEvent , token:Object = null ):void
{
Alert.show("Logout Failed: "+event.fault.faultString);
var faultString:String = "로그아웃에 실패하셨습니다.\n실패이유 : ";
if( event.fault.faultString.indexOf( "Send failed" ) >= 0 )
{
faultString += "로그아웃 정보를 서버에 전송하는데 실패하였습니다.\n이미 로그아웃 된 상태입니다.";
}
else
{
faultString += "서버와의 연결에 실패하였습니다.";
}
Alert.show( faultString );
}
)
);
}
]]>
</mx:Script>
<mx:Button label="서비스 시작" click="remote_object_call()"/>
<mx:Form>
<mx:FormItem label="User Id">
<mx:TextInput id="userId"/>
</mx:FormItem>
<mx:FormItem label="Password">
<mx:TextInput id="password" displayAsPassword="true"/>
</mx:FormItem>
<mx:FormItem direction="horizontal">
<mx:Button label="Login" click="login()" enabled="{this.isPending}"/>
<mx:Button label="Logout" click="logout()" enabled="{!this.isPending}"/>
</mx:FormItem>
</mx:Form>
</mx:Application>
하나하나 살펴보겠습니다.
우선 Application에 applicationComplete="creationCompleteHandler();" 이벤트를 추가해서
creationCompleteHandler메소드에서 RemoteObject에 ChannelSet을 추가해 줍니다.
이 때 URL에 주의하도록 합니다.
login logout 메소드는 더 간단히 할 수 있지만 실제로 프로젝트에 적용할 수 있는 수준으로
개조(?) 해봤습니다.
Security를 적용하실 정도의 실력이시면 저정도 소스는 간단히 이해하시리라 믿습니다!
이대로 실행을 시켜보시면 로그인이 되지 않은 상태에서는 getStr 메소드가 호출되지 않는것을
확인하실 수 있습니다.
이제 미리 설정해놓은 ID PASSWORD (john / john)를 입력하고 로그인을 하면
데이터를 불러올 수 있게됩니다.
이때 코딩시 주의점은 login이 된 상태에서 다시 login시도를 하면 런타임오류가 나타나니
로그인이 되면 다른페이지로 넘기시거나 버튼의 enable을 조절하시길 바랍니다.
logout은 여러번 시도해도 문제가 없는걸 확인했습니다.
이렇게 로그인에 성공을 하게 되면 event로 권한 내용을 얻을 수 있고 이를 저장해두고
페이지나 기능에 권한을 적용하고 Time을 걸어두면 웹서버 Session처럼 권한을 시간단위로
적용하는데 활용가능할 것 같습니다.
이를 적용한 workspace를 압축해서 올려두겠습니다.
제가 Cairngorm을 테스트하던 workspace라 BlazeDS_Hello와 관련된 부분만 보시면 됩니다.
실행은 CairngormTest.mxml로 하시면 됩니다.
실행하셔서 바로 데이터를 요청하면
로그인 하시면
로그아웃 하시면
위와 같은 창을 보시면 Security 적용 완료입니다!
CairngormTest(Spring BlazeDS Integration) by Raison d'etre .a00