Spring Security with Database

김재현·2022년 9월 8일
0

Programmers

목록 보기
23/28
post-custom-banner

Spring Security DB랑 같이 쓰기

  • 앞에서 배운 Spring Security 내용을 곧바로 실제 서비스에 적용할 수 있을까?
    사용자 로그인 정보를 Java Configuration을 통해 관리한다는 것은 현실적이지 않으며, 따라서 사용자 로그인 정보를 데이터베이스에서 관리할 수 있도록 변경해야한다.
  • JDBC 연동을 통해 사용자 로그인 정보를 MySQL 같은 RDBMS에서 관리할 수 있도록 변경해보자.

H2 DB 사용하기

  • H2 : 별도의 설치 필요 없이 로컬 개발용으로 빠르게 사용하기 좋은 경량 RDBMS 엔진
  • H2로 테스트된 코드는 간단한 설정변경을 통해서 MySQL과 같은 실제 서비스를 위한 데이터 베이스로 연동 가능.
  • 실제 현업에서는 이런 특징들을 이용, 로컬·개발·운영 환경 설정을 각각 분리하고 로컬·개발환경에서는 H2를 이용해서 테스트를 수행하고, 운영환경에서는 서비스용 데이터베이스를 사용하도록 구성하는 방식을 사용하기도 함.
  • pom.xml에서 의존성 추가.
    • 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
  • Spring Security 설정
@Override
public void configure(WebSecurity web) {
  web.ignoring().antMatchers("/assets/**", "/h2-console/**");
}
  • Spring Security 설정에서 웹 시큐리티를 이용, 스프링 스큐리티가 위에 설정한 경로에 대해서는 무시하도록 하는 설정을 추가해주어야 한다.
    h2 콘솔 경로를 추가해주지 않는다면, CSRF 필터같은 것이 콘솔페이지가 막혀서 열 수 없게된다.

H2를 데이터베이스로 사용할 수 있도록 DataSoruce 설정 추가

  • 라이브러리 의존성을 pom.xml에서 추가해준다.
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
  • 가장 기본이 되는 spring boot starter jdbc 추가.
<dependency>
  <groupId>com.zaxxer</groupId>
  <artifactId>HikariCP</artifactId>
  <version>4.0.3</version>
</dependency>
  • 데이터베이스 커넥션 풀링을 위해서, 가장 많이 사용하는 HikariCP라는 커넥션 풀 추가.
<dependency>
  <groupId>org.lazyluke</groupId>
  <artifactId>log4jdbc-remix</artifactId>
  <version>0.2.7</version>
</dependency>
  • log4jdbc 라이브러리는 실행되는 쿼리를 로그파일로 남기기 위한 라이브러리.
  • yml파일에서 DataSoruce를 추가해주어야 한다.
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
  • spring_security 데이터 베이스 이름
    MODE=MYSQL MySQL모드
    DB_CLOSE_DELAY 어플리케이션이 종료될때까지 h2를 클로즈하지 않음.
  • h2:mem h2데이터베이스를 인메모리 데이터베이스로 사용하겠다는 의미.
    프로젝트를 시작 할때마다 데이터베이스가 초기화됨. 하지만 데이터베이스가 초기화 될 때마다 데이터를 수동으로 입력하는 것은 매우 번거로운 작업이다.
    때문에 더미 데이터 입력용 sql 파일을 준비하는 것이 좋다.
  • 프로젝트가 시작되고 초기화될 때 미리 만들어놓은 sql파일을 이용할 수 있도록 설정할 수 있다.
spring:
	sql:
    init:
      platform: h2
      schema-locations: classpath:sql/schema.sql
      data-locations: classpath:sql/data.sql
      encoding: UTF-8
  • H2 데이터베이스는 어플리케이션을 재시작이 매번 초기화되고, 어플리케이션 시작과 함께 데이터베이스에 테이블을 생성하고, 필요한 데이터를 입력해야 함

log4jdbc 라이브러리 사용

  • 필수적이지는 않다. 스킵하더라도 로그인 같은 기능은 정상적으로 동작할 것.
  • jdbc 드라이버를 통해 실행되는 쿼리와 그 쿼리를 통해서 만들어지는 ResultSet 등을 로그로 남겨 쿼리가 의도대로 잘 실행되었는지 편리하게 확인하기 위한 용도
  • JPA를 사용해서 쿼리 로깅을 간단한 설정을 통해 활성화시킬 수 있지만, log4jdbc 라이브러리를 사용해서 쿼리 로깅을 남길 수도 있다.

log4jdbc-remix 적용

  • 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;
    }
  }
}
DataSource 객체를 Log4jdbcProxyDataSource 타입으로 Wrapping 처리하기 위한 BeanPostProcessor 
  • 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"/>
logback.xml — log4jdbc 선택적 로깅 설정

데이터베이스 기반 인증 처리

  • 실제 기능을 구현하기 위해서 작성해야하는 코드의 양은 많지 않지만, 스프링 시큐리티에서 인증처리 절차가 어떻게 동작하는지 잘 알고 있어야한다.
  • 스프링 시큐리티의 인증처리
    • AuthenticationManager는 사용자의 인증 처치를 위한 작업을 AuthenticationProvider로 위임함
    • UsernamePasswordAuthenticationToken 타입의 인증 요청은 DaoAuthenticationProvider가 처리함

  • DaoAuthenticationProvider
    • 데이터베이스에서 사용자 인증 정보를 조회하는 작업을 UserDetailsService 인터페이스 구현체에 위임
    • 지금까지 우리는 UserDetailsService 인터페이스 구현체 중 InMemoryUserDetailsManager 클래스를 사용했기 때문에, UserDetailsService 인터페이스 구현체 중 JDBC를 지원하는 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl 구현체를 사용하면됨
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 = ?  -- 사용자의 권한 조회
JdbcDaoImpl 클래스에서 사용자 및 권한 조회를 위해 사용되는 기본 Query
  • schema.sql 에서 제공하는 기본 테이블 구조가 JdbcDaoImpl 클래스에서 사용하는 쿼리에 적합하게 된것은 우연의 일치가 아님 (의도적으로 그렇게 만듬)
  • 마지막으로 JdbcDaoImpl 객체를 Bean으로 등록하면 데이터베이스 기반 인증 처리가 완료된다.
    • jdbcAuthentication 메소드는 UserDetailsService 인터페이스 구현체로 JdbcUserDetailsManager 객체를 등록함
    • JdbcUserDetailsManager 클래스는 JdbcDaoImpl 클래스를 상속하며, 보다 풍부한 기능을 제공함
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth.jdbcAuthentication()
    .dataSource(dataSource);
}

inMemoryAuthentication 메소드는 UserDetailsService 인터페이스 구현체로 InMemoryUserDetailsManager 객체를 등록했었다.

데이터베이스 기반 인증 고급 설정

  • 대부분의 경우 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 컬럼 순서이어야함
  • authoritiesByUsernameQuery 기본 SQL 쿼리
    select username, password, enabled from users where username = ?
  • authoritiesByUsernameQuery — 사용자에게 직접 부여된 하나 이상의 권한을 반환 (Group-based Access Control 미적용시)
    • 조회하는 두 번째 값은 반드시 authority: String 컬럼이어야 함
  • authoritiesByUsernameQuery 기본 SQL 쿼리
    select username, authority from authorities where username = ?
  • groupAuthoritiesByUsernameQuery — 그룹 멤버십을 통해 사용자에게 승인된 권한을 반환 (Group-based Access Control 적용시)
    • 조회하는 세 번째 값은 반드시 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

JdbcUserDetailsManager 설정

  • JdbcDaoImpl 객체를 Bean으로 등록
    • 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)
  ;
}
post-custom-banner

0개의 댓글