Spring MVC

springmvc.egloos.com


포토로그


2012/03/01 08:30

3장 데이터베이스로 이동하는 Authentication 정보 Spring Security

로그아웃 커스터마이징

이제 로그아웃 페이지를 커스터마이징 해볼까 합니다. 만약 로그아웃 주소를 커스터마이징 하지 않는다면 기본적으로 "/j_spring_security_logout"이란 URL을 통해 사용자의 권한을 해제하는 작업을 시작할 수 있습니다. 그러나 이런 눈에 뻔히 보이는 URL을 이용하면 악의적인 공격자가 해당 어플리케이션의 보안이 스프링 시큐리티로 이루어진 것을 알아채고 쉽게 보안취약점을 찾아낼 지도 모릅니다.

<http>에 다음과 같은 요소를 추가함으로서 손쉽게 로그아웃 경로를 커스터마이징 할 수 있습니다.

<logout invalidate-session="true" logout-url="/unAuthentication" logout-success-url="/" />

invaldate-session : 세션을 모두 무효로 할 것인지를 사용자에게 묻습니다.
logout-url : 로그아웃 경로를 설정합니다.
logout-seccess-url : 로그아웃이 성공한 뒤에 이동한 경로를 설정합니다.

MySQL로 Authentication 구현하기

2장에서 말했다시피 서비스를 상용화 하기 위해선 Authentication(인증) 부분을 DB로 이전해야 합니다. 이게 무슨 말이냐면 해당 사용자의 권한(Authority)과, 세부정보(UserDetails)들을 모두 DB에 기록하고 스프링 시큐리티는 그 정보들을 쿼리를 이용해 가져오는 방식으로 바뀐다는 뜻입니다. 당연한 듯 해 보일 수도 있겠지만 이게 기본적인 인증과 권한부여에 대한 개념이 잡혀있지 않으면은 금새 삼천포로 빠지기 쉽상입니다.
더욱이 상용 서비스로 오픈하기 위해서는 인증 자료의 DB이전은 필수 중 하나이며 Authentication을 위한 정보들을 하나도 남기지 않고 말끔히 DB로 옮기기 때문에 이런 과정들이 조금 복잡하고 이해가 가지 않을 수도 있습니다. 일단 쿼리문에 익숙하지 않은 사용자들은 이 부분에서 많이 답답하실 수도 있으므로 MySQL과 쿼리문에 대해서는 따로 사이드 패널을 만들어 정보를 올리도록 하겠습니다.

CREATE TABLE users (
principle int auto_increment primary key,
id varchar(50) not null,
crc_id int not null,
password varchar(50) not null,
enabled boolean not null,
INDEX(crc_id)
);

먼저 위와 같이 사용자의 정보를 담고 있는 테이블을 만들어 봅시다. 더 필요한 정보가 있으시다면 ALTER를 이용해 추가하셔도 상관없지만 지금은 우선적으로 로그인과 권한의 DB이전을 목적으로 하므로 필요한 최소한의 정보만 기입하도록 합시다. 위와 같은 인덱스 구조는 필자가 주로 사용하는 방식이므로 마음에 안드신다면 문서를 모두 읽으신 뒤에 인덱스 설정을 바꾸셔도 상관 없습니다.

'principle' 은 사용자의 유일값을 담는 컬럼입니다. 1장에서 필자가 아이디를 principle이라고 부르기도 한다 했지만 정확히 말하자면 어떠한 값과도 일치하지 않는 사용자의 고유값을 나타내므로 위처럼 id와 분리하여 사용해도 괜찮습니다. principle은 동일한 컬럼 내에서 어떠한 값과도 일치해서는 안되므로 해당 컬럼에 primary key 속성을 부여하였고 별도의 유일값 인증 로직을 구현하지 않기 위해 auto_increment로 자동증가값을 삽입하였습니다. (만약 principle이 공개키가 아닌 보안키라면은 auto_increment로 구현해서는 안됩니다.)

crc_id는 id 컬럼을 crc32로 암호화한 데이터입니다. 왜 해당 컬럼을 crc32로 암호화하여 따로 저장하느냐면 바로 속도 때문인데요. 'id'같이 스트링 값이 들어가는 컬럼을 primary key나 index로 설정한다면 인덱싱 속도가 엄청나게 느려지게 됩니다. 왜 지대한 영향을 끼치게 되는지는 추후에 다른 문서를 통해 설명하도록 하고 여러분은 일단 varchar과 char를 인덱스로 설정하는 것은 가급적 피해야될 사항이라는 것만 기억하시면 됩니다. 

CRC32는 MD5나 SHA1보다 가벼운 데이터 암호화를 위해 만들어진 기법입니다. CRC32의 암호화 값은 모두 int 범위 안에 속하므로 주로 데이터 인덱싱을 위해 자주 사용되곤 합니다. 다만 주의할 것은 이 값을 유일값으로 설정하면은 안되는데, 그 이유는 CRC32는 확률적으로 동일한 암호화 값이 나올 가능성이 다른 암호화 기법에 비해 매우 높기 때문입니다.

enabled는 해당 유저를 활성화할 것인지 비활성화 할 것인지를 묻는 컬럼입니다. 이 컬럼은 필수컬럼이며 향후 해당 사용자의 기록을 지우지 않고서도 비활성화 할 수 있는 기능을 제공할 것입니다.


필자는 스프링 시큐리티를 공부하면서 바로 이 테이블을 작성하는 과정에서 너무 많은 의문이 들어 쉽사리 다음 과정으로 이동할 수가 없었습니다. 거의 몇일을 여기에 목매며 왜 꼭 이래야만 하냐는… 고민을 수없이 되뇌였습니다. 이유는 알고 싶었는데 어떻게 알아가야 하는지 방법은 도통 알 수가 없었죠. 그러다가 문득 든 생각이 스프링 시큐리티의 커맨드 오브젝트(테이블과 매핑되는 자바빈 클래스가)를 역추적하면 이유를 알 수 있겠다는 것이었습니다.

이와 관련해서 잘 이해가 가지 않으신다면 위의 링크된 문서를 읽어보시는 것도 좋습니다. 일단은 여기서 대략적으로 설명드리자면 자바 프로그래밍에서는 자바빈 클래스는 대개 테이블과 밀접한 관계를 맺곤 합니다. 그렇다면… 만약 스프링 시큐리티에서도 테이블의 정보를 담는 빈이 있고 우리가 이 빈을 참조할 수 있다면 왜 이런 테이블 구조를 구성해야 하는지 알 수 있지 않을까요?

이 생각은 그대로 통했습니다. 스프링 시큐리티에서는 테이블에 매핑되는 정보를 User란 자바빈 클래스를 가지고 있었고 테이블과 관계된 내용만 요약해서 보자면 아래의 코드를 통해 이해할 수 있었습니다.

public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this(username, password, true, true, true, true, authorities);
    }

public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {

        if (((username == null) || "".equals(username)) || (password == null)) {
            throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
        }

        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
    }

User 클래스는 UserDetails란 인터페이스를 상속한 클래스이며 위와 같은 생성자를 갖고 있습니다. User 클래스는 오로지 생성자만을 통해 프로퍼티의 설정이 가능하므로 굳이 클래스 전체를 보지 않더라도 위의 생성자만으로 이해가 가능합니다. 여기서 enabled를 제외한 나머지 불리언 값들은 계정의 만료와 락 설정이며 기본적으로 true로 설정되고 있으므로 여기에서는 설명을 생략합니다.

차근차근 프로퍼티를 하나씩 살펴보자면 먼저 username은 테이블의 principle과 일치합니다. 이름이 달라 헷갈릴 수도 있지만 다른 값과 구별되는 유일한 값이라 생각하면 이해하기 쉬우실 겁니다. 물론 principle 컬럼 보다는 crc_id가 유일값으로 적당하지 않겠냐고 생각할 수도 있지만 crc32로 만든 암호화 코드는 데이터가 1만개 이상일 때 약 1%의 확률로 서로 일치하는 값이 생기므로 유일값으로 적당하지 않습니다. (그렇기 때문에 unique index가 아닌 일반 index로 설정해둔 것입니다.)

password는 username의 principle을 확인하는 크리덴셜이며 테이블의 password와 동일합니다. 그 다음 Collection<? extends GrantedAuthority> authorities는 이 유저의 권한을 뜻하는데 좀 이상하게 authority(권한)의 복수형인 authorities(권한들)로 되어 있는데다 Collection 인터페이스 형태로 되어 있습니다. 눈치 채셨겠지만 그 이유는 한 사용자가 하나 이상의 권한을 가질 수 있게 설정할 수 있게 하기 위해서입니다.

근데 authorities를 보면서 조금 싸한 느낌과 함께 이상한 의문이 번뜩 미간을 스칩니다. 왜냐하면 우리가 설정한 테이블에서 authorities는 없었거든요! 스프링 시큐리티에서 users 테이블에 authorities를 함께 설정하지 없는 이유는 하나 이상의 권한을 설정하기 위해서이기도 하지만 또 하나의 이유는 확장성 때문입니다. 지금은 자세히 설명할 수 없지만 스프링 시큐리티에서는 사용자에게 직접 권한을 부여하는 방식과 그룹으로 묶어 권한을 부여하는 방식, 2가지를 제공합니다.

그리고 우리가 지금부터 할 것은 사용자에게 직접 권한을 부여하는 방식입니다. 다음과 같은 권한 테이블을 하나 더 만들어 보도록 합시다.

create table authorities (
principle int not null,
authority varchar(50) not null
unique index ix_auth_principle (principle, authority)
);

이 테이블은 users 테이블과 밀접한 관계를 맺고 있으며 authorities.principle과 users.principle은 서로 일치하므로 레퍼런스 키로 두 키를 묶어줄 수도 있지만 원할한 속도와 MySQL 기능에 너무 의존하지 않기 위해 이런 밀접한 관계는 트랜잭션으로 해결하는 것이 좋습니다.

이제 어느 정도 감을 잡으셨으리라 생각합니다. 글이 길어지는데다 제 지식이 바닥나가는 관계로 오늘은 여기까지 하고 다음 장에서 스프링 시큐리티의 JdbcDaoImpl을 MyBatisDaoImpl로 대체해보고 스프링 시큐리티의 확장 가능성에 대해 이야기 해보도록 하겠습니다.

핑백

  • 스프링 시큐리티 | StyleWear 2015-12-06 12:54:57 #

    ... 3장 데이터베이스로 이동하는 Authentication 정보</a>  로그아웃 커스터마이징 이제 로그아웃 페이지를 커스터마이징 해볼까 합니다. 만약 로그아웃 주소를 커스터마이징 하지 않는다면 기본적으로 &#8220;/j_spring_security_logout&#8221;이란 URL을 통해 사용자의 권한을 해제하는 작업을 시작할 수 있습니다. 그러나 이런 눈에 뻔히 보이는 URL을 이용하면 악의적인 공격자가 해당 어플리케이션의 ... more

덧글

  • 쿠야남 2013/05/16 10:23 # 삭제 답글

    좋은글 정말 감사합니다
  • iskwon 2014/11/27 11:53 # 삭제 답글

    정말 도움 많이 됐습니다!!
    포스팅된 글 모두 정독중입니다 ~ ㅎㅎ
댓글 입력 영역