주요글: 도커 시작하기
반응형
Apache Shiro는 사용하는 입장에서 보면 매우 단순한 API를 제공하고 있다. 예를 들어, 아래 코드는 Shiro가 제공하는 Subject를 이용한 로그인/권한검사/로그아웃 코드 예를 보여주고 있다.

Subject subject = SecurityUtils.getSubject();

subject.login(new UsernamePasswordToken(id, password);

// ROLE 체크
subject.checkRole("MEMBER");
subject.checkRole("TEAMLEADER");

// 권한 체크
subject.checkPermission("project:approve");
subject.checkPermission("team:summary:regist");

subject.logout();

위 코드만 보면 '아~ 정말 쉽다'라고 하면서 도전해 보고 싶은 마음이 생기게 된다. 하지만, Shiro의 '정말 쉽다'는 어디까지나 자체적으로 사용하는 회원 DB와 역할/퍼미션 매핑 DB가 없다는 가정하에서 '정말 쉽다'이다. 회원 DB와 역할/퍼미션 DB가 자체적으로 존재하고, 이 DB에 맞게 Shiro를 커스터마이징하려면 전체 클래스 구성을 이해하는 것이 중요하다. 제대로 된 이해 없이 쉬울 것 같아 덤볐다간 뭘 해야할지 몰라 멍 때리며 시간만 보내게 될 것이다.

최근에 Apache Shiro를 기존 어플리케이션에 적용할 일이 있었는데, 몇 가지 커스터마이징을 해야 했다. 커스터 구현체를 Shiro 프레임워크에 꽂아주기 위해 Shiro의 전체 구성과 동작 방식을 분석했으며, 이 글을 통해 전체 구조와 커스터마이징 포인트를 찍어 보고자 한다.

Apache Shiro의 SecurityManager

Apache Shiro를 보안 프레임워크로 사용하려면 Apache Shiro의 핵심인 SecurityManager에 대한 이해가 필요하다. 이걸 이해해야 비로서 어느 부분에 커스텀 구현체를 꽂아 넣을 지 알 수 있기 때문이다.

SecurityManager 및 구현 클래스, 그리고 SecurityManager가 동작하는 데 사용되는 협업 클래스의 구성은 아래  아래 그림과 같다.



SecurityManager 인터페이스는 인증/권한 검사/세션 관리와 관련된 모든 기능을 정의하고 있다. 예를 들어, 로그인 처리를 위한 login() 메서드, 권한 검사를 위한 hasRole(), checkPermission() 등의 메서드를 정의하고 있다.

SecurityManager의 하위 클래스들은 각각 특정 역할을 수행하며, 실제로 우리가 사용하는 구현 클래스는 DefaultSecurityManager와 DefaultWebSecurityManager이다. 계층에서 각 클래스의 역할은 아래와 같다.
  • RealmSecurityManager: Realm 목록을 관리해준다.
  • AuthenticatingSecurityManager: 인증 처리를 Authenticator에 위임하는 기능을 제공한다.
  • AuthorizingSecurityManager: 권한 검사 처리를 Authorizer에 위임하는 기능을 제공한다.
  • SessionSecurityManager: 세션 관리 기능을 제공한다.
  • DefaultSecurityManager: 기본 구현 클래스이다. '기억하기(Remember Me)' 기능을 추가로 제공한다.
  • DefaultWebSecurityManager: 웹 어플리케이션에서 사용되는 구현 클래스이다.
DefaultSecurityManager 클래스를 사용하면 인증 처리와 권한 검사 처리는 각각 Authenticator와 Authorizer에 위임하므로, 자신의 어플리케이션에 맞게 인증/권한 검사 처리를 수행하려면 Authenticator와 Authorizer 구현 클래스를 제공하면 된다.

Authenticator와 Authorizer를 따로 지정하지 않으면 각각 ModularRealmAuthenticator와 ModularRealmAuthorizer를 구현 클래스로 사용하며, 이 두 ModularRealm 객체는 다시 Realm에게 인증 처리나 권한 처리를 위임한다. 예를 들어, 기본 구현 클래스를 사용할 경우 Subject.login()을 실행하면 다음의 흐름에 따라 인증 처리를 수행하게 된다.


그렇다면 ModularRealmAuthenticator가 위임하게 되는 Realm은 어디서부터 구할까? 이 목록은 RealmSecurityManager가 관리하는 Realm 목록을 사용하게 된다. DefaultSecurityManager가 RealmSecurityManager를 상속받고 있으므로, RealmSecurityManager가 내부적으로 관리하고 있는 Realm 객체들에 위임을 하게 된다. ModularRealmAuthorizer도 동일한 과정을 거쳐 Realm에 역할/퍼미션 검사를 위임한다.

지금까지의 설명을 바탕으로 필자는 다음의 두 가지 정도의 확장 포인트가 유용할 거라 생각했다.
  • Authenticator
    • 만들려는 어플리케이션의 환경에 들어 맞는 인증 수단을 제공하는 Realm이 없다면, Authenticator를 직접 구현한다.
    • 적용 가능한 Realm이 있다면, 그 Realm을 사용하거나 또는 Realm을 확장해서 구현한다.
  • Realm
    • 인증/권한 검사에 적합한 Realm 구현체가 존재하지 않으면 Realm을 직접 구현한다.
    • 또는, 기존의 Realm 구현 클래스를 확장해서 구현한다.
필자의 경우는 인증 처리를 위해 Authenticator를 새로 구현하였고, 역할/권한 구현을 위해 기존에 존재하던 Realm을 확장해서 구현했다.

Apache Shiro의 Realm

앞서 살펴봤듯이 DefaultSecurityManager는 (ModularRealmAuthenticator와 ModularRealmAuthorizer를 사용한다는 가정하에) 최종적으로 Realm을 이용해서 인증과 권한 검사를 수행하게 된다. 그렇다면 Realm은 뭘까? Realm은 인증과 관련된 유저 정보와 역할/퍼미션과 관련된 정보를 담고 있는 DB라고 생각하면 된다.

Shiro가 제공하는 Realm 구현 클래스는 아래 그림과 같다.


각 Realm 구현 클래스의 역할은 다음과 같다.
  • AuthenticatingRealm: 인증 토큰(AuthenticationToken-예를 들어, 아이디/암호)에 일치하는 인증 정보(AuthenticationInfo)를 제공한다. 인증 토큰이 인증 정보와 일치할 경우 인증된다.
    • CredentialsMatcher: 인증 토큰과 인증 정보가 일치하는지 검사한다.
  • AuthorizingRealm: 역할/권한 검사를 위한 기반 기능을 제공한다. 역할/권한 검사 기능을 제공할 Realm은 이 클래스를 상속 받아 구현하면 좀 더 쉽게 구현할 수 있다.
  • SimpleAccountRealm: 메모리 상에 사용자 정보/역할 정보/퍼미션 정보를 관리하는 경우에 사용한다.
    • IniRealm: INI 형식의 파일로 사용자/역할/퍼미션 정보를 설정할 수 있도록 해 주는 Realm. 즉, INI 파일을 사용자/역할/퍼미션에 대한 DB로 사용한다. Shiro를 테스트 해 보거나 사용자 계정에 변경이 거의 없는 단순한 어플리케이션에 한해서 사용하는 것이 좋다.
  • JdbcRealm: DB로부터 사용자 정보, 역할 정보, 퍼미션 정보를 가져오는 Realm 구현 클래스이다.
Apache Shiro를 이용해서 인증/권한 검사를 수행하려면 결국 알맞은 Realm을 선택해서 사용하거나 새로운 Realm을 구현해 주어야 한다. 필자의 경우 DB에 역할과 퍼미션 등의 정보를 넣었기 때문에 JdbcRealm을 상속받아 커스텀 Realm을 구현해 주었다.

Shiro 커스텀 구현을 위해 더 알아야 할 내용들

인증을 수행하는 Authenticator는 인증 처리를 위해 다음의 메서드를 제공하고 있다.

public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)

앞서 커뮤니케이션 다이어그램에서 봤듯이, Subject의 login() 메서드를 호출하면 실제로 Authenticator의 authenticate() 메서드가 호출된다. 이 때, Authenticator가 리턴하는 타입은 AuthenticationInfo 이다. 이 AuthenticationInfo는 다음과 같이 정의되어 있다.

public interface AuthenticationInfo extends Serializable {
    // 내가 누구인지를 알려주는 정보
    PrincipalCollection getPrincipals();

    // 날 증명하는 정보
    Object getCredentials();
}

PrincipalCollection에는 ID와 같은 정보가 기록되며, Authenticator가 생성한 PrincipalCollection 정보는 Subject에 전달된다. 실제로 Shiro에서 '사용자'를 표현할 때 사용되는 Subject는 다음의 메서드를 이용해서 그 사용자가 누구인지를 알 수 있도록 하고 있다.

// Subject에 정의된 메서드

Object getPrincipal(); // PrincipalCollection에서 주요 Principal 객체를 리턴
PrincipalCollection getPrincipals();

Subject가 checkPermission(String permission)와 같은 메서드를 실행하게 되면, Subject는 내부적으로 SecurityManager에 그 처리를 위임하는데 그 때 호출하는 메서드는 아래와 같다. 즉, 권한 검사를 수항핼 때 마다 Authenticator가 생성한 PrincipalCollection 객체가 SecurityManager에 전달된다.

// SecurityManager에 정의된 메서드

void checkPermission(PrincipalCollection subjectPrincipal, String permission)

(DefaultSecurityManager라는 가정하에) SecurityManager는 다시 Authorizer에 권한 검사를 위임하게 되고, Authorizer가 ModularRealmAuthorizer인 경우 Authorizer는 다시 Realm에 권한 검사를 위임하게 된다. 이 얘기는 Subject의 checkPermission() 메서드를 호출하면 최종적으로 Subject가 전달한 PrincipalCollection 객체가 Realm까지 전달된다는 것을 의미한다.

이는 PrincipalCollection이 내부적으로 갖고 있는 '사용자 정보'의 타입 문제가 발생할 수 있음을 말한다. 예를 들어, Authenticator가 Employee 타입을 갖는 객체를 PrincipalCollection에 넣었는데 Realm은 String 타입을 필요로 한다고 해 보자. 이 경우 타입 불일치로 인해 권한 검사가 정상적으로 동작하지 않을 것이다.

Shiro의 기본 컴포넌트를 사용하면 하나의 Realm이 인증도 하고 권한 검사도 같이 하기 때문에, PrincipalCollection 타입을 자신에 맞게 맞추게 된다. 하지만, 인증을 수행하는 Realm과 권한을 검사하는 Realm이 다를 경우 또는 인증은 커스텀 Authenticator로 수행하고 권한 검사는 제공되는 Realm을 사용할 경우에는 PrincipalCollection에 저장될 '사용자 정보' 타입을 맞춰주어야 한다. 실제로 JdbcRealm의 경우 PrincipalCollection에 보관된 '사용자 정보' 객체를 무조건 String으로 타입 변환해주는 코드가 있었는데 필자가 작성한 Authenticator는 String이 아닌 다른 타입의 객체를 PrincipalCollection에 저장해서 타입 변환 오류가 발생했었다. 이 문제를 해소하기 위해 부득이 JdbcRealm을 상속받은 커스텀 Realm을 만들게 되었다.

정리

이 글에서 Shiro에 대해 자세하게 살펴본 것은 아니지만, 이 정도 내용이면 Shiro를 사용할 때 커스터마이징 지점을 찾는데에는 도움이 될 거라 생각한다. Shiro 프레임워크를 사용해서 보안 기능을 적용하고자 하는 개발자들에게 이 글이 도움이 되길 바라며, 글을 마친다.




+ Recent posts