Whiteship's Note


[스프링 시큐리티 3.0] @PostAuthorize

분류없음 : 2009.08.23 15:27


이 녀석도 아주 유용한 애노테이션입니다. @PreAuthorize랑 비슷하게 권한을 확인하지만, 차이점은 메서드를 일단 실행한 뒤에 권한을 확인한다는 것입니다. 처음에는.. 이런 생각을 했었습니다.

이게 뭐야.. @_@.. 이미 실행 한 뒤에 권한을 체크하면... 무슨 소용이지??

그런데 막상 보안관련 코드를 작성하다 보면 그럴 일이 생기더군요. 예를 들어, 봄싹 프로젝트에서 개인 정보 수정 기능이 있는데, 이 때, springsprout.org/member/update/{id}.do 이런 URL 구조를 사용합니다. 이 기능은 컨트롤러에서 GET, POST 2단으로 나눠서 처리합니다. 아주 일반적인 경우죠.

    @RequestMapping(value = "/member/update/{id}.do", method = RequestMethod.GET)
    public String updateForm(@PathVariable int id, Model model)
            throws ServletRequestBindingException {
        model.addAttribute(service.getMemberById(id));
        return "member/update";
    }


    @RequestMapping(value = "/member/update/{id}.do", method = RequestMethod.POST)
    public String updateForm(@PathVariable int id, Member member, BindingResult result,
            SessionStatus status, HttpSession session) throws ServletRequestBindingException {
        validator.validate(member, result);
        if (result.hasErrors()) {
            return "member/update";
        } else {
            service.update(member);
            status.isComplete();
            session.setAttribute("SESSION_FLASH_MSG", "회원정보가 수정되었습니다.");
            return "mypage/index";
        }
    }

시큐리티 보안 기능 중 하나인 메서드 보안 말고, URL 보안으로 MEMBER나 ADMIN 권한이 있는지 정도는 확인할 수 있습니다.

그러나.. GET 요청일 때, 만약 다른 회원 정보를 보기 위해 URL에서 자신의 id 가 아닌 다른 id를 입력한다면 어떻게 될까요? URL 보안은 못 막습니다. 아마도 getMember() 앞쪽에 이런 코드가 들어갈 겁니다.

    public boolean isCurrentUserOrAdmin(int id) {
        if(!isCurrentMembersInfo(id) && !isAdmin())
            throw new AccessDeniedException("다른 회원의 정보에 접근을 시도할 경우 계정이 차단 됩니다.");
        return true;
    }
   
    private boolean isCurrentMembersInfo(int id) {
        return getCurrentMemberId() == id;
    }

isCurrentUserOrAdmin 같은 걸 호출해서 확인을 거친 다음에 getMember()를 호출할 수 있게 해야합니다.

POST 요청은 어떤가요? 누군가 POST 요청을 임의로 만들어서 접근을 시도한다면?? 그 경우도 막기 위해 위와 같은 코드를 update 하기 전에 실행해야 합니다. 이 경우는 이전 글에서 살펴보았던, @PreAuthorize가 적당하기 때문에 쉽게 바꿀 수 있습니다.

    @PreAuthorize("hasRole('ROLE_ADMIN') or (#member.email == principal.Username)")
    public void update(Member member) {
        member.loadAvatar();
        repository.update(member);
    }

이렇게 말이죠. 굳이 컨트롤러에 시큐리티 관련 코드를 삽입할 필요가 없습니다. 그럼 GET 요청 처리는 어떻게 할까요??

    @PostAuthorize("(returnObject.email == principal.Username) or hasRole('ROLE_ADMIN')")
    public Member getMemberById(int id) {
        return repository.getMemberById(id);
    }

이렇게 getMember() 위에 @PostAuthrize를 붙여서 해결할 수 있습니다. 현재 상요자가 화면에 보여주려는 객체에 대한 권한이 있는지 혹은 관리자인지 확인해 보는 것이죠.

결론은.. @PostAuthorize도 매우 유용하답니다.

그런데 참고할만한 자료가 너무 없네요. 애노테이션 API에서 EL에서 자주 쓰일만한 기본 내장 객체 이름 (principal 이나 returnObject) 들을 알려줬으면 하는데 그런 내용이 없습니다.

거의 유일한 참고 자료는 예전에 번역/편역/요역해서 올렸었던 글 하나 뿐..

http://blog.springsource.com/2009/06/03/spring-security-300m1-released/
http://whiteship.me/2257

top

Write a comment.


[스프링 시큐리티 3.0] @PreAuthorize

Spring Security/etc : 2009.08.23 14:38


    <global-method-security secured-annotations="enabled"
        jsr250-annotations="enabled" pre-post-annotations="enabled" />

시큐리티 설정 파일에 위와 같이 설정하면 @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter를 사용할 수 있습니다.

이들 애노테이션에서는 스프링 EL을 사용해서 현재 사용자 정보에 접근하거나, (pre 인 경우)메서드의 인자값 또는 (post 인 경우)메서드의 반환값의 정보에 접근할 수 있습니다.

    @PreAuthorize("(#study.manager.email == principal.Username) or hasRole('ROLE_ADMIN')")
    public void updateStudy(Study study) {
        repository.update(study);
    }

위 예제는 다음 주에 오픈 할 봄싹 프로젝트에서 사용하고 있는 코드입니다. Study를 수정하려는 사람이 관리자이거나, 스터디를 만든 사람인지 확인한 뒤에 메서드를 실행합니다. 만약 해당 EL이 false로 판단되면 Access Dinied 에러를 던져줍니다.

애노테이션을 메서드에만 붙이지 않고 클래스에도 붙여서 클래스에 정의한 모든 메서드에 적용할 수도 있습니다. 이런식으로요.

@Service
@Transactional
@PreAuthorize("hasRole('ROLE_USER')")
public class StudyService {

...

}





top

Write a comment.


스프링 시큐리티 2.X -> 스프링 시큐리티 3.X

모하니?/Coding : 2009.07.17 19:52


하루동안 시큐리티를 두 번 각각 다른 버전으로 적용해보니 차이점이 보입니다.

시큐리티를 확장할 수 있는 포인트가 하두 여러 가지라 그만큼 확장하는 방법도 다양하겠지만, 저는 톱님 코드를 보고 그대로 적용해 봤습니다.

적용하는 방법은 별도의 UserDetailsService와 UserDetails를 구현하는 방법입니다. 그리고 여기서 구현한 UserDetailsService를 빈으로 등록해 주는 거죠.

    <http>
        <intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
        <intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
        <form-login login-page="/login.do"
            authentication-failure-url="/login.do?login_error=t"
            default-target-url="/main.do" />
        <logout logout-success-url="/main.do" />
        <anonymous/>
        <remember-me />
    </http>

    <authentication-provider user-service-ref="customUserDetailsService" />

    <global-method-security secured-annotations="enabled"
        jsr250-annotations="enabled" />

설정은 이렇게 간단해졌지만, 내부에서 해주는 일은 여전히 필터체인프록시 와 여러 개의 필터, 프로바이더, 엔트리포인트 등이 수고 해 줍니다.

일단 위 설정은 2.X 대의 설정인데, 왜냐면, <anonymuos />가 있기 때문입니다. 이 녀석은 익명사용자를 나타내는  IS_AUTHENTICATED_ANONYMOUSLY를 쓸 때 필요한데, 3.X에서는 이 엘리먼트를 사용하지 못합니다. 하지만 IS_AUTHENTICATED_ANONYMOUSLY는 기본으로 쓸 수 있습니다. 클래스가 없어졌거나 패키지 이동을 했을 겁니다.

패키지가 바꼈습니다. 확장해야 할 인터페이스 2개

import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService;

이 녀석들이 3.X에서는

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

이쪽으로 옮겨갔습니다. 이밖에도

import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;

이 녀석들도 같이 이동했습니다. 다행히 클래스 이름이 같기 때문에 마이그레이션 할 때 별 어려움은 없을 것입니다.

마지막으로 인터페이스가 바뀐 녀석이 있습니다. UserDetails는 2.X에서

public GrantedAuthority[] getAuthorities()

이런 인터페이스를 가지고 있었는데. 3.X에서는 List 타입을 반환하도록 바꼈습니다. 배열을 채워줄때는 배열 사이즈를 미리 알아야 하기 때문에 불편한 코딩이 조금 추가되는데, 3.X에서는 사이즈 상관없이 그냥 add만 해주면 되니까 조금 더 간편해 졌다고 느껴지네요.

(톱님 따라 저도) 오늘의 결론
- 시큐리티 2.X에서 3.X로 넘어가는 길이 아주 편한건 아니지만, 그리 불편하지도 않네요.







top

  1. Favicon of http://toby.epril.com BlogIcon 2009.07.17 21:15 PERM. MOD/DEL REPLY

    톱님은 누구야?

    Favicon of http://whiteship.tistory.com BlogIcon 2009.07.17 21:33 PERM MOD/DEL

    글쎄요. 누굴까요?

Write a comment.


스프링 시큐리티 3.0.0 M1 배포



참조 편역 요역: http://blog.springsource.com/2009/06/03/spring-security-300m1-released/

http://www.springsource.com/download 에서 직접 다운로드 할 수도 있고, 메이븐을 사용한다면, http://maven.springframework.org/milestone 메이븐 저장소를 추가하면 M1 의존성을 추가할 수 있다. JDK 1.5 이상, 스프링 3.0이 필요함.

표현식-기반 접근 제어

스프링 EL 기반 권한 관리를 지원한다. 메서드 애노테이션이나 웹 시큐리티에서 표현식을 사용할 수 있다. 속성이나 보터(voter)-기반 매커니즘에 비해 새로운 조합을 꾀할 수 있다. 다음은 웹 시큐리티에서 시큐리티 네임스페이스를 사용하는 간단한 예제다.

<http>
   <intercept-url pattern="/secure/**" access="hasRole('ROLE_SUPERVISOR') and hasIpAddress('192.168.1.0/24')" />
   ...
</http>

hasRole('ROLE_SUPERVISOR')은 사용자의 권한 목록을 확인하고 사용자가 해당 롤을 가지고 있다면, true를 반환한다. 여기에 IP를 확인할 수 있는 새로운 표현식을 추가했다.

@Pre와 @Post 애노테이션

메서드 시큐리티는 웹 요청을 수락하거나 거부하는 것과는 달리 조금 더 복잡하다. 메서드 시큐리티에 표현식을 사용해서 좀 더 다양한 기능을 제공하기 위해, 4개의 새로운 애노테이션을 추가했다. 이 애노테이션들을 사용하여 메서드 호출 전과 후에 특정 로직을 실행할 수 있다. 이 기능을 사용하려면 global-method-security 네임스페이스 엘리먼트에 새로운 속성을 사용해야한다.

<global-method-security pre-post-annotations="enabled"/>

가장 유용한 것으로 @PreAuthorize가 있는데, 이 애노테이션은 메서드를 실제로 실행할지 말지 여부를 제어한다. 예를 들어(예제 애플리케이션의 Contacts에서) 다음 메서드를 보자.

@PreAuthorize("hasRole('ROLE_USER')")
public void create(Contact contact);

이것은 ROLE_USER라는 롤을 가진 사용자만 접근을 허용한다는 뜻이다. 별 다른게 없다.

@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);

이번에는 메서드 인자를 표현식에서 참조하고 있다. 해당 contact에 대해 현재 사용자가 admin 권한이 있는지 확인한다. hasPermission() 표현식은 애플리케이션 컨텍스트를 통해서 스프링 시큐리티 ACL 모듈과 연결되어 있다.(어떻게 연결되어 있는지는 Contacts 예제를 통해 살펴보기 바란다.) 메서드 인자를 표현식 변수로 참조할 수 있다. 스프링 EL의 모든 기능을 사용할 수 있기 때문에 인자의 속성에도 접근할 수 있다. 따라서 특정 사용자의 이름이 contact의 이름과 대응할 경우로 제한하고 싶을 때 다음과 같은 표현식을 사용할 수도 있다.

@PreAuthorize("#contact.name == principal.name)")
public void doSomething(Contact contact);

여기서는 내장된 표현식 principal을 사용했다. 이 것은 현재 스프링 시큐리티의 Authentication 객체를 기반한 것으로 시큐리티 컨텍스트에서 가져온 것이다. Authenticatino 객체에 직접 접근하려면 authentication 표현식이름을 사용할 수도 있다. 메서드 호출 후에 권한 작업을 수행할 수 있는데, 이 때는 @PostAuthotize 애노테이션을 사용하고, 반환값은 "returnObject"로 참조할 수 있다.

필터링

스프링 시큐리티는 컬렉션과 배열 필터링을 이미 제공하고 있었는데, 이제는 표현식을 사용할 수도 있다.

@PreAuthorize("hasRole('ROLE_USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List getAll();

여기서 filterObject는 반환하는 컬렉션에 들어있는 각각의 요소들을 지칭하고, 해당 요소에 대한 사용자의 권한이 read 이거나 admin이 아닌 것은 컬렉션에서 빼낸다. @PreFilter를 사용해서 메서드 호출 전에 필터링을 할 수도 있지만, 거의 사용하지 않는듯 하다. 문법은 같은데, 인자에 두 개 이상의 컬렉션이 있을 때 filterTarget 속성을 사용하여 어떤것을 사용하는지 지칭한다.

코드기반 재구성

3.0에서 대부분의 코드는 spring-security-core.jar로 들어갔다. 몇 년에 걸쳐 여러 기능이 추가되다보니 의존성간에 CR(circular reference)도 생기고 복잡한 의존성 구조가 되어버렸다. 또한 여러 jar에 나눠져서 들어간 패키지가 OSGi에서 말썽을 일으킨다는 이슈도 있었다. 이로인해 유지보수 오버헤드가 발생했고, 그 걸과 3.0에서 코드기반을 재구성하기로 결정했다.

프로젝트 JAR 파일

- 이 부분은 별도로 포스팅. 여기서는 생략.

이로인해 코드를 순회하며 참조하거나 이해하기 쉬워졌다.

스프링 시큐리티 3.0 JAR 의존성

패키지 구조

더이상 CR도 없고 훨씬 깔끔해졌다.


기타 변경사항

클래스 이름 변경: 이름들이 훨씬 명시적으로 바꼈군요.
AbstractProcessingFilter -> AbstractAuthenticationProcessingFilter
AuthenticationProcessingFilter -> UsernamePasswordAuthenticationProcessingFilter
AuthenticationEntryPoint -> LoginUrlAuthenticationEntryPoint
ObjectDefinitionSource -> SecurityMetadataSource
HttpSessionContextIntegrationFilter -> SecurityContextPersistenceFilter

인증 성공 또는 실패시 리다이렉션/포워딩: 인증 성공 또는 실패시에 브라우저가 이동할 목적지를 제어하는 방법 제공.
AuthenticationSuccessHandler
AuthenticationFailureHandler
http://jira.springsource.org/browse/SEC-745

레퍼런스 매뉴얼과 웹 사이트 업데이트: 아직 작업 중이지만 몇 개 챕터(네임스페이스, 기술 개요)는 업데이트 했다. 프로젝트 사이트의 FAQ도 업데이트 해서 몇몇 발표 비디오와 온라인 기사를 참조할 수 있다.

결론

스프링 EL을 사용하여 기능이 좀 더 풍부해졌고, 코드기반을 깔끔하게 정리했다.
JIRA 변경로그
커뮤니티 포럼
JIRA 이슈
top

  1. Favicon of http://ryys1993.tistory.com BlogIcon 윤성철 2009.06.08 17:47 PERM. MOD/DEL REPLY

    으아 언제나 느끼는거지만 정말 대단하셔요~~

    도데체 저런 그림은 어떻게 그리시는건지~ ^^

    머리에 쏘옥 쏙 들어오네요 ^^.

    저런 그림은 뭘로 그리시는지요? ^^..

    그림판은 아닐텐데 ^^.. 혹시 이클립스 플러긴이 있는건가 흐헉..

    Favicon of https://whiteship.tistory.com BlogIcon 기선 2009.06.08 18:17 신고 PERM MOD/DEL

    절대로 제가 그린게 아니구요.
    원문에 있던거 퍼온거에요.
    오해를 불러일으켜 죄송합니다~

    아마도 이클립스에서 무슨 플러긴으로 자동 생성한 그림이겠거니 추측해 봅니다.

  2. Favicon of http://ryys1993.tistory.com BlogIcon 윤성철 2009.06.09 12:44 PERM. MOD/DEL REPLY

    직접 그리시지 않으셨다 하여도.. 위 기사를 쉽게 접할수 있게 해주시는것에 대해

    매우 감사합니다.

    사실 영어 울렁증때문에 --;;; ㅋ....

    앞으로 영어 공부좀 해야할거같아요 ^^.

    오늘 날 무지 꿀꿀스럽지만

    즐거운 날 되세요~...

    Favicon of http://whiteship.me BlogIcon 기선 2009.06.09 14:03 PERM MOD/DEL

    넵 ㅎㅎㅎ
    시원하고 좋네요. 좋은 하루 되세요~

Write a comment.