앞선 spring security에서는 memory 기반에 사용자 정보를 추가하고 동작시켜보았다. 하지만 일반 실무에서 그럴리는 거의 없을 것이다. 이번 포스트에서는 JDBC 기반으로 사용자를 관리하는 방법에 대해 알아보자.
AuthenticationManager
AuthenticationManager와 DaoAuthenticationManager
인증과 관련된 처리는 AuthenticationManager 인터페이스가 담당하는데 그 구현체는 ProviderManager이다. 말 그대로 인증 방식을 제공하는 Provider들(AuthenticationProvider)을 관리하는 객체이다. Provider는 상황에 따라 여러가지 형태가 가능하므로 ProviderManager는 AuthenticationProvider를 List 형태로 관리한다. AuthenticationProvider interface는 기본 구현체는 DaoAuthenticationProvider이다.
ProviderManager는 관리하고 있던 Provider 중 하나인 DaoAuthenticationProvider에 token을 전달한다.
DaoAuthenticationProvider는 UserDetailsService를 이용해 token의 username에 해당하는 사용자를 찾아본다.
사용자를 찾았다면 PasswordEncoder를 통해서 비밀번호가 valid 한지 검사한다.
인증에 성공하면 최종적으로 token에 UserDetails와 Authorities 객체가 담겨서 반환된다.
DaoAuthenticationProvider는 UserDetailsService를 갖는데UserDetailsService는 UserDetails라는 객체를 이용해서 인증 정보를 처리한다. UserDetailsService의 구현체로는 메모리상에서 인증 정보를 관리하는 InMemoryUserDetailsManager,데이터베이스에서 인증 정보를 관리하는 JdbcUserDetailsManager가 있다.
DaoAuthenticationProvider 연관 객체
JdbcUserDetailsManager
UserDetailsService를 구현하고 있는 JdbcDaoImpl은 JDBC를 이용해서 username-password 기반의 인증을 처리할 때 사용된다. JdbcUserDetailsManager는 다시 JdbcDaoImpl을 상속받고 있다.
JdbcUserDetailsManager는 Jdbc 기반으로 사용자 인증을 처리한다.
이제 기존의 InMemoryUserDetailsManager 대신 JdbcUserDetailsManager로 교체해주면 이제 JDBC 기반으로 사용자 인증이 이뤄지는데 JdbcUserDetailsManager는 DataSource 타입의 빈이 추가로 필요하다.
참고로 default schema는 org/springframework/security/core/userdetails/jdbc/users.ddl에서 확인할 수 있다. 물론 테이블을 만들어주는 것은 아니다.
이 테이블에 조회하는 쿼리는 JdbcUserDetailsManager#setUsersByUsernameQuery 등으로 설정할 수 있다.
JdbcUserDetailsManagermanager=newJdbcUserDetailsManager(ds);
manager.setUsersByUsernameQuery("select username, password, enabled from users where username = ?");
manager.setAuthoritiesByUsernameQuery("select username, authority from authorities where username = ?");
따라서 필요하다면 테이블도, 쿼리도 사용자 정의로 지정해서 변경할 수 있지만 Spring Security의 기본 구조에 따라 코드가 흘러갈 수 있도록 처리하는 것이 향후 Spring Security 업그레이드 시 호환성 유지나 유지 보수 측면에서 유리할 것 같다. 여기서는 위 테이블 구조에 맞춰서 Entity를 구성해보자.
Entity 만들기
이번 예제에서는 JPA의 create ddl 옵션을 create로 사용할 계획이므로 아래와 같이 entity만 만들어주면 된다. 관계는 User와 Authority를 1:N의 단방향으로 만들어주자.
ROLE 기반의 권한 관리를 위해 생성자에서 authority를 설정할 때 ROLE_를 추가해주고 있다.
로그인 동작 확인
이제 in-memory 방식에서 jdbc 기반으로 변경해봤으니 기존의 로그인 동작이 잘 동작하는지 확인해보자. 기존의 SecurityWebTest는 단순히 @WebMvcTest를 해서 DataSource 등 빈이 로딩되지 않는다. 따라서 다음과 같이 에너테이션을 교체해서 테스트 해보자.
@SpringBootTest@AutoConfigureMockMvc
기존의 단위테스트는 물론 실제 웹 브라우저를 이용해서도 생각대로 잘 동작해야 한다.
테스트 수정
JPA의 개입에 따른 UserDetailsService 모킹
기존의 단위테스트는 @ExtendWith(SpringExtension.class)를 이용해서 처리했는데 이는 JPA 관련 빈들을 로딩하지 못한다. 따라서 JPA를 사용해야 하는 UserDetailsService#loadUserByUsername을 재정의 해주어야 한다. 다음과 같이 추상 클래스를 말들고 UserDetailsService에 대한 mock을 생성해보자.
그런데 H2-Console을 이용해서 실제 DB를 확인하려고 하면 약간의 문제가 발생한다.H2-Console을 웹에서 접근하다보니 security가 호출을 허용하지 않기 때문이다. 크게 h2-console을 사용하지 못하는 이유는 3가지 정도로 요약해볼 수 있다.(처리하지는 않을 계획이므로 언급만 하고 가자.)
권한 필요: /h2-console 경로에 대해 permitAll()의 적용이 필요하다.
CSRF 제외: CSRF 공격을 방어하기 위해서 csrf_token을 전달해야 하는데 이것이 불가하므로 예외가 필요하다.
X-Frame-Options 설정: h2-console은 iframe을 사용하는데 이 역시 보안상 좋지 않다. 따라서 X-Frame-Options헤더에 DENY, SAMEORIGIN 등의 설정이 필요하다.
위의 번거로움을 간단히 처리해줄 수 있는 방법으로WebSecurityCustomizer를 사용할 수 있다.
WebSecurityCustomizer
WebSecurityCustomizer는 특정 리소스나 경로에 대해 보안 설정을 무시하고 싶을 때 사용할 수 있는 빈으로 주로 CSS, JavaScript, 이미지 등에 대한 보안 설정을 제외할 때 사용한다. 여기에 h2-console의 경로를 추가해주면 된다.