데이터베이스를 사용한 인증 적용
앞서 5장까지 Spring Security의 아키텍처와 주요 개념들 그리고 소소하게 몇 개의 필터들에 대해 살펴보았다. 이제 우리는 곧바로 Spring Security를 실제 서비스에 적용할 준비가 되어 있는 것일까?
안타깝게도 Spring Security를 실제 서비스에 적용하기 위해 넘어야할 산이 하나 더 있다. 그것은 바로 사용자 로그인 정보를 Java Configuration을 통해 관리 한다는 것은 현실적이지 않다는 것이다. 따라서 사용자 로그인 정보를 데이터베이스에서 관리할수 있도록 변경해야 한다.
JDBC 연동을 통해 사용자 로그인 정보를 MySQL 같은 RDBMS에서 관리할 수 있도록 변경해 보자.
H2 Database 사용하기
- H2 — 별도의 설치 필요없이 로컬 개발용으로 빠르게 사용하기 좋은 경량 RDBMS 엔진
- In-Memory 모드로 사용할 경우 어플리케이션을 재시작 할때 마다 초기화됨
- Compile 시 의존성이 필요한것은 아니므로 scope을 runetime으로 지정
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> <scope>runtime</scope> </dependency>
- h2-console 설정
- 브라우저에서 h2-console 화면을 통해 기본적인 데이터베이스 관리 및 쿼리 실행을 할 수 있음
- application.yml 설정 — path 부분에 입력한 경로로 브라우저에서 접근 가능
spring: h2: console: enabled: true path: /h2-console
@Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/assets/**", "/h2-console/**"); }
H2를 데이터베이스로 사용할 수 있도록 DataSoruce 설정 추가
- 관련 의존성 추가
- spring-boot-starter-jdbc — spring-jdbc
- HikariCP — 데이터베이스 커넥션 풀
- log4jdbc-remix — JDBC 쿼리 및 ResultSet 로깅
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency> <dependency> <groupId>org.lazyluke</groupId> <artifactId>log4jdbc-remix</artifactId> <version>0.2.7</version> </dependency>
- HikariCP 기반 DataSource 설정
spring: datasource: driver-class-name: org.h2.Driver url: "jdbc:h2:mem:spring_security;MODE=MYSQL;DB_CLOSE_DELAY=-1" username: sa password: hikari: minimum-idle: 1 maximum-pool-size: 5 pool-name: H2_DB
H2 데이터베이스 초기화
- H2 데이터베이스는 어플리케이션을 재시작이 매번 초기화됨
- 따라서, 어플리케이션 시작과 함께 데이터베이스에 테이블을 생성하고, 필요한 데이터를 입력해야 함
spring: sql: init: platform: h2 schema-locations: classpath:sql/schema.sql data-locations: classpath:sql/data.sql encoding: UTF-8

log4jdbc-remix 적용
- 실행되는 SQL 및 ResultSet을 로깅
- BeanPostProcessor 인터페이스를 구현하여, DataSource 객체를 Log4jdbcProxyDataSource 타입으로 Wrapping 처리
@Component public class DataSourcePostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof DataSource && !(bean instanceof Log4jdbcProxyDataSource)) { return new Log4jdbcProxyDataSource((DataSource) bean); } else { return bean; } } }
- logback 설정을 통해 선택적으로 로깅 처리
<logger name="jdbc.sqltiming" level="INFO"/> <logger name="jdbc.audit" level="OFF"/> <logger name="jdbc.resultset" level="OFF"/> <logger name="jdbc.resultsettable" level="INFO"/> <logger name="jdbc.connection" level="OFF"/> <logger name="jdbc.sqlonly" level="OFF"/>
데이터베이스 기반 인증 처리
- 3장에서의 인증 처리 내용을
조금복습해보면 - AuthenticationManager는 사용자의 인증 처치를 위한 작업을 AuthenticationProvider로 위임함
- UsernamePasswordAuthenticationToken 타입의 인증 요청은 DaoAuthenticationProvider가 처리함

- DaoAuthenticationProvider
- 데이터베이스에서 사용자 인증 정보를 조회하는 작업을 UserDetailsService 인터페이스 구현체에 위임
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
- JdbcDaoImpl
- 이름 그대로 JDBC를 통해 데이터베이스에서 사용자 인증 정보를 가져옴
select username, password, enabled from users where username = ? -- 사용자 조회 select username, authority from authorities where username = ? -- 사용자의 권한 조회
- jdbcAuthentication 메소드는 UserDetailsService 인터페이스 구현체로 JdbcUserDetailsManager 객체를 등록함
- JdbcUserDetailsManager 클래스는 JdbcDaoImpl 클래스를 상속하며, 보다 풍부한 기능을 제공함
inMemoryAuthentication 메소드는 UserDetailsService 인터페이스 구현체로 InMemoryUserDetailsManager 객체를 등록했었다.
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource) ; }
데이터베이스 기반 인증 고급 설정
- 즉, 대부분의 경우 UserDetailsService의 커스텀 구현체를 만들지 않더라도 JdbcDaoImpl 클래스를 설정하여 대응할 수 있음
- JdbcDaoImpl 클래스는 기존 데이터베이스 스키마에 적용하거나 기존 기능을 더욱 정교하게 설정할 수 있도록 다양한 옵션을 제공함
- Group-based Access Control — 사용자와 권한 사이에 그룹이라는 간접 계층을 둘 수 있음
- 사용자는 특정 그룹에 속하게 되고, 그룹은 권한 집합을 참조함
- 즉, 사용자를 특정 그룹에 속하게 함으로써, 그룹에 속한 권한을 일괄 적용할 수 있음
sql 디렉토리 아래에 schema_new.sql, data_new.sql 쿼리 파일은 Group-based Access Control 기능을 사용할 있도록 데이터베이스에 테이블을 생성하고, 필요한 데이터를 입력한다.
sql 디렉토리 아래에 data_new.sql 쿼리 파일에는 두 개의 사용자 계정 (user, admin) 더미 데이터가 정의되어 있는데, 비밀번호가 BCrypt Hash로 암호화되어 있다. 암호화 되기 이전 비밀번호는 user123, admin123 이다.

- JdbcDaoImpl 클래스는 수행 목적에 따라 3개의 SQL 쿼리를 정의하고 있는데 이를 위 테이블 구조에 맞게 재정의하여 활용해야 함
- usersByUsernameQuery — 사용자명과 일치하는 하나 이상의 사용자를 조회
- 조회하는 값들은 반드시 username: String, password: String, enabled: Boolean 컬럼 순서이어야함
select username, password, enabled from users where username = ?
- 조회하는 두 번째 값은 반드시 authority: String 컬럼이어야 함
select username, authority from authorities where username = ?
- 조회하는 세 번째 값은 반드시 authority: String 컬럼이어야 함
select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id
- usersByUsernameQuery, groupAuthoritiesByUsernameQuery SQL 쿼리 재정의
- enableGroups — Group-based Access Control 활용시 true 입력
- groupAuthoritiesByUsername 쿼리 정의시 자동으로 true 설정됨
- enableAuthorities — Group-based Access Control 활용시 false 입력
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource) .usersByUsernameQuery( "SELECT " + "login_id, passwd, true " + "FROM " + "USERS " + "WHERE " + "login_id = ?" ) .groupAuthoritiesByUsername( "SELECT " + "u.login_id, g.name, p.name " + "FROM " + "users u JOIN groups g ON u.group_id = g.id " + "LEFT JOIN group_permission gp ON g.id = gp.group_id " + "JOIN permissions p ON p.id = gp.permission_id " + "WHERE " + "u.login_id = ?" ) .getUserDetailsService().setEnableAuthorities(false) ; }
미션
데이터베이스 기반 인증 고급 설정 부분(Group-based Access Control 적용)을 JPA로 구현해보기 (com.prgrms.devcourse.user 패키지 아래에 생성)
- 구현 클래스 목록
- Entity 클래스
- User 클래스 — users 테이블
- Group 클래스 — groups 테이블
- Permission 클래스 — permissions 테이블
- GroupPermission 클래스 — group_permission 테이블 (다대다 매핑을 위한 교차 테이블)
- JPA 기반 UserRepository
- UserService 클래스 — UserDetailsService 인터페이스 구현체 (JdbcDaoImpl를 대체해야함)
- 단, schema_new.sql, data_new.sql 쿼리 파일은 그대로 사용함