[Spring Security] http 네임스페이스 쓰려면 필터 이름은 항상 고정(?)

http://static.springsource.org/spring-security/site/docs/3.0.x/reference/appendix-namespace.html#nsa-http-attributes

<filter>
<filter-name>securityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
   <filter-mapping>
       <filter-name>securityFilterChain</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>
    
자 이렇게 필터를 설정하고 
<http>
        <intercept-url pattern=”/base/color/mgt” access=”ROLE_USER” />
        <intercept-url pattern=”/**” access=”IS_AUTHENTICATED_ANONYMOUSLY” />
<form-login login-page=”/login” />
<logout logout-success-url=”/index” />
<remember-me />
</http>
    <beans:bean id=”smdisUserDetailsService” class=”smdis.common.security.SmdisUserDetailsService”/>
<authentication-manager alias=”authenticationManager”>
<authentication-provider user-service-ref=”smdisUserDetailsService”/>
</authentication-manager>
<global-method-security secured-annotations=”enabled”
jsr250-annotations=”enabled” pre-post-annotations=”enabled” />
이렇게 시큐리티 설정을 했다.
잘 돌아갈까?? 안 돌아간다.. 시큐리티 네임스페이스를 사용해서 <http>를 등록하면 FileChainProxy 빈 이름은 항상 springSecurityFilterChain이 된다. 그래서 필터 이름을 springSecurityFilterChain으로 설정해줘야 한다.
뭐.. DelegatingFilterProxy 필터 자체에 targetBeanName 속성을 사용해서 연결할 빈 이름을 설정할 수도 있지만 기본적으로 이 이름은 필터 이름을 따르게 된다. 필터 이름을 바꾸고 targetBeanName을 또 설정해 주느니 그냥 필터 이름을 springSecurityFileterChain으로 하는게 좋겠다.

<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>

[Spring Security] sec:authentication

참조: http://static.springsource.org/spring-security/site/docs/3.0.x/reference/taglibs.html

스터디의 각 모임에는 댓글을 달 수 있습니다. 삭제 기능이 필요해져서 댓글 삭제 기능을 만들었는데 이것을 일반적인 User-Role 기반 Voter를 사용해서 권한 처리하는것이 그렇게 단순하지는 않습니다. 
관리자 권한이 있는 사람만 삭제할 수 있게 할까요? 아님 회원 권한을 가진 사람은 누구나 삭제하게 할까요? 비즈니스 룰이야 정하기 나름이지만 그렇게 무식하게 정하고 싶지는 않았습니다. 작성자만 자신이 작성한 댓글을 삭제하는 것이 가장 정당해 보입니다.
먼저 문제가 되는 부분은 뷰. 삭제 버튼을 감춰야 합니다.
<sec:authentication property=”principal.username” var=”currentUserName”/>
<c:if test=”${currentUserName == comment.writer.email}”>
//감출 버튼
</c:if>
이렇게 authenrication 태그를 사용해서 principal 객체와 그 속성값에 접근할 수 있습니다. 현재 로그인 되어있는 사용자의 username을 가져와서 현재 댓글 작성자의 email과 비교합니다. 봄싹에서는 email을 아이디로 사용하고 있기 때문에 이렇게 비교합니다. 그 둘이 일치하는 경우에만 버튼을 보여줍니다. 
참 쉽죠;
하지만… 버튼을 감춘다고 다가 아니죠. URL 보내면 보내집니다. 웹 보안으로는 위에서 언급했듯이 힘듭니다. 이럴 때는 서비스 쪽에서 객체 정보와 pricipal 정보를 기반으로 권한 처리를 해주면 되겠습니다. 그건 다음에.

[Spring Security] Method Security Test

    @PreAuthorize(“hasRole(‘ROLE_MEMBER’)”)
    public boolean write(String contents) {
        while (graffitiRepository.getTotalRowCount() >= GRAFFITI_LIMIT_COUNT) {
            graffitiRepository.deleteFirstGraffiti();
        }
        Graffiti graffiti = new Graffiti(contents, securityService.getCurrentMember());
        graffitiRepository.add(graffiti);
        return true;
    }

위 코드는 봄싹 낙서장 서비스의 코드이다. 봄싹 메인 화면에 있는 낙서장은 최대 100개를 유지하며 ROLE_MEMBER 권한을 가지고 있는 사용자(이메일 인증 절차를 거친 기본 회원)라면 누구나 낙서를 추가할 수 있다.

이 코드의 내용 대부분은 단위 테스트로 커버가 가능하다. 하지만 스프링 시큐리티 애노테이션이 제대로 동작하는지는 어떻게 테스트 할 것인가?

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {“/testContext.xml”, “/testContext-security.xml“})
@Transactional
public class MethodSecurityTest extends DBUnitSupport{

    @Autowired GraffitiService graffitiService;
    @Autowired GraffitiRepository graffitiRepository;

    @Before
    public void login() throws Exception {
        insertXmlData(“testData.xml”);
        SecurityContext securityContext = new SecurityContextImpl();
        Authentication authentication = new UsernamePasswordAuthenticationToken(“whiteship@email.com”, “passwd”);
        securityContext.setAuthentication(authentication);
        SecurityContextHolder.setContext(securityContext);
    }

    @Test
    public void methodAuth(){
        assertThat(graffitiRepository.getAll().size(), is(0));
        graffitiService.write(“hi”);
        assertThat(graffitiRepository.getAll().size(), is(1));
    }

    @After
    public void after(){
        SecurityContextHolder.clearContext();
    }

}

이런식으로 테스트 할 수 있다.

1. 시큐리티 설정 파일을 테스트에서 만들 AC(ApplicationContext)용 설정에 추가한다.
2. DB에 xml로 작성한 가짜 사용자, 권한 정보를 넣어야 하니까 DBUnit을 편하게 사용할 수 있게 해주는 클래스를 이용한다.
3. 테스트 실행하기 전에 로그인 한다. (SecurityContextHolder에 SecurityContext를 넣는다.)
4. 테스트 실행한 뒤에 로그오프 한다. (SecurityContextHolder를 비워버린다.)

[권한] 3단 구조

봄싹은 권한 관리는 3단계로 설계했습니다.

사용자 – 역할 – 권한
Member *—->1 Role *—->1 Right

현재 봄싹의 권한 관리 구조와 Role을 주로 사용하고 있습니다. 따라서 *와 **등을 이용해서 여러 URL을 큰 뭉탱이로 ROLE_ADMIN, ROLE_MEMBER로 구분하고 있죠. 굉장히 Coarse-grained 한 설정이죠. 이런 상태라면 설정하기는 간편하지만, 만약에…

“일반 회원 중에서 관리자는 아니지만 스터디를 관리할 수 있었으면 좋겠다.”

이런 시나리오를 만족시키려면 다음과 해야합니다.

1. StudyManager라는 Role을 만듭니다.
2. 해당 사용자에게 StudyManager Role을 추가해줍니다.
3. URL 설정 및 메서드 보안 설정을 찾아다니며 hasRole(Role_StudyManager)를 추가해줘야 합니다.

만약에 URL 설정이 여러줄이고 메서드 보안이 여러줄이라면 어떻게 될까요??
이런 상태에서 RIght라는 것은 의미가 있기나 할까요?

그럼, URL 설정을 Right 기반으로 바꿨다고 가정하겠습니다. 아마 지금보다 더 세부적으로 URL 권한을 설정할테니 갈아타는데 손이 좀 갈 것으로 예상됩니다. Role 기반 설정에 비하면 fine-grained 합니다.

전부 Role 기반으로 바꿨다는 가정하에 다시 한 번 위의 시나리오를 적용하는 과정을 상상해 보겠습니다.

1. StudyManager라는 Role을 만들고
2. 해당 Role에 Add_Study, Update_Study, Delete_Study, End_Study, Start_Study 등 Study와 관련된 모든 권한을 추가해둡니다.
3. 해당 사용자에게 StudyManager 권한을 줍니다.

StudyManager라는 Role을 만드는 과정이 다소 귀찮을 순 있겠지만, A라는 사용자 말고 B 사용자에게 위와같은 시나리오를 적용해야 하는 상황이 온다면 어떨까요? 1, 2번은 필요없고 3번만 하면 됩니다.

“스터디 관리자여도 스터디 삭제는 못하게 하고 싶다. 그건 오직 관리자만 할 수 있게해야지”

이런 시나리오를 적용해야 한다면 Role 기반의 설정을 사용할때는 분명 study/34/delete.do URL이나 deleteStudy(study) 메서드에 hasRole(Role_StudyManager, Role_Admin)이라고 되어있을텐데, 저걸.. 다시 hasRole(Role_ADMIN)으로 바꿔줘야 할 겁니다.

그런데 Right기반으로 설정되어 있는 경우 hasRole(Right_Delete_Study)라고 설정되어 있을것이기 때문에 건드릴건 아무것도 없고, 단지 Role_StudyManager에서 Right_Delete_Study 만 빼주면 됩니다.

앗;; 전 이만 저녁먹으러…

스프링 시큐리티 맞춤확장(customization) – 파트 1. UserDetail 또는 GrantedAuthority 맞추기

참조 및 번역: http://blog.springsource.com/2009/01/02/spring-security-customization-part-1-customizing-userdetails-or-extending-grantedauthority/

이번 글은 스프링 시큐리티 맞춤확장과 관련된 실용적인 예제 중심의 여러 작은 글들의 시리즈 중 첫 번째 글이다. 이번 맞춤확장 요구 사항은 상상에서 온 것이 아니라 전부 현장에서 요구한 것이다.

다음과 같은 요구사항이 있다고 가정해보자. 역할(role) 목록이 있고 각각의 역할은 비즈니스 기능(business function) 목록을 가지고 있다.(아래를 참조하라.)

ROLE_ADMIN
  BF_QUOTE_CREATE
  BF_POLICY_CREATE
  BF_POLICY_DELETE

ROLE_AGENT
  BF_QUOTE_CREATE
  BF_POLICY_CREATE

ROLE_USER
  BF_QUOTE_CREATE

필요한 기술은 권한 결정을 ROLE과 BF 모두를 가지고 결정할 수 있어야 한다는 것이다.

예를 들어:

ROLE_ADMIN이라는 역할을 가진 사용자는 해당 역할로 보호하고 있는 모든 리소스에 접근할 수 있어야 한다.

<sec:authorize ifAllGranted=”ROLE_ADMIN”>
    <p><a href=”http://www.google.com”>Google</a>
</sec:authorize>

또는

@Secured(“ROLE_ADMIN”)
public void foo()
    . . .
}

또한 해당 사용자는 해당 역할이 가지고 있는 비즈니스 기능으로 제한하고 있는 모든 리소스에도 접근할 수 있어야 한다.

<sec:authorize ifAllGranted=”BF_POLICY_DELETE”>
    <p><a href=”http://www.google.com”>Google</a>
</sec:authorize>

또는

@Secured(“BF_POLICY_DELETE”)
public void foo()
    . . .
}

이 요구사항을 다루는 방법은 몇 가지가 있다. 그 중 하나는 RoleHierarchy를 만들고 RoleHierarchyVoter를 사용하여 역할의 계층 구조를 순회하는 것이다. 이 접근 방법의 단점은 현재 스프링 시큐리티 2.0.4 구현체의 태그 라이브러리(security: authority …)가 AccessDecisionManager를 통해서 의사 결정을 하고 있지 않으며 게다가 어떤 Voter도 제한하고 있는 HTML 엘리먼트에 대한 결정을 할 때 Role을 신경쓰지 않는다. 하지만 스프링 시큐리티의 놀라운 유연함과 맞춤확장 힘으로 인해 이 요구사항을 매우 간단하게 해결할 수 있다.
스프링 시큐리티의 가장 큰 잇점 중 하나는 어떻게 Principal(UserDetail 객체)을 생성하는지 맞춤확장 할 수 있다는 것이다. UserDetail 객체를 만들 때 GrantedAutorities 목록을 생성한다. 이 목록은 나중에 리소스를 제한하고 있는 GrantedAutority와 비교해볼 때 사용된다.
맞춤확장을 하는 또 한 가지 방법은 UserDetail 객체를 생성할 때 GrantedAuthorities 목록을 수정하는 것이다.

아래에서 제공하고 있는 예제에는 두 개의 프로퍼티 파일이 있다.(간단하게 하려고 프로퍼티 파일을 사용했지만, 여러분은 쉽게 그것을 DB나 LDAP으로 바꿀 수 있을 것이다.)

파일 하나는 users.properties로 사용자를 role에 맵핑한다.

oleg=powder,ROLE_ADMIN

다른 파일 하나는 role-to-db.properties로 각각의 role에 비즈니스 기능 목록을 맵핑한다.

ROLE_ADMIN=BF_QUOTE_CREATE,BF_POLICY_CREATE,BF_POLICY_DELETE

이제 필요한 작업은 UserDetailsService 구현체를 정의하여 위 두 개의 프로퍼티 파일을 사용하여 GrantedAUthorities 목록을 만들고 그것을 UserDetails 객체에 주입하는 것이다. 이 작업은 GrantedAuthorityImpl같은 GrantedAuthority 인터페이스 구현체를 재사용할 수 있으며 매우 간단하다. 하지만, 우리는 이때 (디버깅 또는 다른 의도로) 비즈니스 기능을 표현하는 GrantedAuthority의 상위(parent) GrantedAuthority를 추적할 수 있도록 하고 싶다.
이 두 가지 목적을 달성하기위해 GrantedAuthorityImpl을 확장하는 BusinessFunctionGrantedAuthority 클래스를 정의하고 모든 상위 GrantedAuthority 객체에 대한 목록을 담고 있게 했다.

public class BusinessFunctionGrantedAuthority extends GrantedAuthorityImpl {
    private List<GrantedAuthority> parentAuthorities;
        . . .
}

그런 다음 UserDetailsService 구현체를 만들었고 loadUserByNames(..) 메소드를 구현하여 다음 작업을 수행하도록 했다.

1. users.properties 파일 내용을 기반하여 UserAttribute 객체를 만든다. UserAttribute는 role을 나타내는 GrantedAuthority 목록을 담고 있다.
2. role-GrantedAuthorities 목록을 순회하면서 각각의 role-GrantedAuthorities 마다 BusinessFunctionGrantedAuthority를 만들고 그것을 미리 만들어둔 GrantedAuthority 목록에 추가한다.
  2-1. 각각의 BusinessFunctionGrantedAuthority에 parent GrantedAuthority를 추가한다.
3. 전체 GrantedAuthority 목록을 가지고 있는 최종 UserDetail 객체를 만든다.

그런 다음 AuthenticationProvider를 스프링 시큐리티 설정 파일에 정의한다.

사용자 삽입 이미지
노트: AuthenticationProvider에 커스텀 UserDetailService 구현체 ComplexAuthorityUserDetailsService 클래스(더 자세한 내용은 샘플 코드를 살펴보라.)를 주입했다.

리소스를 보안하고 배포한 다음 http://localhost:8080/spring-security-sample-grantedAuthority/index.jsp에 접속하라.

로그인한 뒤 여러분은 GrantedAuthorities 목록을 볼 수 있을 것이다.

사용자 삽입 이미지
여기서 여러분은 비즈니스 기능을 나타내는 GrantedAuthority가 해당 비즈니스 기능을 가지는 상위 GrantedAuthorities 목록도 보여주고 있다는 것을 확인할 수 있을 것이다.
index.jsp를 살펴보고 어떻게 security:authorize 태그를 사용ㅇ하여 role과 비즈니스 기능 목록을 사용하여 HTML 엘리먼트를 제한하는지 살펴보라
이게 끝이다. 여러분은 최소한의 맞춤확장으로 쉽게 Principal 구조를 확장하고 커스터마이징 할 수 있는지 살펴보았다. 그리고 이것을 비즈니스 코드를 더럽히지 않고 커스텀 GrantedAuthorities에 기반하여 선언적으로 보안을 적용하는지 살펴보았다.

샘플 코드는 여기서 다운로드 할 수 있다. spring-security-sample-grantedauthority