[Spring Security] DB에 유저 정보 저장하기

minseok Kim·2025년 2월 24일

Spring Security

목록 보기
11/15

이전까지는 웹 서버 내부에 유저 정보를 저장했었는데, 실제 프로덕트 환경에서는 유저 정보를 외부 DB에 저장하는 경우가 많다.

이렇게 유저 정보를 DB에 저장하기 위해 Spring Security는 JdbcUserDetailsManager를 제공한다.



JdbcUserDetailManager는 UserDetailService의 구현체로, 이를 bean으로 등록하여 유저정보를 DB에 저장할 수 있다.

datasource는 DB에 대한 정보로, application.yml에 DB 경로를 설정해놓으면 해당 DB 경로가 datasource로 자동 주입된다.
JdbcUserDetailsManager의 생성자로 이 datasource를 주입하면, 해당 DB에 유저 정보를 저장하고 인증할 수 있다.

다만, JdbcUserDetailsManager를 사용하려면 JdbcUserDetailsManager에서 제공하는 테이블 및 컬럼만을 사용해야 한다.
Users, Authorities 테이블과 username, password, enabled, authoirity 컬럼 등 미리 정해둔 내용만 사용이 가능한데, 해당 컬럼 이외의 것을 사용하고 싶거나 사용자를 이메일을 통해 로드하는 경우 등에서는 개발자가 직접 정의한 테이블을 사용하고 싶을 때가 있을 것이다.
이런 경우엔 JdbcUserDetailManager 대신 직접 UserDetailsService를 구현하여 사용자 커스텀 된 테이블 및 컬럼을 사용한다.



유저 정보를 저장하는 Customer이라는 테이블을 작성했다. 해당 테이블은 id, email, pwd, role이라는 컬럼을 가지고 있다.



이후 UserDetailsService를 implement하여 직접 구현하였다. UserDetailsService는 loadUserByUsername() 메서드만 있으므로 해당 메서드를 구현해야 한다.

유저를 load할 때 email을 기반으로 사용자를 load하였고, 리턴 값이 UserDetails이므로, UserDetails의 구현체인 User를 반환하였다.
User를 생성할 때 username, password, authorities가 필요하다.
authorities는 GrantedAuthority형이므로, customer.getRole()을 GrantedAuthority형으로 형변환을 하였다.



다음은 테스트를 위한 controller를 만들었다.

편의를 위해 DTO가 아닌 테이블 자체를 입력으로 받게 하였고, passwordEncoder를 통해 사용자가 입력한 패스워드를 해쉬하여 DB에 저장하였다.
사용자가 제대로 저장되었다면 savedCustomer.getId() > 0가 참이 되어 HTTP 201를 반환할것이고, 그렇지 않다면 400을 반환할 것이다.


Sequence Flow

UserDetailsService를 직접 구현했을 때 어떤 과정을 통해 인증이 진행되는지 Sequence Flow를 보고 정리해보았다.

1) 유저가 보호된 페이지에 접근


2) AuthorizationFilter, AbstractAuthenticationProcessingFilter, DefaultLoginPageGeneratingFilter와 같은 Filter들이 유저가 로그인 되지 않았음을 확인하고 로그인 페이지로 redirect


3) 유저가 아이디/비밀번호를 입력


4) UsernamePasswordAuthenticationFilter와 같은 Filter들이 username, password를 추출하고 이를 Authentication의 구현체인 UsernamePasswordAuthenticationToken으로 변환한다.
이후 AuthenticaitonManager의 구현체인 ProviderManager의 authenticate() 메서드를 호출한다.

5) ProviderManager는 가능한 모든 Authentication provider의 authenticate() 메서드를 호출한다. AuthenticationProvider를 따로 구현하지않았으므로, Default로 DaoAuthenticationProvider가 선택될 것이다.


6) DaoAuthenticationProvider는 내가 정의한 UserDetailsService의 loadUserByUsername()을 호출한다. UserDetails이 load되면, passwordEncoder를 통해 비밀번호가 일치하는지 확인할 것이고, 해당 유저가 인증되었는지 여부를 파악한다.


7) 최종적으로 AuthenticationProvider는 AuthenticationManager에게 인증이 성공적으로 수행되었는지 여부를 가지는 Authentication 객체를 반환한다.


8) ProviderManager가 인증이 성공적으로 수행되었는지를 파악한다. 인증이 실패했다면, 다른 AuthenticationProvider를 시도해 볼 것이고, 성공했다면 Filter에게 Authentication을 리턴한다.


9) 특정 Filter가 Authentication 객체를 추후 이용을 위해 SecurityContext에 저장한다.



아래는 서비스 단위로 표현한 Sequence Flow이다.

UserDetailsService의 반환값이 UserDetails이므로, AuthenticationProvider에서 UserDetails을 Authentication 객체로 변환하는 과정이 필요하다.


이번 포스트에서는 UserDetailsService를 직접 커스텀하여 구현했는데, UserDetailsManager는 따로 구현하지 않아도 되나? 라는 의문점이 들었다.
이에 관하여 찾아보니, 결론적으로 UserDetailsManager은 따로 구현하지 않는 경우가 많은데 UserDetailsManager가 제공하는 유저의 CRUD 기능은 실제 프로덕트에서 REST API 형태로 사용하는 경우가 많아 Spring에서 제공하는 UserDetailsManager를 사용하지 않고 REST API를 직접 만들어 CRUD를 진행한다고 한다.

0개의 댓글