Spring MVC

springmvc.egloos.com


포토로그


2012/03/02 16:54

4장 JdbcDaoImpl의 커스터마이징. Spring Security

JdbcDaoImpl을 이용한 토큰 생성

이제 본격적으로 DB를 이용한 로그인 서비스를 완성 해보도록 하겠습니다. 설명드리기 이전에 제가 저번 장에서 JdbcDaoImpl을 MyBatisDaoImpl로 대체하겠다고 말씀드렸었는데 이게 직접 시도해본 결과 몇가지 문제점이 있다는 것을 알게 되었습니다. 정확히 말하자면 완전히 불가능한 사항은 아니지만 User 클래스(UserDetails 인터페이스를 구현한…)가 내부 프로퍼티들의 get,set 메서드를 가지고 있지 않고 오로지 생성자만을 통해 해당 클래스 생성이 가능한데다가, 생성자만을 통해 User 클래스를 만들 수 있게 한 이유가 보안 때문인지 아니면 단순히 설계 때문인지 명확하지 않기 때문입니다.

그러므로 이 문제가 확실해지기 전까지 이번 장에 JdbcDaoImpl을 MyBatisDaoImpl로 확장하지 않고 JdbcDaoImpl의 기능을 조금 수정한 CrcJdbcDaoImpl 클래스를 만들어 문제를 해결해 보도록 하겠습니다.

근데 곰곰히 생각해보니 제가 JdbcDaoImpl이 무엇인지도 제대로 설명하지 않고 무작정 횡설수설한 것 같네요. 우선 JdbcDaoImpl을 확장하기 전에 이 클래스가 무슨 역할을 하는지부터 설명드리도록 하겠습니다.
JdbcDaoImpl의 위치를 그림으로 설명해보자면 위의 이미지와 같습니다. JdbcDaoImpl은 UserDetailsService 인터페이스를 구현한 클래스이며 데이터베이스에서 정보를 가져와 스프링 시큐리티에서 사용할 수 있게끔 UserDetails 형태로 가공해 줍니다. 그리고 스프링 시큐리티에서 제공하는 또 하나의 UserDetailsService, JdbcUserDatailsManager는 JdbcDaoImpl을 확장한 클래스이며 기본적인 CRUD(Create · Read · Update · Delete) 작업과 그룹설정을 추가한 클래스입니다. 그러나 JdbcUserDetailsManager는 쿼리문을 클래스 내부에 삽입해둔데다 쿼리문을 컨텍스트 파일에 직접 설정해줘야 하는 불편함이 있습니다.

단순히 스프링 시큐리티에서 제공하는 기본 기능에 만족하는 사용자라면 JdbcUserDatailsManager를 이용해도 상관없지만 궁극적으로 스프링 시큐리티를 자신의 서비스에 최적화 되게끔 튜닝해서 사용하고 싶다면 이 클래스를 사용하는 대신 직접 JdbcDaoImpl을 확장해 사용하시는 것이 좋습니다. 더욱이 우리가 MyBatis를 통해서 쿼리문과 서비스의 완전한 분리시키길 원했던 것처럼 완전히 독립적인 코드의 사용을 위해서라도 JdbcUserDatailsManager 클래스를 이용하는 것은 자제합시다.

이제 본격적으로 JdbcDaoImpl을 튜닝할 차례입니다. 필자는 이 클래스를 MyBatis로 변환하려고 한차례 클래스를 완전히 뜯어 고쳤었는데 스프링 시큐리티에서 JdbcTemplate를 사용하는 것이 영 마음에 들지 않았었거든요. 근데 뜯어고치는 와중에 위에도 말했다시피 MyBatis로 ORM을 교체하려면 User 클래스의 생성자 주입을 get, set 방식으로 새로 구성해야 한다는 사실을 깨닫고 User 클래스까지 새로 구현해볼 작정이었습니다.

근데 User 클래스를 자세히 들여다보니 이 클래스에는 set 메서드가 아예 존재하지 않았던 겁니다. 하나만 없으면 모르겠는데 사용자의 크리덴션을 생성하는 클래스가 프로퍼티의 set 메서드를 전부 없앴므로 이건 뭔가 이유가 있다는 거겠죠. 그래서 그동안의 작업을 전부 지우고 간단하게 Crc32를 이용해 아이디를 검색하는 CrcJdbcDaoImpl 클래스로 바뀌게 되었습니다.
public class CrcJdbcDaoImpl extends JdbcDaoImpl {
@Override
protected List<UserDetails> loadUsersByUsername(String username) {
String crcCode = String.valueOf(new Crc32().getCode(username));
return getJdbcTemplate().query(getUsersByUsernameQuery(), new String[] {crcCode, username}, new RowMapper<UserDetails>() {
public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
String username = rs.getString(1);
String password = rs.getString(2);
boolean enabled = rs.getBoolean(3);
return new User(username, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES); }
});
}
}

새로 만든 클래스는 JdbcDaoImpl을 상속하였으며 위의 메서드 하나만 오버라이딩 하면 됩니다. 간단히 위의 클래스를 설명하자면 원래 JdbcDaoImpl를 이용하면 varchar값을 인덱스로 설정할 수 밖에 없는 탓에 사용자가 늘어날수록 로그인 속도가 줄어드는 것을 줄이기 위해 Crc32로 암호화한 int값을 해시코드(crc_id)로 설정했었죠. 이 CrcJdbcDaoImpl 클래스는 앞으로 스프링 시큐리티가 로그인을 시도하는 이용자의 아이디를 검색할 때 아이디와 더불어 int값의 암호화 인덱스를 타게 해줍니다. 현재로써는 속도가 개선됬는지 잘 못느끼시겠지만 사용자가 늘어날수록 인트값 인덱스와 varchar값 인덱스의 속도차는 어마어마해집니다.

이제 JdbcDaoImpl을 작성하였다면 이걸 컨텍스트에 등록할 차례입니다.
<authentication-manager>
<authentication-provider user-service-ref="securityService" />
</authentication-manager>

<beans:bean id="securityService" class="com.billions.market.security.CrcJdbcDaoImpl">
<beans:property name="dataSource" ref="dataSource" />
<beans:property name="usersByUsernameQuery">
<beans:value>
SELECT principle, password, enabled
FROM users
WHERE crc_id = ? AND id = ?
</beans:value>
</beans:property>
<beans:property name="authoritiesByUsernameQuery">
<beans:value>
SELECT principle, authority
FROM authorities
WHERE principle = ?
</beans:value>
</beans:property>
</beans:bean>

위의 컨텍스트 예제를 여러분의 설정에 맞춰서 바꾸시면 됩니다. 이 설정을 보시면 쿼리문이 직접 컨텍스트에 등록되있는 것을 알 수 있는데 사실 제가 이런 것을 너무 싫어해서 MyBatis로 바꾸려다 보안으로 인해 딱 여기까지만 한수접은 부분입니다. 보안을 위해서였다 생각하시고 만약에 여기에 쿼리문을 등록하시기 싫으시다면 CrcJdbcDaoImpl 클래스를 더욱 확장해서 아예 쿼리문을 CrcJdbcDaoImpl 안에 박어 넣는 방법도 있습니다. 그러나 저는 혹시나 추후에 쿼리문을 수정할 일이 생길지도 모를 것 같아 이렇게 컨텍스트에서 직접 작성하는 방식을 택하였습니다.

컨텍스트에 등록한 프로퍼티를 하나씩 설명하자면 usersByUsernameQuery는 Username 즉, 로그인시 아이디 값을 통해 User 객체를 생성하는 쿼리문을 말합니다. 위에 제가 만든 클래스가 바로 이 쿼리문을 이용하게 될거라면 이해하시기 편할겁니다. 원래는 검색조건이 하나 밖에 안됬었는데 살짝 수정해서 2개가 들어갈 수 있도록 바꾼 것 뿐입니다 ^^;

그리고 authoritiesByUsernameQuery는 사용자의 권한을 가져오는 쿼리를 일컫습니다. 위의 쿼리문을 자세히 관찰하신다면 얻을 수 있는 결론이지만 우리가 이런 권한을 가져올 때 username 대신에 principle을 조건문으로 사용할 수 있는 까닭은 usersByUsernameQuery에서 쿼리문을 통해 principle 값을 가져왔기 때문입니다.

SELECT principle, password, enabled FROM users WHERE crc_id = ? AND id = ?

return new User(username(principle이 들어감), password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES);

여기서 위의 쿼리문의 순서는 매우 중요합니다. 왜냐하면 바로 아래 User 객체를 만드는 생성자에 쿼리문에서 불러들인 순서대로 값이 입력되기 때문이죠. 만약 쿼리문의 순서가 뒤죽박죽이 된다면 값이 제대로 안들어간다거나 오류가 발생할 수 있습니다.

이제 스프링 시큐리티의 설정이 모두 끝났으므로 Authorities 테이블과 매핑되는 Authority 자바빈 객체를 만들고 컨트롤 할 수 있도록 MyBatis를 활용해 서비스 계층과 DAO 계층 클래스을 만듭니다. 그리고 DB에 users테이블과 authorities 테이블에 각각 로그인 정보와 권한 설정을 해준 뒤 테스트를 해보시면 잘 작동할 것입니다.

지금 스프링 시큐리티에 대해 작성했던 글들을 다시 한번 훑어보고 있는데 부족한 점이 너무 많네요. 나름 스프링 시큐리티에 대해 자세히 설명해드리고자 시작했던 문서였지만 워낙 스프링 시큐리티가 확장이 필수적인 프레임워크인데다 새로 확장해야할 부분을 자세하게 설명하자면 연관있는 모든 클래스를 하나하나 나열해야 하는 탓에 생략된 부분이 너무 많습니다.

아직 많이 살펴보진 못했지만 지금까지의 결과로만 말씀드리자면 스프링 시큐리티는 굉장히 좋은 보안 프레임워크지만 제대로 이용하기 위해선 절대로 기본 세팅만으로 사용할 수는 없는 프레임워크인 듯 합니다. 스프링 시큐리티를 제대로 이용하고 싶으시다면 어떻게든 어거지로 짜맞춰 기본 기능만으로 이용하려하지 마시고 최대한 확장해서 자신의 기능에 맞춰서 사용할 수 있도록 클래스들의 원본을 찬찬히 뜯어보세요.

핑백

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

    ... 4장 JdbcDaoImpl의 커스터마이징.</a>  JdbcDaoImpl을 이용한 토큰 생성 이제 본격적으로 DB를 이용한 로그인 서비스를 완성 해보도록 하겠습니다. 설명드리기 이전에 제가 저번 장에서 JdbcDaoImpl을 MyBatisDaoImpl로 대체하겠다고 말씀드렸었는데 이게 직접 시도해본 결과 몇가지 문제점이 있다는 것을 알게 되었습니다. 정확히 말하자면 완전히 불가능한 사항은 아니지만 User 클래스(UserDet ... more

덧글

  • 理屈 2012/04/06 10:05 # 답글

    좋은글 감사합니다. 자주오게 되네요.
    spring security 부분은 토비의 스프링3에서 못보던 부분인데
    참고한 서적이나 사이트에 대해 알려주실수 있으신지요?
    계속적으로 좋은글 포스팅해 주세요.
  • 거짓말 2012/04/06 17:53 # 답글

    현재로서는 스프링 시큐리티3가 국내에서 참고할 수 있는 서적 중 유일합니다. ^^;
  • 理屈 2012/04/13 16:23 # 답글

    답변감사합니다~ 마침 회사에서 구입했네요. 열공
  • dd 2015/07/04 09:05 # 삭제 답글

    혹시 예제 샘플 같은거는 받을수있을까여 ?? 블로그그대로했는데 제가 정한 쿼리문이 안나오고 스프링 시큐리티의 쿼리문이 나오네염 ㅠㅠ;
댓글 입력 영역