::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 적용 완료입니다!