Clean Code that Works.

집에가서 소스 코드 업로드 및 수정 보완 할 것.

기본적인 구성은 http://starplatina.tistory.com/entry/Spring-Security-ACL-Example

여기 링크의 url 을 참고로 소스를 재구성 하였삼.


스프링 도메인 ACLS
- Security 부분이 다른 비즈니스 로직과 독립적으로 운용된다.(도메인 클래스에 따로 ACL 및 보안 관련된 값들이 없고, 별도 테이블에서 관리)
- Affirmative(긍정적인), Consensus(합의에 의한), Unanimous(만장일치의) 권한 체크를 한다. Voter라는 클래스가 투표를 하게 되는데 여기서 긍적적인 표가 한개라도 있으면, 긍정 중립 부정 표를 비교 해서 긍정 표가 있으면, 모두 긍정으로 투표하였을 경우
- accessControllist라는 jsp tag로 권한에 따라서 뷰를 조작 할 수 있다.
- 권한 관리를 위한 테이블이 추가 된다(acl 관련 테이블 4개, 기타 시큐리티 권한 관련 클래스 2~3개)
- 도메인 클래스에 대한 acl 작성시 aop를 사용해서 권한을 부여하는 작업을 해야 한다.


도메인 ACL의 경우 물론 스프링 시큐리티에 대한 이해가 필요 합니다.
(url 보안, 메서드 보안(롤베이스, acl 베이스))

그럼 간단하게 acl을 적용하는 케이스를 보면서 이에 대해서 코드를 구성해나가는 방법으로 알아 보도록 하겠습니다.
일단 어떤 도메인 오브젝트가 있다고 보고 이에 대한 읽기 권한에 대해서 제어를 할 경우.
보통 서비스 클래스의 메서드에 작업을 하게 됩니다.

@Secured({"ROLE_USER", "ACL_OBJECT_READ"})
 public Object getObj( String id) {
  return (AclBbs)aclBbsDAO.getBbs( id);
 }


@Security("USER_ROLE")은 스프링 시큐리티에서 지원하는 롤 베이스 메서드 보안입니다.
getObj()라는 메서드를 호출 할 때, 이 메서드를 호출 하는 사용자가 "USER_ROLE"이라는 권한이 있는지 확인하여 권한이 있을 경우에만 메서드를 실행 할 수 있도록 합니다.

@Security({"USER_ROLE", "ACL_OBJECT_READ"}) 굵게 표시한 "ACL_OBJECT_READ" 이 부분이 바로 ACL 권한을 체크하는 부분입니다.
이 경우에는 조회 할려는 도메인 오브젝트에 대한 사용자의 권한(읽기)이  존재 할 경우에만 메서드를 실행 합니다.

그럼 위 메서드 보안이 동작 하도록 하기 위해서 작업해야할 흐름들을 알아 보도록 하겠습니다.

1. 도메인 오브젝트를 작성 할 때(insert 할때), ACL 권한과 관련된 작업들을 함께 처리해 주어야 한다.
 -> 오브젝트에 대한 권한 부여를 해주어야 한다.
2. 시큐리티 설정을 통해서 오브젝트를 호출 할 때, 메서드 보안을 통해 ACL 권한 체크를 하도록 처리 한다.
 -> 권한체크 방법(긍정적인, 합의에 의한, 만장일치)에 따라 권한을 체크 해 주어야한다.


기본적으로 위 두가지 작업을 해 주게 되면 보안 작업이 완료 됩니다.
ACL 권한 부여의 경우 서비스 클래스에서 insert 작업이 일어날 경우 작업을 진행하면 됩니다.

insert 메서드 내부에 권한부여 작업을 호출할 수도 있지만, 이렇게 하면 비즈니스로직과 관련없는 로직이 들어가므로 AOP를 적용하여 권한부여 작업을 진행 하도록 합니다.

시큐리티 UML


위 UML에서 AclSecurityUtil은 Acl 오브젝트에 대한 권한 부여를 하는 메서드(addPermission, deletePermission)를 가진 인터페이스 입니다. SecurityService는 AOP를 사용해서 실제 권한 부여 작업을 하는 인터페이스 입니다. SecurityService에서 setPermissions를 통해 AclSecurityUtil의 addPermission을 호출하여 권한을 부여 합니다.


위와 같은 방법으로 권한 부여에 관한 로직을 작성 한 후, XML 설정을 통해서 이를 동작하도록 해 주어야 합니다. 

XML 설정

XML 설정은 권한 체크 방법을 정의 하고, 체크할 권한에 대한 정의, ACL 권한 부여에 필요한 bean 정의 를 하게 됩니다.
일단 가장 먼저 해야할 일이 메서드 시큐리티에 대한 access-decision-manager를 설정 해야 합니다.

<sec:global-method-security
  secured-annotations="enabled" access-decision-manager-ref="businessAccessDecisionManager" >
 </sec:global-method-security>

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

위의 설정을 보면 access-decision-manager로 businessAccessDecisionManager를 참조 하도록 하고, businessAccessDecisionManager에서는 UnanimouseBased(만장일치) 방식으로 권한 체크를 하도록 하고 있습니다. 만약 다른 방법으로 사용하고 싶다면 클래스를 AffirmativeBased나 ConsensusBased로 정의 하면 각각의 방식으로 권한 체크를 하게 됩니다.

그다음에 정의해 주어야 할 것이 decisionVoters 입니다. 권한 체크를 할 떄 저 voter들을 순차적으로 호출 하면서, 권한이 있는지 실질적으로 체크 하게 됩니다. UnanimouseBased의 decide메서드를 살표보면 아래와 같은 로직을 실행합니다.

AffirmativeBased의 경우 한개라도 허가가 발생 하면 바로 return 하여 권한을 부여 하고, ConsensusBased는 찬성, 반대, 중립 표들을 비교 하여 찬성일 경우만, UnanimouseBased는 모두 찬성일 경우만 권한을 부여 하게 됩니다.


        for (ConfigAttribute attribute : attributes) {
            singleAttributeList.set(0, attribute);
            for(AccessDecisionVoter voter : getDecisionVoters()) {
                int result = voter.vote(authentication, object, singleAttributeList);
                if (logger.isDebugEnabled()) {
                    logger.debug("Voter: " + voter + ", returned: " + result);
                }

이 경우에는 "ROLE_BASE", "ACL_OBJECT_READ" 가 attributes 가 되겠고, getDecisionVoters()에서 가져온 voter들은 XML 설정 파일에서 정의한 roleVoter, aclObjectReadVoter, aclObjectWriterVoter가 됩니다.

각각의 방식으로 vote를 호출해서 로그인한 사용자가 권한이 있는지 없는지를 확인하게 됩니다.

스프링 시큐리티 ACL 권한의 종류.

BasePermission 클래스 확인하기
기본 권한으로는 READ, WRITE, CREATE, DELETE, ADMINISTRATION  권한을 가지고 있습니다.
만약 다른 권한을 만들고 싶다면, AbstractPermission를 확장하여 구현 하면 됩니다.

추가로 그럼 ACL DA 로직은 어디에 있는가 하는가 하면. 
JdbcMutableAclService 클래스를 사용 하여 Acl 관련 서비스를 제공 합니다. 
기본 쿼리가 PostregeSql 기본으로 되어 있기 때문에 각각 DB 벤더에 맞게 쿼리를 커스터마이징 해야합니다.
(call identity 등 ANSI 표준 쿼리가 아닌 것만 작성하면 됨)

프로그램 실행 화면.

소스 구성은 간단한 테스트 클래스와, ACL 을 활용한 게시판으로 구성 되어 있습니다.
소스에 첨부된 스키마대로 db를 설치 하고(mysql 기본) STS 최신버전(이클립스 스프링 버전, Spring tool suit 인가 ;)에서 첨부한 파일을 가져다가 실행 시키면 됩니다. 서버는 톰캣 6.0 으로 실행 시키시면 됩니다.

일단 서버를 실행 시키면


이런 화면이 표시 됩니다.
제가 전에 공부하던 소스를 가지고 구성한 것이라... 몇몇 소스가 동작하지 않을 수도 있습니다. ^^;
jquery plug-in을 몇개 포함하고 있습니다.
액션은 전부 jquery를 사용해서 ajax로 처리하도록 되어 있습니다.

글쓰기를 선택하면 글쓰기 화면이 표시되고, 제목을 선택하면 보기 화면으로 넘어갑니다.

jsp에서 권한을 체크하는 부분이 있는데, 이 부분을 보면 아래와 같습니다.


hasPermission 부분에서 접속한 사용자의 권한과 조회한 오브젝트의 ACL 권한을 비교해서 권한이 있을 경우에만 태그 안의 내용을 보여주게 됩니다.(삭제 및 수정 버튼 렌더링 처리)


아래 파일은 프로젝트 압축학 war 파일.

STS에서 불러서 쓰삼. 





<sec:accesscontrollist hasPermission="8" domainObject="${aclBbs}">
</sec:accesscontrollist>

이렇게 하면 로그인한 사람의 권한이 acl Object의 권한과 일치 하는지 검색 하게 된다.

여기서 문제점이.. admin의 경우 permission 이 '16'이다.
admin은 하위 권한들을 가지고 있기 때문에 위 태그의 '8(delete, BasePermission)' 권한을 포함 해서 확인을 해야 하는데.
'16'이 모든 권한을 가지고 있으므로 '8' 일 경우에도 권한을 가지고 있어야 한다.

근데 aclObject는 admin이 이 객체에 대해서 16의 권한을 가지고 있어도
AclImpl 클래스의 isGranted에서 이를 == 비교로 하기 때문에 '16' 과 '8'은 다르기 때문에 권한이 있다고 표시 되지 않는다.

그럼 이를 변경 하기 위해서는 AclImp을 extends 해서 isGranted를 override 해야 하는가??

다른 방법으로 Permission을 비교하는 방법은 없을까?

음 좀 더 생각을 해 보자.


적용하는 SecurityService 클래스를 어디에 등록 해 주어야 하는가.

dao를 호출하는 서비스 클래스의 메서드에다가 aop를 걸어서(acl에 의존하지 않도록) 해 주면 되나??

각각 도메인 클래스마다 해당 securySevice클래스를 등록해 주어야 하는가?
작성자에 따라 글 에 대한 권한도 틀려질 텐데 이를 어떻게 해결할 것인가?
-> 롤 확인해서 롤에 따라 acls을 달리 부여 하면 될려나?

이거 원 산넘어 산이군 ㅋㅋ


acl 값을 가져 온 후에 이 값들을 맵에 담게 된다.

근데 이 맵의 키가 ObjectIdentityImpl 인데, 요고에 담기는 키의 값들과, acl 값을 가져올 ObjectIdentityImpl 값이 같아야
acl 값들을 제대로 가져 올 수 있다.

근대, Object id 값을 비교 하는데 대상 Object의 id는 hashcode 값이 있는데, 맵에 담겨질 object의 id에는 hashcode 값이 없다.

하여 map.containsKey( target)으로 하여 비교 하면 hashcode로 비교를 하는데, 여기서 에러가 나서

voter가 AccessDenied로 투표를 한다.

꿱..
어려워 -_-

그래서 이거 넘어 갈려면.... hashcode 값도 담겨가게 하던가,
String 값 비교로 해야 할 것 같은데..

근데 전에 잘 된거 같은데. -_-;;


이 무슨 또 삽질을 -,.-;;

ObjectIdentityRetrievalStrategy를 구현한 클래스에서 getObjectIdentity 메서드가 ObjectIdentityImpl를 리턴 하는데..
여기서 스트링으로 identity를 주니 당연히 안되지 -_-;; long 값으로 주도록 변경.

이제 다음 단계에 접어 들어간다.

일주일 휴가 했더니 다 까먹었어.

시큐리티 ACL이 동작을 하면서 acl 관련 정보들을 insert한 후 이 데이터들에 대한 id 값들을 가져와야 하는데.
기본적으로 identity 하는 방식이 아래와 같이 사용하고 있다.

private String classIdentityQuery = "call identity()";
private String sidIdentityQuery = "call identity()";

하지만 내가 사용하고 있는 MYSQL에는 저런게 없기 때문에.. -ㅁ-.. 어디서 사용하는 거지.. postrege sql??
아무튼 저것들을 변경해 주어야 한다.

구글링 해서 http://forum.springsource.org/showthread.php?t=67461 이 스레드에서 해결책을 발견.

뭐 그냥 쿼리만 mysql로 변경해 주면 된다

그래서 바꾸면 아래와 같이 MYSQL에서 사용하는 방식으로 쿼리를 변경 해 주면 된다. ㅋ
http://blog.sqlauthority.com/2007/03/25/sql-server-identity-vs-scope_identity-vs-ident_current-retrieve-last-inserted-identity-of-record/
여기도 참고 하면 괜찮은 URL(스레드에 있는 url이다)
<bean id="aclService" class="org.springframework.security.acls.jdbc.JdbcMutableAclService">
        <constructor-arg ref="dataSource"/>
        <constructor-arg ref="lookupStrategy"/>
        <constructor-arg ref="aclCache"/>
        <property name="classIdentityQuery" value="SELECT @@IDENTITY"/>
       <property name="sidIdentityQuery" value="SELECT @@IDENTITY"/>
</bean>

이제 거의 다 진행 되간다 ~_~

JdbcMutableAclService 클래스에 acl과 관련된 쿼리들이 다 담겨 있고, 이것들은 위와 같은 방식으로 각각 DB환경에 맞는 쿼리로 수정하여 사용하면 된다.

2010.08.27일에 추가
PostgreSQL에서는 다른 방법으로 identity를 확인한다.

You will have to set the classIdentityQuery and sidIdentityQuery properties of JdbcMutableAclService to the following values, respectively:

  • select currval(pg_get_serial_sequence('acl_class', 'id'))

  • select currval(pg_get_serial_sequence('acl_sid', 'id'))




private PermissionFactory permissionFactory = new DefaultPermissionFactory();이전 포스트에서 보았던 블로그 샘플을 보면, 커스텀 ACL을 등록해서 사용 하는 것을  볼 수 있다.

public class ExtendedPermission extends BasePermission {

    private static final long serialVersionUID = 1L;
    public static final Permission ACCEPT = new ExtendedPermission( 1 << 5, 'a'); // 32
    public static final Permission WRITE_CREATE = new ExtendedPermission( 1 << 6, 'E'); // 64
   
    protected ExtendedPermission( int mask, char code) {
        super( mask, code);
    }
}

이렇게 원하는 권한을 만든 후에, 이것을 스프링에게 알려줘야 하는데..
2.0 버전에는 아래의 BasePermission.registerPermissionFor() 메서드를 호출해서 새로 등록한 퍼미션 클래스를 등록하면 됬었다.

/**
* Registers the public static permissions defined on this class. This is
* mandatory so that the static methods will operate correctly. (copied from
* super class)
*/
static {
registerPermissionsFor(ExtendedPermission.class);
}

하지만 3.0 버전으로 넘어 오면서 패키지 구조가 많이 바뀌고, 이로 인해 메서드들도 바뀌게 되었다.(햇갈려!!!)

하여, 위의 registerPermissionFor 는 사라지고(다른 클래스로 옴겨가고)
이를 해결 하기 위하여,
PermissionFactory를 구현한 Concrete Class 인 DefaultPermissionFactory의 registerPublicPermissions() 메서드가 있다.

바로 registerPublicPermissions() 메서드가 내부적으로 registerPermission을 호출 해서 스프링에 등록이 되는 것이다.(이 표현이 맞나.. 아무튼 레지스트리에)
하여 이를 사용해서 등록을 해야 하는데, DefaultPermissionFactory의 생성자 중 하나인
/**
     * Registers the <tt>Permission</tt> fields from the supplied class.
     */
    public DefaultPermissionFactory(Class<? extends Permission> permissionClass) {
        registerPublicPermissions(permissionClass);
    }

요것을 통해서 Permission 클래스를 상속 받은(ExtendedPermission) 클래스를 파라미터로 넘겨 주면 이를 등록하게 된다.

이렇게 해서 ExtenedPermission을 등록 할 수 있는데, 그럼 과연 DefaultPermissionFactory는 어디서 등록을 해야 하는가.
이거 찾느라 시간을 너무 허비..덕분에 acl에 대해 좀 더 알 수 있었다.

바로 BasicLookupStrategy에 permissionFactory에다가 DefaultPermissionFactory를 등록 하면 된다.
위 클래스의 소스를 보면 알겠지만 기본적으로
private PermissionFactory permissionFactory = new DefaultPermissionFactory();
이렇게 해서 사용 하고 있다. 이걸 new DefaultPermissionFactory(ExtendedPermissionFactory.class); 로 바꿔주어야 하는데,
XML 설정 파일을 통해서 변경 할 수 있다.

시큐리티 설정을 위한 XML 설정 역시 머리가 아팠는데... 영어 문서의 압박과 구버전 샘플의 압박.
요리 저리 디버깅 해가면서 조금은 더 알게 되었다.

대충 구조를 살펴 보면 아래와 같은 구조를 가지게 된다.

XML 설정 구조


그럼 lookupStrategy를 어디에다가 등록을 하는가 하면
aclService의 구현클래스의 변수인 lookupStragegy에다가 등록을 해 주면 된다.

 <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>
        <property name="permissionFactory">
            <bean class="org.springframework.security.acls.domain.DefaultPermissionFactory">
                <constructor-arg value="net.study.spring.test.security.ExtendedPermission"/>
            </bean>
        </property>
    </bean>

이렇게 등록을 하면 스프링이 올라갈때, 커스텀된 권한까지 가지고 올라가기 때문에 acl_entry 테이블의 mask에 등록된 커스텀 acl 값이 잘 올라가고, Mask '32' does not have a corresponding static Permission(커스텀 acl 등록 안될경우 발생하는 오류) 이런 오류는 표시되지 않게 된다.

Spring Security 샘플

Spring2010. 4. 26. 14:05
스프링 시큐리티 샘플 파일.

스프링 도메인 시큐리티(ACLs) 관련해서 자료가 많이 빈약한데..
문서에서도 설명이 -_-;; 안습하다.

그중에서 그나마 좀 이해 하기 쉬운 ACLs 관련 소스 코드이다.

http://grzegorzborkowski.blogspot.com/2008/10/spring-security-acl-very-basic-tutorial.html
위 블로그 보면 설명이 나와 있는데, 보시다 싶이 소스가 2008년 10월 버전이라 클래스 위치가 몇몇 바뀐곳도 있고,
소스도 좀 바뀌어서 이를 수정하였다.

꽥!!
그래도 어려워. @_@

메모리 기반의(맵에 저장하고 막 요런 정도) ACLs 설정을 확인해 볼 수 있다.

이제 이것을 DB와 연결되도록 변경. 고고싱.



스프링에서 테스트를 진행할 시 트랜잭션은 오토 롤백 되게 구성되어 있다.

이걸 몰라서 -,.-;; 왜 롤백되지 하고 잠시 삽질을..

spring one 동영상 파일중에서 캡춰


테스트에 대한 문서를 보면 테스트 문서 보기 확인 할 수 있다.

@TransactionConfiguration(defaultRollback=true)
기본적으로 디폴트 롤백이 true로 설정되어 있어서 이다.
이것을 false 로 바꾸던지, 테스트를 실행하는 메서드에
@Rollback(false)이렇게 주면 된다.

스프링에 있는 메시지들(*.properties)로 자바스크립트 메시지와 일치 시키고 싶다.

어떻게 해야할까..

간단하게 생각해보면..
index 파일 열때 스프링쪽에다가 getJson해서 스프링에서 메세지들을 다 읽어서, 이것을 json으로 만들어서 리턴해주는 방법??

그럼 스프링에서 메세지들을 다 읽어 올 수 있는가??
기본적으로 key, value로 key로 읽어 오는데...

메세지소스액세서에 키들 다 가지고 오는 방법이 있을려나???

자바에서 지원하는 ResourceBundle이 getKeys()가 있으니깐 요놈으로 해서 사용하면 될 것 같다.

음..

아마 스프링 버전을 3.0.0 에서 3.0.1 로 올리면서 나는 오류였던것 같기도 하고...

아무튼 테스트 케이스를 돌리면 무슨 클래스 없다고 하면서 테스트가 안되는 경우가 있다.

뭔지는 까먹... -,.-;;

아무튼.. junit 버전이 4.7이어서 그랬다.
pom.xml에 junit 버전을 4.8.1로 올려줘서 해결..

헉. 완전 불친절한 포스팅인데 -,.-;;;