[Spring Security] Domain ACL 실무판

Posted at 2010.08.18 01:04 // in OpenSource // by MOOVA 무바㏇
본 아티클은 ACL Spring Security tutorial 의 내용을 대부분 의역, 개선해서 편집하였습니다.

아직 한국에서는 Spring Security의 쓰임이 비교적 많지 않고, 실무에 쓰일 적당한 예제나 문서가 매우 빈약한 편입니다.
얼마전에 자바지기님이 블로그에 스프링 시큐리티에 대한 연재를 한 것과 동시에, 저도 지속적인 공유를 하기위해 스프링 시큐리티와 관련된 아티클을 정리해서 올려두었습니다.

스프링 시큐리티를 그대로 쓸 수 없는 빈약한 한국의 환경에도 불구하고 여전히 스프링 시큐리티가 가지고 있는 수 많은 옵션은 매우 매력적인 부분이 많이 있습니다. 관공서, SDS, LGT등 다수의 프로젝트를 진행하면서 종종 보안과 관련된 작업을 하면서 겉잡을 수 없을 많큼 중복되는 코드와 구 공통 개발자들이 만들어놓은 교착상태의 코드를 보고 스프링 시큐리티의 도입이 절실히 필요하다고 느꼈습니다. 차 후 보안 시스템이 복잡하게 되더라도 스프링 시큐리티가 제공하는 많은 옵션을 사용하면 예상치 못한 크리티컬한 문제점에 매우 유연하게 처리할 수 있으리라 기대해 봅니다.

시작

이 아티클은 PostrgeSQL을 사용하여 Spring Security 웹 애플리케이션으로 구축할 수 있는 간단한 방법을 보여주고 있습니다.. 효과적인 Spring Security를 위한 학습은 상당히 까다로운 편이여서 우리는 매우 초보적인 단계에서부터 독자들을 이해시키기 위해 본 아티클을 쓰기로 했습니다. 필자는 범위성 및 확장성있는 PostgreSQL의 완고한 팬으로서 본문을 공유하는 것이 매우 즐겁습니다. 대부분의 웹 어플리케이션(application)은, 인가되지 않은 사람들과 인가된 사람들의 행동을 분리하기 위해서 보안 시스템을 도입합니다.

보안 시스템의 가장 간단한 구조는 등록된 사용자가 메시지를 투고하거나, 미등록 사용자가 원하는 자원을 읽게 되거나, 관리자가 사용자를 차단하거나 자원을 수정, 삭제 금지하는 행위를 기본적으로 포함하고 있습니다. 보다 복잡한 애플리케이션에서는 확실한 접근을 위해 사용자가, 사용하게 될 클래스에 영역의 결정을 그리 쉽게 처리할 수 없는 문제입니다.
운이 좋게도 스프링 시큐리티는 위와같은 기본적인 구조를 가지고 있습니다. 추가로 ACL란 용어에 대해서 기본적으로 알아야 할 필요가 있는데 이것은 주어진 사용자가 필요한 도메인 오브젝트에 접근하기 위한 행동양식을 리스트로 나타낸 표와 같습니다. 우리는 스프링 시큐리티 ACL의 튜토리얼을 BSD라이센스인 spring starter 애플리케이션을 활용할 것입니다. 주어진 url로 부터 해당 샘플을 다운로드 받아 소스코드를 빌드하고 배포해 봅시다. (배포방법은 생략)

이 애플리케이션에서 구현하게 될 것은 간단한 인터넷뱅킹 시스템입니다.. 인터넷뱅킹을 주로 이용하는 사용자는 직원과 고객입니다.. 두 종류의 사용자는 애플리케이션에서 공통의 User라는 개념에 포함되지만 사실은 뱅킹시스템에 접근하는 목적이 다릅니다. 두 가지 타입을 분류해 놓기 위해서 User 를 상속한 Clerk와 Customer 클래스로 타입을 분류해 놓습니다.
Clerk와 Customer는 은행예금계좌(bank account)에 접근할 수 있고, 각각의 은행예금계좌는 은행업무와 관련된 로직을 포함하고 있습니다.. (예금이나 철회등)

Clerk는 Full Admin권한으로 고객은행예금계좌(create,read,update,delete)를 완전하게 관리할 수 있지만,  은행예금계좌의 조작은 read-only로 제한을 받습니다.

다음의 클래스 다이어그램에서 AbstractSecureObject를 베이스로한 상속관계를 살펴볼 수 있습니다.

Figure 1 . AbstractSecureObject를 상속한 도메인 오브젝트s

위의 그림에서 Clerk, Customer, BankAccount, BankAccountOpperation 클래스는 각각의 의미를 가지고 있습니다. Clerk와 Customer는 Authority-s의 리스트를 가지고 있는 Users로 정확하게 대응됩니다. User와 Authority는 N:N 관계로서 하나의 Users는 다수의 Authority를 포함할 수 있고, 하나의 Authority는 다수의 Users를 포함할 수 있습니다. 클래스다이 그램은 users와 authority를 단일의 클래스로 나타낼 수 있지만 데이터베이스는 3개의 테이블(Users, Authority, Users_Authority)이 필요할 것입니다. Customer객체는 BankAccountOperation-s의 집합체를 가지고 있는 BankAccount의 키와 연관이 됩니다. 애플리케이션에 걸쳐 보안 관련 정보의 식별을 처리하기 위해서, Clerk,Customer,BankAccount, BankAccountOperation는 AbstractSecureObject라고 하는 공통된 추상화 클래로부터 상속되어집니다. 이것은 Spring Security에서 연동을 위한 DomainObject를 단일화하고 통일화하기 위한 설계입니다.

자..이제 Spring Security와 연동하기 위해서 비즈니스 로직을 작성해야 할 필요가 있다. Spring Security는 일련의 보안관련정보를 저장하기 위해서 데이터베이스 테이블을 이용하고 테이블을 생성할 것입니다.

Springstarter application에서 acl.sql파일을 postgresql로 임포트를 하면 apring acl에 관련된 테이블들이 작성되어집니다. ( 만약 데이터베이스가 postgre이외의 것, mysql이나 sqlite등의 데이터베이스라면 우리는 그에 적절한 드라이버를 다운로드 받아 애플리케이션 WEB-INF/lib의 위치에 넣어 두어야 합니다. 그리고 해당 데이터베이스 환경에 맞게, hibernate.properties를 수정해야합니다. 본고에서는 기본으로 설정되어 있는 SpringStarter 애플리케이션의 수정없이 환경을 동일하게 적용하여 진행하기로 합니다.

http://www.postgresql.org/ 최신버전
hibernate.properties 의 데이터베이스 설정환경 (springstarter 데이터베이스를 생성하고, 유저는 postgres를 그대로 사용함)

hibernate.connection.url=jdbc:postgresql://localhost:5432/springstarter
hibernate.connection.driver_class=org.postgresql.Driver
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
hibernate.connection.username=postgres
hibernate.connection.password=

데이터베이스 테이블 설명 :

DROP TABLE ACL_ENTRY;
DROP TABLE ACL_OBJECT_IDENTITY;
DROP TABLE ACL_CLASS;
DROP TABLE ACL_SID;

ACL_SID 테이블은 Spring security를 사용하는 모든 사용자(Users)들을 sid기준으로 목록화합니다. SID는 사용자가 요구한 오브젝트를 수행하기 위해서 ACL에서 사용될 수 있으며, 사실상 sid는 애플리케이션에 있어서 액션을 수행하는 유저와 Role로 구별될 수 있습니다. 이 두가지 구별은 ACL_SID 테이블에 principal 컬럼의 boolean값으로 구별합니다. principal이: true라면 sid가 user의 키값으로 적재될 수 있으며, false라면 granted authority 를 의미합니다. 알아두어야 할 부분은 이 테이블은 사용자의 세부정보(이름,패스워드,연관된 자세한 사용자 정보들..)를 포함하지 않는다. 그것은 단지 users나 authority의 id를 사용할 뿐이다.

CREATE TABLE ACL_SID (
    id BIGSERIAL NOT NULL PRIMARY KEY,
    principal BOOLEAN NOT NULL,
    sid VARCHAR(100) NOT NULL,
    CONSTRAINT UNIQUE_UK_1 UNIQUE( sid, principal)
);


사용자를 목록화하고, 그밖에 사용자가 수행하는 오브젝트를 목록화 하기 위해, 우리는 도메인 오브젝트와 관련된 자바 클래스와 그 클래스가 인스턴스된 상태의 값을 데이터베이스에 적재해야합니다. 이는 Spring Security ACL에서 권장하고 있는 ACL_CLASS 테이블과 ACL_OBJECT_IDENTITY테이블로 모델링 할 수 있습니다. 이렇게 ACL를 위한 기본적인 테이블 생성은 Springstarter 애플리케이션을 위해, 또는 Spring Security가 인식할 수 있는 고 수준의 기법입니다.

ACL_CLASS 테이블안에 class컬럼은 자바의 풀 패키지명과 클래스이름을 기록합니다.(예 : com.denksoft.springstarter.BankAccount ) 이것은 Domain Object의 유일한 식별이름입니다. 각각의 풀 클래스이름에 구별될 수 있는 id를 지정하는 이유는 Spring Security에서 키를 사용해서 이 테이블을 접근 할 수 있기 때문입니다.

CREATE TABLE ACL_CLASS (
        id BIGSERIAL NOT NULL PRIMARY KEY,
        class VARCHAR NOT NULL,
        CONSTRAINT UNIQUE_UK_2 UNIQUE(class)
);

보안하고자 하는 시스템의 오브젝트는 ACL_OBJECT_IDENTITY 테이블에 단일하게 식별되어어서 등록됩니다. 해당테이블은 ACL_CLASS의 식별자 id를 참조키로 지정한 object_id_class 컬럼과, 실제 클래스가 수행된(예:BankAccount 테이블) 테이블의 참조키로 지정된 object_id_identity 컬럼, 각각의 오브젝트의 책임자인 owner_id컬럼이 준비되어 있습니다. 만약에 준비된 오븐젝트에 중첩된 구조를 표현하고자 한다면, parent_object와 entries_inheriting컬럼을 활용합니다.

CREATE TABLE ACL_OBJECT_IDENTITY (
        id BIGSERIAL NOT NULL PRIMARY KEY,
        object_id_class BIGINT NOT NULL,
        object_id_identity BIGINT NOT NULL,
        owner_sid BIGINT,
        parent_object BIGINT,
        entries_inheriting BOOLEAN NOT NULL,
        CONSTRAINT UNIQUE_UK_3 UNIQUE ( object_id_class , object_id_identity ),
        CONSTRAINT FOREIGN_FK_1 FOREIGN KEY ( parent_object ) REFERENCES ACL_OBJECT_IDENTITY(id),
        CONSTRAINT FOREIGN_FK_2 FOREIGN KEY ( object_id_class ) REFERENCES ACL_CLASS(id),
        CONSTRAINT FOREIGN_FK_3 FOREIGN KEY ( owner_sid ) REFERENCES ACL_SID(id)
);


최종적으로, 모든 사용자와 모든 보안관련 테이블을 참조해서 요청을 한 사용자로 하여금 각각의 오브젝들을 취급(create,update,delete,read)가능하게 해 줄 ACL_ENTRY테이블을 설계합니다.

(필드acl_object_identity)은 ACL_OBJECT_IDENTITY를 참조키로 등록하고, 그것을 수행할 수 있는 유저의 SID또한 sid컬럼에 참조키로 연관짖습니다. Mask는 별도로 관리된 비트숫자로 등록할 수 있습니다. Mask는 퍼미션에 관련된 컬럼으로써 예를들어 8비트의 00000101이 등록되어 있으면, 수행하는 액션은 0과 2를 허락하고 1은 허락하지 않습니다. Mask지정하는데 좋은 참고가 되는 것은 OS에서 사용되는 Security 관리 방법입니다. 이것은 리눅스나 유닉스, 윈도우환경에서 모두 비슷한 처리방식을 가지고 있기 때문에 OS의 보안관련사항을 학습하시면 Mask적용방법을 이해하는데 수월할 것으로 예상됩니다.

2010:08:17 07:31:55

 ACL_ENTRY테이블에서 granting 컬럼이 true로 지정되어 있으면 mask로 지정된 허가를 수행하며, false일 경우 그것을 취소하고 블록킹합니다. 운좋게도 ace_order필드는 접근 권한 엔트리의 우선순위를 결정하는 컬럽입니다.

CREATE TABLE ACL_ENTRY (
        id BIGSERIAL NOT NULL PRIMARY KEY,
        acl_object_identity BIGINT NOT NULL,
        sid BIGINT NOT NULL,
        mask INTEGER NOT NULL,
        ace_order INT NOT NULL,
        granting BOOLEAN NOT NULL,
        audit_success BOOLEAN NOT NULL,
        audit_failure BOOLEAN NOT NULL,
        CONSTRAINT UNIQUE_UK_4 UNIQUE(acl_object_identity,ace_order),
        CONSTRAINT FOREIGN_FK_4 FOREIGN KEY(acl_object_identity) REFERENCES ACL_OBJECT_IDENTITY(id),
        CONSTRAINT FOREIGN_FK_5 FOREIGN KEY(sid) REFERENCES ACL_SID(id)
);

아래 그림은 AbstractSecurityobject를 상속한 클래스들과 데이터베이스에 구축한 테이블의 데이터들이 어떻게 유기적으로 맞물리고 있는지 알기쉽게 표현한 그림입니다.

Spring Security configuration
security-config.xml 파일에 ACL의 기본적인 구축을 위해 사전에 필요한 설정을 해야합니다. 본 설정의 보다 자세한 사항은 다음의 포스트를 참조해 주십시오.

( Spring Security - URL 권한을 DB로 관리하기 1. Spring Security - URL 권한을 DB로 관리하기 2 )

<?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"
                 xmlns:p="http://www.springframework.org/schema/p"
                 xsi:schemaLocation="http://www.springframework.org/schema/beans
                 http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                 http://www.springframework.org/schema/security
                 http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">
         <global-method-security secured-annotations="enabled"/>
         <authentication-provider user-service-ref="userDetailsServiceWrapper" />
         <http>
             <intercept-url pattern="/app/clerk/**" access="ROLE_CLERK"/>
             <intercept-url pattern="/app/customer/**" access="ROLE_CUSTOMER"/>
             <intercept-url pattern="/app/public/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
             <anonymous/>
             <form-login login-processing-url="/login" login-page="/login.jsp" default-target-url="/app/index.task" />
             <logout logout-url="/logout" logout-success-url="/app/index.task" />
         </http>
</beans:beans>

위의 설정으로 기본적인 인증과 인가에 관련된 로그인 매커니즘을 스프링 시큐리티가 알아서 제공해 줍니다.

Authorization configuration file

다음으로 설정될 항목은 objectManagerSecurity입니다. 이 빈의 등록으로 보안 매커니즘을 시작할 수 있는 인터셉터가 생긴 것과 다름없다고 볼 수 있습니다. 이것은 스프링시큐리티에서 ACL을 시작할 수 있는 뿌리가 됩니다.objectManagerSecurity 빈은  org. springframework. security. intercept. AbstractSecurityInterceptor을 확장한 클래스입니다. 자세한 학습을 위해 API를 참조할 수도 있겠지만, 지금은 이 빈을 등록함으로써 모든 보안 요청을 가로채기에 충분함으로 api에 대한 자세한 설명은 레퍼런스를 참고하시기 바랍니다.

<bean id="objectManagerSecurity" class="org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor" autowire="byType">
        <property name="accessDecisionManager" ref="businessAccessDecisionManager"/>
        <property name="afterInvocationManager" ref="afterInvocationManager"/>
        <property name="objectDefinitionSource" ref="objectDefinitionSource"/>
</bean>
<bean id="objectDefinitionSource" class="org.springframework.security.annotation.SecuredMethodDefinitionSource" />

또한, 이 설정을 가능하게 하기 위해 secure-config.xml에 다음과 같이 설정을 합니다.

objectManagerSecurity, 즉 businessAccessDecisionManager에 의해 사용된 Access Decison Manager는, 요청을 한 사용자가, 요청될 오브젝트에 조작을 실행할 수 있는지 아닌지를 판단합니다. 조작 허용의 판단 여부는 아래 그림에서 볼 수 있듯이 Voter들에 의해서 결정됩니다.


<bean id="businessAccessDecisionManager" class="org.springframework.security.vote.UnanimousBased">
        <property name="allowIfAllAbstainDecisions" value="true"/>
        <property name="decisionVoters">
            <list>
                <ref local="roleVoter"/>
                <ref local="aclObjectReadVoter"/>
                <ref local="aclObjectWriteVoter"/>
                <ref local="aclObjectDeleteVoter"/>
                <ref local="aclObjectAdminVoter"/>
            </list>
        </property>
</bean>


Voter의 상위 타입인 AccessDecisionVoter 인터페이스를 살펴보면 정적인 상수 필드를 vote메소드를 통해서 최종 값으로 리턴하고 있습니다. 정적인 상수 필드는 ACCESS_ABSTAIN, ACCESS_DENIED, ACCESS_GRANTED의 3개가 기본으로 준비되어 있습니다.

public interface AccessDecisionVoter {

    int ACCESS_GRANTED = 1;
    int ACCESS_ABSTAIN = 0;
    int ACCESS_DENIED = -1;
    boolean supports (ConfigAttribute attribute);

    boolean supports (Class clazz);
    int vote (Authentication authentication, Object object, ConfigAttributeDefinition config);
}

각각의 votwer는, 그것을 동작하게 만드는 퍼미션 리스트를 가지고 있습니다  그리고 ACL-s를 조회하기 위해서 사용되는 aclService를 참조합니다.

각각의 voter는 퍼미션 리스트를 가지고 있고, ACL을 조회하기 위해서 사용되는 aclService를 참조합니다. 이 퍼미션의 정의는 aclService에 의해 되돌려진 데이터에 기초를 두고 있습니다. 아래 코드에서 보는 바와 같이 voter는 하나의 상수값을 리턴하고, 현재 요청에 대해 접근을 가능하게 할 것인지 말 것인지 그 의견을 이야기합니다.

org.springframework.security.vote.AclEntryVoter 클래스의 vote 메소드.

// Obtain the OID applicable to the domain object
ObjectIdentity objectIdentity = objectIdentityRetrievalStrategy.getObjectIdentity(domainObject);

// Obtain the SIDs applicable to the principal
Sid[] sids = sidRetrievalStrategy.getSids(authentication);

Acl acl;

try {
   // Lookup only ACLs for SIDs we're interested in
 acl = aclService.readAclById(objectIdentity, sids);
} catch (NotFoundException nfe) {
if (logger.isDebugEnabled()) {
   logger.debug("Voting to deny access - no ACLs apply for this principal");
}

return AccessDecisionVoter.ACCESS_DENIED;
}

try {
if (acl.isGranted(requirePermission, sids, false)) {
 if (logger.isDebugEnabled()) {
    logger.debug("Voting to grant access");
 }

 return AccessDecisionVoter.ACCESS_GRANTED;
} else {
 if (logger.isDebugEnabled()) {
    logger.debug(
     "Voting to deny access - ACLs returned, but insufficient permissions for this principal");
 }

 return AccessDecisionVoter.ACCESS_DENIED;
}
} catch (NotFoundException nfe) {
if (logger.isDebugEnabled()) {
   logger.debug("Voting to deny access - no ACLs apply for this principal");
}

  return AccessDecisionVoter.ACCESS_DENIED;
}



이 리턴된 결과로 접근 및 부정 접근에 관한 최종적인 결정은, voter들의 리턴된 값을 가지고 있는 decision manager에 의해 다시한번 결정권한을 이관합니다. 여기에서 사용된 빈은 org.springframework.security.vote.AbstractAccessDesicionManager을 상속한 org.springframework.security.vote.UnanimousBased빈 입니다. AbstractAccessDesicionManager 빈은 다시한번 AccessDecisionManager을 상속하므로 이 빈은 모든 voter들에게 접근이 허용되었을 때 접근 가능의 유무를 판단하는 로직이 담겨있습니다.

allowIfAllAbstainDecisions를 true로 설정되어 있으면 checkAllowIfAllAbstainDecisions() 메소드에 의해 최종 접근에 대한 접근을 허용합니다. 만약 voter들이 모두 기권을 선언할지라도 이 프로퍼티의 설정이 true로 설정되어 있으면 그것들의 의견을 무시하고 통과 시켜버립니다.

각각의 AclEntryVoter는 생성자에 중요한 세개의 파라미터를 받습니다.
첫번째 파라미터는 aclService, 두번째는 보안된 액션의 상수 값, 세번째는 액션을 실행할 때 참조하는 퍼미션의 리스트입니다.

processDomainObjectClass는 Spring Security Domain ACL에서 보안을 통제하고 싶은 도메인 오브젝트를 지정하는 프로퍼티입니다. 위의 클래스 다이어그램에서 볼 수 있듯이 보안과 관련된 모든 도메인 오브젝트는 AbstractSecureObject를 상속하고 있으므로 com.denksoft.springstarter.entity.AbstractSecureObject의 풀이름을 넣어줍니다.

<bean id="aclObjectReadVoter" class="org.springframework.security.vote.AclEntryVoter">
        <constructor-arg ref="aclService"/>
        <constructor-arg value="ACL_OBJECT_READ"/>
        <constructor-arg>
            <list>
                <ref local="administrationPermission"/>
                <ref local="readPermission"/>
            </list>
        </constructor-arg>
        <property name="processDomainObjectClass" value="com.denksoft.springstarter.entity.AbstractSecureObject"/>
</bean>

우리의 애플리케이션에는 위의 aclObjectReadVoter와 같은 형태로 생긴 3개의 decision voter들이 준비되어 있습니다. 만약 오브젝트를 쓰기위한 퍼미션을 설절하려면 ACL_OBJECT_READ의 값을 ACL_OBJECT_WRITE로 변경해 주고 생성자에 writePermission과 administrationPermission으로 변경해 줍니다.

일반적인 role-based voter의 roleVoter는 다른 voter들과 명확하게 분리되어 있습니다.현재 principal의 인증된 권한을 증명하기 위해 ROLE_*된 설정을 읽어들입니다.

<bean id="roleVoter" class="org.springframework.security.vote.RoleVoter"/>

public class RoleVoter implements AccessDecisionVoter {
       private String rolePrefix = "ROLE_";
       ....생략....
}


우리는 애플리케이션에 걸쳐 사용될 ACL 퍼미션(admin,read,write,delete)을 빈으로 등록하고 그것을 사용하는 AclEntryVoter의 생성자에 넘겨줄 수 있습니다. org.springframework.security.acls.Permission에 상수로 정의되어 있는 스태틱 필드의 Permission을, 이 값을 조회하는 FieldretrievingFactoryBean에 등록하여 사용합니다.

<bean id="administrationPermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
        <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
</bean>

<bean id="readPermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
 <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.READ"/>
</bean>

<bean id="writePermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
 <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.WRITE"/>
</bean>

<bean id="deletePermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
 <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.DELETE"/>
</bean>

AclService는, ACL-secured instance를 보조해 주는 역할을 합니다. MutableAclService는, 데이터베이스의 정보를 작성하고, 축적하기 위해서 사용됩니다.  Spring security의 디폴트 implementation 인 JdbcMutableAclService는 기본적인 일련의 쿼리를 포함하고 있지만, 이 쿼리는 PostgreSQL을 위한 쿼리가 아니고 세터인젝션이나 게터인젝션이 마련되어 있지 않습니다. 그래서 MutableAclService를 구현한 별도의 사용자 정의 서비스를 작성합니다. PostgreSqlJdbcMutableAclService는 SpringStarter application에서 찾아 볼 수 있습니다.

<bean id="aclService" class="com.denksoft.springstarter.util.security.PostgresqlJdbcMutableAclService">
        <constructor-arg ref="dataSource"/>
        <constructor-arg ref="lookupStrategy"/>
        <constructor-arg ref="aclCache"/>
</bean>

추가로 데이터 소스(data source) 프로퍼티를 설정하기 위해서, hibernate.properties파일의 내용을 수정하여 등록하여 줍니다.

<context:property-placeholder location="WEB-INF/classes/hibernate.properties"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
          p:driverClassName="${hibernate.connection.driver_class}"
          p:url="${hibernate.connection.url}"
          p:username="${hibernate.connection.username}"
          p:password="${hibernate.connection.password}"/>

lookupStrategy 는 데이터베이스로부터 직접 ACL의 리스트를 조회합니다. 우리는 스프링 시큐리티의 BasicLookupStrategy를 사용합니다. 또한 dataSource 와 aclCache그리고 ConsoleAuditLogger를 생성자에 등록해 줍니다. 또 하나 생성자에 넘겨주어야 할 것은 aclAuthorizationStrategy입니다, 이것은 ACL 리스트로부터 다루고자 할 admin 메소드를 콜하는 것을 허용할지 말지 결정합니다.  이 같은 경우 수정할 수 있는 권한을 가진 ROLE_ADMIN 으로 메소드 호출을 위한 허가를 받을 수 있습니다.

현재까지 설명한 모든 설정은 securityAuthorizationContext. Xml에 기록합니다. aclAuthorizationStrategy 는 3개의 필요한 파라미터를 가지고 있습니다 첫째 파라미터는 소유권을 변경에 필요한 권한이며, 두번째 파라미터는 감사 상세를 수정에 필요한 권한이며 마지막 파라미터는 다른 ACL와 ACE의 상세를 변경하는 것을 필요로 하는 권한입니다.(기본적으로 모두 ROLE_ADMIN으로 설정했습니다.) securityAuthorizationContext에 생성자의 인수로 받는 GrantedAuthority[] auths는 다음과 같이 각각의 GrantedAuthority필드로 구성되어 있습니다.

public class AclAuthorizationStrategyImpl implements AclAuthorizationStrategy {

    private GrantedAuthority gaGeneralChanges; // 소유권 변경에 필요한 권한
    private GrantedAuthority gaModifyAuditing; // 감사 상세를 수정에 필요한 권한
    private GrantedAuthority gaTakeOwnership; //다른 ACL과 ACE의 상세를 변경하는 권한
    private SidRetrievalStrategy sidRetrievalStrategy = new SidRetrievalStrategyImpl();

    public AclAuthorizationStrategyImpl(GrantedAuthority[] auths) {
        Assert.notEmpty(auths, "GrantedAuthority[] with three elements required");
        Assert.isTrue(auths.length == 3, "GrantedAuthority[] with three elements required");
        this.gaTakeOwnership = auths[0];
        this.gaModifyAuditing = auths[1];
        this.gaGeneralChanges = auths[2];
    }
    ..생략..
}



 

<bean id="lookupStrategy" class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
        <constructor-arg ref="dataSource"/>
        <constructor-arg ref="aclCache"/>
        <constructor-arg ref="aclAuthorizationStrategy"/>
        <constructor-arg>
            <bean class="org.springframework.security.acls.domain.ConsoleAuditLogger"/>
        </constructor-arg>
</bean>
<bean id="aclCache" class="org.springframework.security.acls.jdbc.EhCacheBasedAclCache">
        <constructor-arg>
            <bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
                <property name="cacheManager">
                    <bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>
                </property>
                <property name="cacheName" value="aclCache"/>
            </bean>
        </constructor-arg>
</bean>
<bean id="aclAuthorizationStrategy" class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.GrantedAuthorityImpl">
                    <constructor-arg value="ROLE_ADMIN"/>
                </bean>
                <bean class="org.springframework.security.GrantedAuthorityImpl">
                    <constructor-arg value="ROLE_ADMIN"/>
                </bean>
                <bean class="org.springframework.security.GrantedAuthorityImpl">
                    <constructor-arg value="ROLE_ADMIN"/>
                </bean>
            </list>
        </constructor-arg>
</bean>

이제 위에서 설정한것들을 사용하는 좋은 예제를 살펴보기로 합니다. 이것을 고려하기 위해 두가지 방법을 생각해 볼 수 있습니다. 하나는 서비스 메소드가 단일 오브젝트를 리턴하는 것과 서비스 메소드가 콜랙션 오브젝트를 리턴하는 경우입니다. 단일 오브젝트를 리턴하는 간단한 공통의 서비스 레이어 메소드부터 이해를 해 보기로 하죠.

Customer getCustomer(long id);

이 경우 주어진 고객을 읽기 위한 퍼미션을 가지고 있어야 있는 principal이 필요합니다. 이것을 체크하기 위해서 Customer instance는 AclEntryAfterInvocationProvider를 통과하고 조회힙니다. 만약 인증된 오브젝트가 그것을 읽는 권한을 가지고 있지 않으면 프로바이더는 AccessDeniesException을 던집니다.
이것은 제일 처음 설정한 objectManagerSecurity 빈에 afterInvocationManager로 등록되어 있습니다.

<bean id="objectManagerSecurity"
  class="org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor" autowire="byType">       
 <property name="accessDecisionManager" ref="businessAccessDecisionManager"/>
 <property name="afterInvocationManager" ref="afterInvocationManager"/>       
 <property name="objectDefinitionSource" ref="objectDefinitionSource"/>
</bean>

<bean id="afterInvocationManager"
  class="org.springframework.security.afterinvocation.AfterInvocationProviderManager">
<property name="providers">
    <list>
 <ref local="afterAclRead"/>
 <ref local="afterAclCollectionRead"/>
    </list>
</property>
</bean>




다음은 AFTER_ACL_READ액션의 설정입니다.(AFTER_ACL_READ의 상수값은 AclEntryAfterInvocationProvider의 생성자부분에서 발견되어 집니다.)

<bean id="afterAclRead" class="org.springframework.security.afterinvocation.AclEntryAfterInvocationProvider">
        <constructor-arg ref="aclService"/>
        <constructor-arg>
            <list>
                <ref local="administrationPermission"/>
                <ref local="readPermission"/>
            </list>
        </constructor-arg>
</bean>


다음으론 자바의 콜랙션 객체를 취득하는 방법을 보겠습니다. afterAclCollectionRead는 콜랙션을 조회하는데 필요합니다. 그것은 AFTER_ACL_COLLECTION_READ액션을 핸들링합니다. (AFTER_ACL_COLLECTION_READ액션은 AclEntryAfterInvocationCollectionFilteringProvider에 생성자에서 발견할 수 있습니다.)

public List getCustomers();

<bean id="afterAclCollectionRead" class="org.springframework.security.afterinvocation.AclEntryAfterInvocationCollectionFilteringProvider">
        <constructor-arg ref="aclService"/>
        <constructor-arg>
            <list>
                <ref local="administrationPermission"/>
                <ref local="readPermission"/>
            </list>
        </constructor-arg>
</bean>


Hierarchical roles and custom user details

SpringStarter어플리케이션(application)에 있어서, 우리들은 계층적인 Role을 사용합니다 ([4]).  역할 히이라키(hierarchy)는, 특정한 Role을 갖고 있는 사용자가 중첩된 인가처리를 하기 위해서 정해진 role보다 더 낮은 모든 행동을 수행하기 위한 인가 매커니즘을 갖고 있습니다. 이경우 더 높은 role은 더 낮은 role에 대해 덮어씌우기 때문에 낮은 Role에 대해서는 자원을 허용하는 규칙을 담고 있습니다.

계층적인 역할(role)을 사용하는 것에 더해서, 우리들은, 하이버네이트를 사용하여 데이터를 조회하고 콘트롤합니다. 그리고 각각의 Role은 그것에 부속되어 있었던 특정한 데이터 타입을 가지게 됩니다.

첫번째로, 스프링 시큐리티의로부터 UserDetailsWrapper를 확장함으로써, 사용자와 관련된 추가적인 정보를 포함하고 있는 CustomUserDetailsWrapper를 정의합니다.

public class CustomUserDetailsWrapper extends UserDetailsWrapper {

        private Object userInfo;

        public CustomUserDetailsWrapper(UserDetails userDetails, RoleHierarchy roleHierarchy) {
            super(userDetails, roleHierarchy);
        }

        public CustomUserDetailsWrapper(UserDetails userDetails, RoleHierarchy roleHierarchy, Object userInfo) {
            super(userDetails, roleHierarchy);
            this.userInfo = userInfo;
        }

        public Object getUserInfo() {
            return userInfo;
        }
}



우리는 방금 생성한 CustomUserDetailsWrapper를 컨트롤하기 위해 커스텀 사용자 서비스 래퍼가 필요합니다. (CustomUserDetailsServiceWrapper) 이것은 스프링시큐리티에 준비되어 있는 기본의 UserDetailsService를 구현합니다. 기본 UserDetailsService는 인증과 인가의 상세 작업을 자동적으로 처리하고 있습니다. 동시에 데이터베이스 조작부분은 하이버네이트를 사용했습니다.

public class CustomUserDetailsServiceWrapper extends HibernateDaoSupport implements UserDetailsService {

 private UserDetailsService userDetailsService = null;
 private RoleHierarchy roleHierarchy = null;
 private Class[] userInfoObjectTypes;

 public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
     this.roleHierarchy = roleHierarchy;
 }

 public void setUserDetailsService(UserDetailsService userDetailsService) {
    this.userDetailsService = userDetailsService;
 }

 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
     UserDetails userDetails = userDetailsService.loadUserByUsername(username);

     for(Class clazz: userInfoObjectTypes) {
  DetachedCriteria query = DetachedCriteria.forClass(clazz).add(Restrictions.eq("user.username", username));
  try {
      Object info = getHibernateTemplate().findByCriteria(query).get(0);
      return new CustomUserDetailsWrapper(userDetails, roleHierarchy, info);
  } catch (IndexOutOfBoundsException ex) {
      //do nothing

  }
     }
     return new CustomUserDetailsWrapper(userDetails, roleHierarchy);
 }

 public UserDetailsService getWrappedUserDetailsService() {
     return userDetailsService;
 }

 public void setUserInfoObjectTypes(Class[] userInfoObjectTypes) {
     this.userInfoObjectTypes = userInfoObjectTypes;
 }
}



사용자 상세 서비스는 security-config.xml파일에 authentication provider 에 연결됩니다.

<bean id="userDetailsServiceWrapper"
          class="com.denksoft.springstarter.util.security.CustomUserDetailsServiceWrapper"
          p:roleHierarchy-ref="roleHierarchy"
          p:userDetailsService-ref="usersDetailServiceJdbc"
          p:sessionFactory-ref="sessionFactory">
        <property name="userInfoObjectTypes" >
            <list>
                <value>com.denksoft.springstarter.entity.Clerk
                <value>com.denksoft.springstarter.entity.Customer
            </list>
        </property>
</bean>

<bean id="roleHierarchy"
          class="org.springframework.security.userdetails.hierarchicalroles.RoleHierarchyImpl">
        <property name="hierarchy">
            <value>
                ROLE_CLERK > ROLE_CUSTOMER
            </value>
        </property>
</bean>

<bean id="usersDetailServiceJdbc"
          class="org.springframework.security.userdetails.jdbc.JdbcDaoImpl"
          p:dataSource-ref="dataSource"
          p:authoritiesByUsernameQuery=" select users_username as username, authority from users_authorities inner join authorities on authorities_id=id where users_username = ? "/>

<bean id="hibernateProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean"
          p:location="WEB-INF/classes/hibernate.properties"/>

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
          p:hibernateProperties-ref="hibernateProperties"
          p:configLocation="classpath:hibernate.cfg.xml"/>

userDetailsServiceWrapper를 정의하기 위해 우리는 몇가지 옵셔으로 추가된 프로퍼티에 대해서 알아둘 필요가 있습니다.

roleHierarchy - 이 경우 ROLE_CLERK가 ROLE_CUSTOMER를 포함합니다.
userDetailsService – 기본 사용자 상세의 jdbc implementation. 우리의 테이블이 구조가 기본과 약간 다르므로, 우리들은 authoritiesByUsernameQuery를 재정의할 필요가 있습니다.
userInfoObjectTypes - 인증된 users에 결합되고 싶은 특별한 정보를 포함하는 entity들의 리스트
sessionFactory - 이 session properties는 PropertiesFactoryBean으로부터 로딩됩니다

hibernate.cfg.xml 파일은 session factory를 사용한 entity class를 포함합니다.

<?xml version='1.0' encoding='UTF-8'?>
    <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
     "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
 <session-factory>
     <mapping class="com.denksoft.springstarter.entity.Clerk" />
     <mapping class="com.denksoft.springstarter.entity.security.User" />
     <mapping class="com.denksoft.springstarter.entity.security.Authority" />
     <mapping class="com.denksoft.springstarter.entity.BankAccount" />
     <mapping class="com.denksoft.springstarter.entity.AbstractSecureObject" />
     <mapping class="com.denksoft.springstarter.entity.BankAccountOperation" />
     <mapping class="com.denksoft.springstarter.entity.Customer" />
 </session-factory>
</hibernate-configuration>



ACL management

우리는 컨트롤할 오브젝트를 위해 주어진 유저에 퍼미션을 쉽게 관리하기를 원합니다.ACL_Entry 테이블에 데이터를 추가할 필요가 있습니다. 일일히 손으로 관리하는 것을 피하기 위해서 aclSecurityUtil  bean을 프록시 인터페이스로 작성하고, 이 프로세스에 의헤서 퍼미션 관리를 자동화 할 수 있습니다.

aclSecurityUtil과 관련된 정의는 다음과 같습니다.

<bean id="aclSecurityUtil" class="org.springframework.aop.framework.ProxyFactoryBean">
        <qualifier value="aclSecurity"/>
        <property name="proxyInterfaces" value="com.denksoft.springstarter.util.security.AclSecurityUtil"/>
        <property name="interceptorNames">
            <list>
                <idref local="transactionInterceptor"/>
                <idref local="aclSecurityUtilTarget"/>
            </list>
        </property>
</bean>
<bean id="aclSecurityUtilTarget" class="com.denksoft.springstarter.util.security.AclSecurityUtilImpl" p:mutableAclService-ref="aclService"/>

<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"
          p:transactionManager-ref="jdbcTransactionManager">
        <property name="transactionAttributeSource">
            <value>
                com.denksoft.springstarter.util.security.AclSecurityUtil.deletePermission=PROPAGATION_REQUIRED
                com.denksoft.springstarter.util.security.AclSecurityUtil.addPermission=PROPAGATION_REQUIRED
            </value>
        </property>
</bean>
<bean id="jdbcTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/>



aclSecurityUtil bean은 com.denksoft.springstarter.util.security.AclSecurityUtil 인터페이스 구현한 AOP 프록시입니다. 이것은 두가지 인터셉터를 가지고 있습니다.:

transactionInterceptor :  transactionInterceptor는 spring security가 acl의 리스트를 조회하기 위해서 JDBC를 사용합니다.
aclSecurityUtilTarget : aclSecurityUtilTarget은 프록시 객체입니다.

AclSecurityUtil 인터페이스는 퍼미션을 추가,삭제할 수 있는 기본 메소드를 제공하고 있습니다. (addPermission,deletePermission)

public interface AclSecurityUtil {

        public void addPermission(AbstractSecureObject securedObject, Permission permission, Class clazz);

        public void addPermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz);

        public void deletePermission(AbstractSecureObject securedObject, Sid recipient, Permission permission);

    }



 

public class AclSecurityUtilImpl implements AclSecurityUtil {
        private static Log logger = LogFactory.getLog(AclSecurityUtil.class);
        private MutableAclService mutableAclService;

        public void setMutableAclService(MutableAclService mutableAclService) {
            this.mutableAclService = mutableAclService;
        }

         public void addPermission(AbstractSecureObject secureObject, Permission permission, Class clazz) {
            addPermission(secureObject, new PrincipalSid(getUsername()), permission, clazz);
        }

 
        public void addPermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz) {
            MutableAcl acl;
            ObjectIdentity oid = new ObjectIdentityImpl(clazz.getCanonicalName(), securedObject.getId());

            try {
                acl = (MutableAcl) mutableAclService.readAclById(oid);
            } catch (NotFoundException nfe) {
                acl = mutableAclService.createAcl(oid);
            }

            acl.insertAce(acl.getEntries().length, permission, recipient, true);
            mutableAclService.updateAcl(acl);

            if (logger.isDebugEnabled()) {
                logger.debug("Added permission " + permission + " for Sid " + recipient + " securedObject " + securedObject);
            }
        }

 

        public void deletePermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz) {
            ObjectIdentity oid = new ObjectIdentityImpl(clazz.getCanonicalName(), securedObject.getId());
            MutableAcl acl = (MutableAcl) mutableAclService.readAclById(oid);

            // Remove all permissions associated with this particular recipient (string equality used to keep things simple)
            AccessControlEntry[] entries = acl.getEntries();

            for (int i = 0; i < entries.length; i++) {
                if (entries[i].getSid().equals(recipient) && entries[i].getPermission().equals(permission)) {
                    acl.deleteAce(i);
                }
            }

            mutableAclService.updateAcl(acl);

            if (logger.isDebugEnabled()) {
                logger.debug("Deleted securedObject " + securedObject + " ACL permissions for recipient " + recipient);
            }
        }

 

        protected String getUsername() {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth.getPrincipal() instanceof UserDetails) {
                return ((UserDetails) auth.getPrincipal()).getUsername();
            } else {
                return auth.getPrincipal().toString();
            }
        }
    }



addPermission(AbstractSecureObject securedObject, Sid recipient, Permission permission, Class clazz) 함수는 securedObject의 ACL을 조회하거나 생성하고 현재 인증된 user에 퍼미션을 추가하는 메소드를 호출합니다. deletePermission(AbstractSecureObject securedObject, 시도 수령자, 허가 허가, 클래스clazz) 메소드는, 퍼미션을 제거합니다.  이곳에 인자로 넘겨준 sid는 principal,sid,GrantedAuthoritySid가 될 수 있습니다.

 우리들은 어떻게 이 API를 사용할 수 있는지 그 방법을 알아야 합니다. 우리들이 ACL조작을 수행 (코드에 걸쳐 산란하고, 카피또는 붙여넣기조작을 가지고 있는 것과 대비되게))하는 정확한 포인트를 원하기 때문에 퍼미션 메니지먼트를 관리하는 SecurityService인터페이스를 정의합니다. 이 구현체는 aclSecurityUtil을 autowired annotation을 써서 묶어서 활용합니다.

public class SecurityServiceImpl implements SecurityService {
   @Autowired
   @Qualifier("aclSecurity")
   private AclSecurityUtil aclSecurityUtil;
   public void setCustomerPermissions(Customer customer) {
      Sid sid = new PrincipalSid(customer.getUser().getUsername());
      Sid sidAdmin = new GrantedAuthoritySid("ROLE_CLERK");
      aclSecurityUtil.addPermission(customer, sid, BasePermission.ADMINISTRATION, Customer.class);
      aclSecurityUtil.addPermission(customer, sidAdmin, BasePermission.ADMINISTRATION, Customer.class);
   }
}


이 코드의 의미는, ROLE_CLERK와 함께 등록된 customer를 위해 administration 퍼미션을 추가합니다.
다음으로, 애플리케이션 레벨의 서비스 인터페이스인 CustomerService에 대해서 살펴봅니다. 그것을 보안하기 위해서 우리는 applicationContext.xml파일안에 objectManagerSecurity를 주입해야합니다. 또, 우리들은 주변의 AOP프록시를 구축하기 위해서  서비스를 구축하기 위해서 ProxyFactoryBean클래스를 사용합니다

<bean id="customerServiceTarget" class="com.denksoft.springstarter.service.impl.CustomerServiceImpl"/>
<bean id="customerService" class="org.springframework.aop.framework.ProxyFactoryBean">
 <qualifier value="customerService"/>
 <property name="proxyInterfaces" value="com.denksoft.springstarter.service.CustomerService"/>
 <property name="interceptorNames">
     <list>
  <idref bean="objectManagerSecurity"/>
  <idref local="customerServiceTarget"/>
     </list>
 </property>
</bean>

이 인터페이스는 자바 5의 어노테이션을 활용했습니다.

public interface CustomerService {
        @Secured({"ROLE_CUSTOMER","AFTER_ACL_READ"})
        public Customer getCustomer(long id);
        @Secured({"ROLE_CUSTOMER","ACL_OBJECT_ADMIN"})
        public void modifyBankAccount(BankAccount bankAccount);
        @Secured({"ROLE_CUSTOMER","AFTER_ACL_COLLECTION_READ"})
        public Collection getCustomerBankAccounts();
}

예를들어 getCustomer를 호출했을 때 그것을 호출한 principal은 만드시 ROLE_CUSTOMER 권한을 가지고 있어야 합니다. 일단 오브젝트가 조회되면, objectManagerSecurity가 해당 흐름을 가로채고, 인증된 유저가 리턴된 값을 갖기 전에 afterInvocationManager로 흐름을 가로챕니다.만약 그 때 사용자가 필요한 Role을 가지고 있으면, 오브젝트는 요청한 사용자에게 필요한 상태를 리턴할 것입니다. 이와 반대로 인증된 사용자가 아니라면 예외를 던지게 됩니다.

만약 사용자가, 은행예금계좌를 수정한다면 그가 BankAccount instance를 위한 ROLE_ADMIN 권한과 ROLE_CUSTOMER를 가지고 있지 않으면 안됩니다.

AFTER_ACL_READ, AFTER_ACL_COLLECTION_READ 와 ACL_OBJECT_READ, ACL_OBJECT_ADMIN은 각각 다른 의미를 가지고 있습니다.

1.     AFTER_ACL_READ, AFTER_ACL_COLLECTION_READ : 보안된 오브젝트를 되돌리는 함수를 호출하기 위해서 사용됩니다.
2.    ACL_OBJECT_READ, ACL_OBJECT_ADMIN : 보안된 object type의 파라미터를 가지고 있는 함수를 호출하기 위해사용됩니다.

작성자 결론

효과적인 사용을 위해 스프링 시큐리티를 셋업하는 것은 절대로 쉬운 수준의 작업이 아닙니다. 확실히, 상대적으로 간단한 어플리케이션(application)을 위해서, 이것들을 손으로 설정하는 것을 생각해보면 상당히 무거운 작업임이 틀림없습니다. Spring security를 사용함에 있어서 모든 버그가 확실히 알려진것도 없고 문서가 턱없이 부족한 상태지만, 그 속엔 매우 매력적인 옵션들이 많이 있습니다. 스프링본래의 매력과 같은 여러가지 옵션을 자유자재로 취할 수 있다는 것에 첫번째 이점을 둘 수 있고. 그 수많은 옵션중에서 적절히 자신의 애플리케이션에 필요한 것을 추가하고, 삭제해 나가기 때문에 매우 유연하고 차 후에 대비한 애자일 스러운 작업임이 틀림없습니다. 또한 차 후 복잡하게 변하게 될 보안 시스템의 미래에 대해서 여러가지 피치못할 치명적인 오류에 대비하기 위해 스프링 시큐리티를 채택하는 것은 맹우 바람직한 모습이라고 할 수 있습니다.

우리는 SpringStarter 애플리케이션이 앞으로 개선해 나갈 수 있도록 당신의 피드백을 받을 것이고, 이 애플리케이션을 더 쉽게 이해시키기 위해 지속적인 노력을 할 것입니다. 감사합니다.

저작자 표시 비영리 변경 금지
신고
블로그코리아에 블UP하기
  1. 2010.08.18 09:38 [수정/삭제] [답글]

    비밀댓글입니다

  2. Favicon of http://starplatina.tistory.com BlogIcon June

    2010.08.26 22:08 신고 [수정/삭제] [답글]

    Postgre SQL 사용시에..
    샘플 파일은 너무 버전이 오래되서... ;;
    시큐리티 문서에 있는 디비 스키마 대로 생성하고 문서 밑에 있는
    classIdentityQuery, sidIdentityQuery 만 JdbcMutableAclService 빈에다가 set 해주면
    잘 동작 합니다. ㅎ

    • Favicon of http://moova.tistory.com BlogIcon MOOVA 무바㏇

      2010.08.27 09:27 신고 [수정/삭제]

      원문 번역이라 버전이 낮을 수도 있었겠네요. 중요한것은 버전이 아니라 원리겠지요?
      제가 학생들한테 하고 싶은 말은, 먼저 원리를 깨우치는게 중요하지 않을까 합니다.
      (Domain ACL이 현업에서 어떻게 , 어디서, 무엇때문에 쓰여야 하는지부터 생각하는게 좋을 듯 하구요, 스터디뿐만이 아닌 활용가치에 대해서도 생각하면서 공부하는게 좋을 듯합니다. )

      근데. 뉘신지??

  3. Favicon of http://starplatina.tistory.com BlogIcon 김제준

    2010.08.27 09:20 신고 [수정/삭제] [답글]

    검색해서 타고 들어왔습니다. ^^

댓글을 남겨주세요.

Spring Security - OID, SID, ACL 2

Posted at 2010.07.23 09:39 // in Principle // by MOOVA 무바㏇

Spring Security의 Domain ACL[각주:1]에선 개별의 Entity객체에 CRUD권한을 걸어둘 수 있다. Role,URL,Resource등 여러가지 자원에 자물쇠를 걸어두는 것은 일반적인 권한 상식이나 실행중인 클래스에 권한을 걸어두는 것은 지극히 드문일일 것이다.
(Windchill의 ACL 편집기에선 Domain Class기준으로 ACL을 설정할 수 있다. JPA도 그렇고, 기타 시큐리티 오픈 소스도 그렇고 좋은 사상은 언제나 배껴가고 복사하고 따라가게 되어있다. 모방은 창조의 어머니라고 하지 않았나)


그렇다면 구지 왜? 실행중인 클래스에 자물쇠를 걸어놓고 사용자에 따라서 차등을 두어야 하는 것일까? Role과 URL권한, 메소드 권한까지 모자라서 클래스에 왜? 이와같은 번거로운 작업을 해야하는 것일까?

한가지 시나리오를 상상해 보자.

2010:07:23 08:41:59
Figure 3. 성인과 청소년에 대한 도메인 클래스의 인가처리


ROLE_ADMIN과 ROLE_USER는 성인과 청소년으로 구별되어 있다. index.task 자원과 Document.word자원 또한 모두 통과 가능할 것이다. 하지만, 도메인클래스영역을 보자. 성인은 Adult 클래스를 조작 가능 하지만, 청소년은 Adult 클래스를 조작하지 못하도록 해야한다.
만약에 청소년이 Adult클래스를 조작가능하다면 이 시스템에 접속하는 대부분의 유저들이 나이를 불문하고 성인물을 감상할 수 있을테니까 말이다. 결과적으로 Domain Class에 권한에 따른 설정을 해 두면 별도의 프로그래밍없이 다음과 같은 뷰를 볼 수 있을 것이다.


성인
---------------------------------------------------------------------------------------------------
제목                               분류                        등급                          대여자
---------------------------------------------------------------------------------------------------
구르물섯달버서난..           액션                        일반                         오승택
라스트사무라이                액션                   성인(19)                         오승택
---------------------------------------------------------------------------------------------------


청소년
---------------------------------------------------------------------------------------------------
제목                               분류                        등급                          대여자
---------------------------------------------------------------------------------------------------
구르물섯달버서난..           액션                        일반                         오승택
---------------------------------------------------------------------------------------------------


이처럼 Domain Class에 자물쇠를 걸어두면 세부적인 사항까지 구현할 수 있게 도와줄 뿐만 아니라, 객체분할과 같은 논리적인 영역까지 그 범위를 넓히게 한다. 또한 클래스당 권한을 강제할 수 있기때문에 앞으로 객체지향언어에서 추천되는 인가 방법이라고 할 수 있다.


다음장은 Spring Security의 Domain ACL에 대해서 알아보도록 하자. (언제가 될지 모르겠다.:)

  1. ACL[에이씨엘]은 개개의 사용자들이 디렉토리나 파일과 같은 특정 시스템 개체에 접근할 수 있는 권한을 컴퓨터의 운영체계에 알리기 위해 설정해 놓은 표라고 할 수 있다. 각 개체는 접근제어목록을 식별할 수 있는 보안 속성을 가지며, 그 목록은 접근권한을 가진 각 시스템 사용자들을 위한 엔트리를 가진다. 가장 일반적인 권한은 1개의 파일이나 또는 한 개의 디렉토리 안에 있는 모든 파일들을 읽을 수 있고(Read), 기록할 수 있으며(Write), 그리고 만약 그것이 실행가능한 파일이나 프로그램인 경우라면 실행시킬 수 있는(Execute) 권한 등을 포함한다. [본문으로]
저작자 표시 비영리 변경 금지
신고
블로그코리아에 블UP하기
  1. Favicon of http://minimonk.net BlogIcon 구차니

    2010.07.23 21:05 신고 [수정/삭제] [답글]

    음.. 리눅스에도 ACL이 있는데 그걸 웹으로 확장한건가요? 아니면 이름만 같은 별개의 유래를 가진 녀석일려나요?

댓글을 남겨주세요.

Spring Security - OID, SID, ACL

Posted at 2010.07.23 09:37 // in Principle // by MOOVA 무바㏇
2010:07:23 04:04:12
Figure 1은 OODBMS에 포함된 개념에 대해서 열거한 그림이다.


Spring Security의 Domain ACL의 개념은 Security Identity(SID)라고 하는 객체 보안 식별자에 의해서 인가작업을 하기 때문에, SID의 부모격인 OID(Object Identity)가 무엇인지 알아볼 필요가 있다.


OID를 사용하기 시작한것은 OODBMS에 객체를 식별하기 위한 방법으로 종종 사용하곤 했다. 최근엔 ORM Framework에서 종종 볼 수 있는 Object의 Uniq한 식별자라고 말하기도 하는데, Domain Driven Design의 Eric Evans는 Entities를 정의하면서 Object Identity를 다음과 같이 설명한다.


객체 지향 언어의 Object identity조작은, ENTITY의 identity를 정의함에 있어서 다소 미흡한 면이 많다.
데이터베이스로부터 얻은 Object의 id는 java와 같은 랭귀지의 identity와 는 다르다. 데이터베이스로부터 얻은 객체는 영속성이 있는 반면 프로그래밍언어의 identity는 비영속성이라는 점이 다르기 때문이다. 하지만 데이터베이스와 프로그래밍 랭귀지를 굳이 동기화하자면 특별한 key로 랭귀지 객체에 아이디를 부여하는 것이 일반적이다. 그 identity는 불변하며, 변화될 수 없다. ID는 시스템에 의해서 자동으로 작성할 수 있고 다른 Object와 구별할 수 있는 유일한 식별자가 바로 Object Identity이다.


보통 데이터베이스와 프로그래밍 언어의 동기화를 위해 식별자로 사용된 Object Identity는 Evans가 Entities를 설명하기 훨씬 전부터 정의되어 왔고 변화됐다는 것을 다음의 목록에서 확인하자. (1978년도 이전부터 Object Identity라는 용어가 사용되어왔으니, 필자가 태어나기 이전부터 그 가치가 논의되고 있었다는 소리다. 참.. 놀랍고도 위대하다.)


[각주:1]Khashafian, Copeland (1986) - identity는 Object 내부에 있다. 그것의 목적은 독립적인 Object의 특성을 표현하는데 있다.
Atkinson et al. (1990) - 두 Object가 동일한 제 삼의 Object를 참조할 수 있다. 이것은 공유 Object의 개념으로 Object를 업데이트하거나 조작하거나 카피할 때 Object identity로 그 식별을 대신한다.
Kent (1991) - 다양한 관점으로 Object identity에 대해서 논의했다. Object Identity를 비교하는 것이 아니라 그들을 참조하는 Object에 관점을 두었다. 즉 Object의 실제주소값으로 Object Identity를 설명했다.
Beeri (1993) - 그가 논의했던 OID는 현재 사용되고 있는 Object Identity의 원류가 되었다. identity의 경우에는 그것을 검색하는 쿼리에 따라서 식별된다



발전된 MDA를 채택한 벤더 솔루션중 하나인 Windchill이 사용하는 OID를 잠깐 살펴보자. Windchill내부의 ORM 프레임웍으로 새로운 Entity Object를 생성해 데이터베이스에 객체 저장을 했다. ( Windchill의 영속 객체는 JPA나 HIbernate의 생명주기와 비슷한 그것을 가지고 있다. ) 실제 데이터베이스에 접속하여 OID를 추출해 보면 다음과 같은 형태의 Object Identity를 발견할 수 있다.

-----------------------------------------------------------------------------------------------
Oid
-----------------------------------------------------------------------------------------------
com.tistory.moova.Moovie:001
com.tistory.moova.Moovie:002
com.tistory.moova.Moovie:003
com.tistory.moova.Moovie:004
-----------------------------------------------------------------------------------------------


Evans가 논의한 것처럼 데이터베이스의 식별자와 OOP 언어(자바와 같은)의 식별자는 개념상 다르기 때문에, 도메인에 적합한 OID생성규칙을 사용해서 동기화 하고 있다. 실제로 이것은 Object Identity가 아닌 Object identifier라고 하는 직렬화된 식별자이다.

Figure2. ObjectIdentity와 ObjectIdentifier의 관계도

ObjectIdentity도 Object의 일종으로써 실제 구분할 값을 찾기 위해 ObjectIdentifier라는 직렬화된 식별자를 참조의 형태로 포함하고 있다.(OCP원리를 따르면서 SRP원리도 포함하고 있다.). 실제로 "com.tistory.moova.Moovie:004"와 같은 값을 얻기 위해선 다음의 코드가 필요할지 모르겠다.

..
PersistentHelper.store(moovie);
moovie.getObjectIdentifier() : Serializable

JPA는 @Id, @IdClass, @EmbeddedId로 OID를 활용하고 있으며, Windchill은 Persistent를 상속하는 방법으로 OID를 활용하고 있다.

JPA 예
@Entity
public class Inventory implements Serializable {

        @Id[각주:2]
        @GeneratedValue(generator="InvSeq")
        @SequenceGenerator(name="InvSeq",sequenceName="INV_SEQ", allocationSize=5)
        private long id;

Wnidchill 예
public class Inventory extends Persistent {...}

언어와 플렛폼의 차이를 그대로 보여준다. 하지만, 근본적인 사상과 배경은 같다는 것을 알 수 있다.

이 설명으로 Object Identity와 Object Identifier에 대한 개념은 개략적으로 이해할 수 있으리라 본다. Secure Identity(SID)를 설명하기에 앞서 OID를 설명한 이유는 SID와 OID는 바늘과 실과 같은 수족과 같은 관계이기 때문이다.

보안작업을 하려하는 사람을 예로 들어보자.

예를들어 오승택이라는 사람이 주민등록증이 없으면 보안작업을 할 때 필요한 보안인가증도 발급받을 수 없다. 주민등록은 그 사람을 대표하는 ID이기 때문에, 말 그대로 객체 ID가 없으면 보안인가(SID)에서 필요한 신원을 확인할 수 없다.

충분히 이해가 되었으리라 믿는다. 지금까지의 설명은 Spring Security의 Domain ACL을 설명하기 위한 재료물들이다.




참조 문서

In “The Art of the Interpreter or, the Modularity Complex (Parts Zero, One, and Two)” [75] (1978),
• In “Values and Objects in Programming Languages” [56] (1982), Bruce J. MacLennan discusses the distinction between values and objects.
• The paper “Object Identity” [43] (1986) by Setrag N. Khoshafian and George P. Copeland is the most cited one with regard to the topic of object identity, and gives a thorough presentation of the concept and its implementation in programming languages and database systems.
• In “The Object-Oriented Database System Manifesto” [4] (1990), Mal-colm Atkinson et al. list requirements that are to be fulfilled by object-oriented databases. Of course, object identity plays an important role here.
• “A Rigorous Model of Object References, Identity and Existence” [42] (1991) by William Kent describes a model for object identity that, among other things, aims at a facility for merging objects and their identities after the fact.
• Catriel Beeri criticizes a too strong focus on object-oriented features in the context of databases in “Some Thoughts on the Future Evolution of Object-Oriented Database Concepts” [8] (1993), and sketches a re- duction of the concept of object identity to pure value-based properties in relational databases.
• Roel Wieringa and Wiebren de Jonge present a detailed formal model in “Object Identifiers, Keys, and Surrogates – Object Identifiers Re-visited” [85] (1995) that captures the essential properties of object identity and can be interpreted as the common denominator of previ-ous approaches.



 

  1. http://deposit.ddb.de/cgi-bin/dokserv?idn=972868291&dok_var=d1&dok_ext=pdf&filename=972868291.pdf [본문으로]
  2. http://www.oracle.com/technology/products/ias/toplink/jpa/resources/toplink-jpa-annotations.html [본문으로]
저작자 표시 비영리 변경 금지
신고
블로그코리아에 블UP하기

댓글을 남겨주세요.

Spring Security - URL 권한을 DB로 관리하기 2.

Posted at 2010.07.18 10:10 // in OpenSource // by MOOVA 무바㏇
필자는 그동안 경험했던 정보나 팁들, 또는 지식을 언제부터인가 온라인보다 오프라인으로 공유하는 것을 즐겨했습니다.
상당수가 프로젝트 보안이 걸려있는 문제이기도 하겠지요, 그 모든 부분을 온라인에 공유할 수 없는 심정, 이루 말할 수 없겠죠?
오픈소스와 관련된 사항도 프로젝트와 연관이 있으면 보안이라는 타이틀이 걸려있습니다. 하지만 공개할 것은 공개하자라는 근본 원칙은 항상 마음속에 간직하고 있습니다.
"버려야 새로운 것을 얻는다." 바로 이 정신. 참 아릅답습니다.~~


그림1. 커스터마이징 할 filterSecurityInterceptor의 인터페이스와 클래스 사용도입니다.


그림2. FilterSecurityInterceptor가 의존하는 두개의 매니저 클래스 ( Authentication Manager, AccessDecision Manager )

FilterSecurityInterceptor는 Spring Security의 중요 필터중 하나입니다. FilterSecurityInterceptor는 AuthenticationManager를 의존하며 User와 관련된 (인증 Authentication) 프로세스를 수행하고, 동시에 수행될 자원에 인가여부를 따지는 AccessDecisionManager를 의존하여 투표를 실시합니다.

<beans:bean id="filterSecurityInterceptor"
class="org.springframework.security.intercept.web.FilterSecurityInterceptor"
autowire="byType">
<custom-filter before="FILTER_SECURITY_INTERCEPTOR" />
<beans:property name="authenticationManager" ref="authenticationManager"></beans:property>
<beans:property name="accessDecisionManager" ref="accessDecisionManager" />
<beans:property name="objectDefinitionSource" >
                    <security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/>
                    <security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
             </beans:property>
</beans:bean>

FilterSecurityInterceptor의 objectDefinitionSource필드를 커스터마이징 해야합니다. 바로 이 부분이 앞에서 모델링한 테이블과 연동을 해야 할 부분입니다. 위의 objectDefinitionSource를 다음과 같이 바꾸어 줍니다.

<beans:property name="objectDefinitionSource"  ref="coolInvocationDefinitionSource"/>

objectDefinitionsource에 reference될 bean을 등록해야합니다.

<beans:bean id="coolInvocationDefinitionSource"
class="com.moova.secure.filterInvocation.CoolObjectDefinitionSourceFactoryBean">
<beans:property name="dataSource" ref="springSecurityDataSource" />
<beans:property name="resourceQuery"
value="
                                 SELECT URL.URL_SPRING, R.NAME
FROM ROLE ROLE JOIN URL_ROLE UR ON ROLE.ID = UR.ROLE_ID JOIN PROGRAM ON PROGRAM.ID = UR.PROGRAM_ID JOIN URL_REPOSITORY REPO ON PROGRAM.ID = REPO.ID
        " />
</beans:bean>

FilterSecurityInterceptor의 objectDefinitionSource의 API를 살펴보면 다음과 같이 setter injection으로 되어 있는 것을 볼 수 있습니다.

Spring Security 2.0

Spring Security 3.0
 SecurityMetadataSource obtainSecurityMetadataSource() 
           
 void setObjectDefinitionSource(FilterInvocationSecurityMetadataSource newSource) 
          Deprecated. use setSecurityMetadataSource instead
 void setObserveOncePerRequest(boolean observeOncePerRequest) 
           
 void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) 

Spring Security 2.0 과 3.0 API를 살펴보면 크게 변화된 부분이 바로 이 objectDefinitionSource입니다. 3.0에선 objectDefinitionSource가 deplecated되었습니다. 이것이 securitymetadataSource로 변경이 되어 있네요. 2.0의 objectDefinitionSource의 Type은 FilterInvocationDefinitionSource 이고 3.0의 objectDefinitionSource의 Type은 FilterInvocationSecurityMetadataSourcen 인터페이스입니다.

이를 확인하고 각 버전에 따라 작성해야하는 클래스가 다를 수 있다는 것에 주목해 주십시오. 여기선 그대로 objectDefinitionSource를 사용합니다.

새로 제작할 CoolObjectDefinitionSourceFactoryBean는 FactoryBean을 implements합니다. FactoryBean을 implements 하면 어떤 객체라도 Spring lifecycle안에서 DI할 수 있게 해줍니다. 이는 단순 객체가 아닌 Factory 클래스로써 getObject()와 getObjectType()를 구현해 주기만 하면 새롭게 사용할 클래스를 반환시켜줍니다. 추가로 데이터베이스와 연계가 필요하니  "org.springframework.jdbc.core.support.JdbcDaoSupport"를 상속합니다.

사용할 구조

public class CoolObjectDefinitionSourceFactoryBean extends JdbcDaoSupport implements FactoryBean {
private String resourceQuery;

public boolean isSingleton() {
return true;
}

public Class getObjectType() {
return FilterInvocationDefinitionSource.class;
}

public Object getObject() {
return new DefaultFilterInvocationDefinitionSource(
this.getUrlMatcher(), this.buildRequestMap());
}
}

데이터베이스 연계부분은 MappingSqlQuery을 사용합니다. MappingSqlQuery 사용법은 여기를 참조해 주세요.

private class UrlRepository {
private String url;
private String role;

public UrlRepository(String url, String role) {
this.url = url;
this.role = role;
}

public String getUrl() {
return url;
}

public String getRole() {
return role;
}
}

private class UrlRepositoryMapping extends MappingSqlQuery {
protected UrlRepositoryMapping(DataSource dataSource, String resourceQuery) {
super(dataSource, resourceQuery);
compile();
}

protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
String url = rs.getString(1);
String role = rs.getString(2);
UrlRepository resource = new UrlRepository(url, role);

return resource;
}
}

"com.moova.secure.filterInvocation.CoolFilterInvocationDefinitionSourceFactoryBean"의 원본 파일

더보기




executeResourceMap()메소드에서 URL과 ROLE 관련된 매핑부분을 key와 Value로 조합해야 합니다. 
DefaultFilterInvocationDefinitionSource의 두 번째 인자는 LinkedHashMap입니다. 
당연히 HampMap값을 넘겨주어야 하기 때문에 buildRequestMap에서는 LinkedHashMap를 new해서 반환해 주고 있습니다. 
DefaultFilterInvocationDefinitionSource의 첫 번째 인자는 AntUrlPathMatcher를 new해서 주입하고 있습니다.

스키마를 새로 개정하거나 생성해서 사용해도 무방합니다.
하지만 URL과 Role에 대한 모델링 연관은 그다지 바뀔게 없다고 봅니다.


저작자 표시 비영리 변경 금지
신고
블로그코리아에 블UP하기

댓글을 남겨주세요.

Spring Security - URL 권한을 DB로 관리하기 1.

Posted at 2010.07.18 10:08 // in OpenSource // by MOOVA 무바㏇
필자는 오픈소스나 자/타 제품을 접할 때 먼저 해당 제품의 생명주기(LifeCycle)을 먼저 파악합니다. 서로 연결된 인터페이스나 외부에 공개된 컴포넌트의 연관관계를 파악하고, 자주 사용하는 기능이나 중요하다고 생각되는 기능이 생명주기에 의존되어 있는지를 파악합니다. 전체 생명주기나 구조적 관점을 늘 중요시 하다보면, 치명적인 이슈를 제외한 세부적인 기술이슈까지도  그때 그때 처리할 수 있으리라 봅니다.
<http auto-config='true'>
    <intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" /> 
    <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
    <intercept-url pattern="/**" access="ROLE_USER" />
    <form-login/>
    <logout logout-success-url="/pages/logout-redirect.jsp"
invalidate-session="true" />
    <remember-me key="moovaSecureRMKey" user-service-ref="userDetailsService" />
</http>


Spring Security에서 url path에 대한 권한 부여는 보통 위와 같이 설정을 해야합니다. (굵게 표기한 부분)
( 권한을 가지고 있는 사용자가 요청한 url 패턴을 기준으로, 해당 자원을 접속할 수 있는지를 따지게 하여 주는 설정입니다. ) 하지만 안타깝게도 보통 실무에서는, url path나 문서 조회권한에 관련된 설정 파일은 따로 관리하지 않습니다. 

User,Member,Group등 중요 인증 및 인가에 관련된 테이블을 데이터베이스로 관리한다면 URL 권한도 데이터베이스로 관리되어야 차 후 유지보수나 확장에도 이점을 얻을 수 있습니다. Spring Security에선 위의 intercept-url설정이 기본기능으로 구성되어 있습니다만, 이것을 데이터베이스로의 관리방법으로 변경해 보도록 할까 합니다.

하고자 하는것은 매우 단순합니다. Spring Security의 중요 Filter중 하나인 FilterSecurityInterceptor의 objectDefinitionSource를 커스터마이징 할 objectDefinitionSource로 바꿔치기하는 것입니다.



1. 테이블 구축


그림1. 실제 실무에서 사용된 인증/인가와 관련된 테이블 ( 축소공개형 ) 



실제 Magpie 오픈소스로 구축중인 테이블 스키마의 축소 일부입니다. 
User와 Role N:N관계, Role과 Program도 N:N관계, Program과 Url_Repository는 1:1관계로 되어 있습니다.
Role과 Url_Repository를 N:N관계로 모델링 해도 무방하지만 보통 URL은 동적인 속성이 강한 부분이니 카테고리로 분류할 수 있는 Program이란 Entity를 따로 생성하여 url port같은 기능을 하도록 설정해 두었습니다.

사용해둘 데이터를 미리 뽑아 보고, 기존 설정에서 사용했던 포멧으로 데이터가 축출 가능한지 확인합니다.


그림 2. 기존 Spring Definition 설정 위치와 비교한 데이터의 위치.

2. 데이터 확인 및 데이터 추출
SELECT URL.URL_SPRING, R.NAME FROM ROLE ROLE JOIN URL_ROLE UR ON ROLE.ID = UR.ROLE_ID JOIN PROGRAM ON PROGRAM.ID = UR.PROGRAM_ID JOIN URL_REPOSITORY REPO ON PROGRAM.ID = REPO.ID RESULT : ROLE_USER | /** ROLE_ANONYMOUSLY | /** ROLE_ADMIN | /admin.jsp ROLE_USER | /login.jsp ROLE_ANONYMOUSLY | /login.jsp ROLE_MANAGER | /manager/index.jsp
"Spring Security URL 권한을 DB로 관리하기"에서 사전 준비해야 할 재료는 모두 준비된 상태입니다. 이제 Spring Security의 일부분을 커스터마이징해야 합니다. ( "org.springframework.security.intercept.web.FilterSecurityInterceptor" )

참고 문서 :  
http://static.springsource.org/spring-security/site/docs/3.0.x/reference/springsecurity.pdf

저작자 표시 비영리 변경 금지
신고

'OpenSource' 카테고리의 다른 글

근무환경  (2) 2010.08.10
Spring Security - URL 권한을 DB로 관리하기 2.  (0) 2010.07.18
Spring Security - URL 권한을 DB로 관리하기 1.  (0) 2010.07.18
심심풀이 납품용  (2) 2010.06.05
[Guide] Vaadin Korean Guide (ver01)  (3) 2010.05.11
[Share Document Note] Evernote  (0) 2010.01.24
블로그코리아에 블UP하기

댓글을 남겨주세요.

티스토리 툴바