06. 스프링시큐리티 설정

스프링시큐리티는 인증/인가 담당하는 모듈 하나에 불과하지만 이것을 적용하기 위해서는 손대야할 것이 많다. 여기서는 웹보안에 대한 것만 간단하게 다룬다.  아래의 소스는 스프링시큐리티를 적용하기 위해 작성한 설정 클래스이다. 


1. SpringSecurityConfig 설정

30번 라인의 @EnableWebSecurity은 웹보안을 활성화 시키는 역하을 하고 31번 라인의 WebSecurityConfigurerAdapter은  웹보안을 위한 메소드를 확장시키고 구현을 재정의할 수 있도록 한다.
44번 라인의 configure(WebSecurity web)은 스프링시큐리티의 보안 설정에 적용받지 않을 URL을 등록한다. 주로 이미지 파일이나, CSS, JavaScript 라이브러리가 대상이다. 
49번 라인의 configure(HttpSecurity http)에서는 본격적인 보안 사항을 설정한다. http.formLogin() 에서는 로그인과 관련된 사항을 설정한다. loginPage("/login_form.html").permitAll()은 로그인 페이지가 어떤 URL 인지 지정하고, 로그인을 처리할 loginProcessingUrl("/auth") URL이 무엇인지 지정한다. 본인은 /login_f
orm.html URL을 처리할 컨트롤러를 controller.common.CommonController에 설정했다. usernameParameter("loginid").passwordParameter("passwd")는 로그인에 사용할 파라메터를 지정하는 부분인다. 마지막으로 successHandler()와 failureHandler()는 성공시 처리할 모듈과 실패시 처리할 모듈을 설정했다.
http.logout()에서는 로그아웃과 관련된 사항을 설정한다. logoutRequestMatcher()는 어떤 URL을 입력했을 경우 로그아웃 처리를 수행할 것인지를 지정하는 부분이며, logoutSuccessHandler()는 로그아웃시 처리할 모듈을 설정하는 부분이다. clearAuthentication(true).invalidateHttpSession(true)는 로그 아웃시 인증정보를 지우하고 세션을 무효화 시킨다는 설정이다.
http.exceptionHandling()은 예외사항을 설정한다. 여기서는 권한이 없는 URL에 접근했을 때 동작을 설정하는 부분으로 accessDeniedHandler() 통해 처리할 모듈을 설정했다.
http.authorizeRequests()는 URL별 권한 사항을 설정한다. 이 부분은 userService.findAllComMenuMngtAndAuthGrpMngt() 통해 메뉴정보와 권한 그룹정보를 조회한 후 URL별 권한 정보를 만들어 설정한다. 스프링시큐리티는 ROLE_PREFIX가 'ROLE_'로 설정되어 있는데 이것을 그대로 사용하기 위해 권한 그룹 정보의 시퀀스 앞에 'ROLE_G'라는 문자열을 붙여 설정하였다.
http.sessionManagement() 세션 정책을 설정한다. maximumSessions()에 1을 넣어 같은 아이디로 1명만 접속하도록 설정하였다. maxSessionsPreventsLogin(false)는 기존에 접속한 사용자가 있을 경우 기존 사용자의 접속을 끊고, 나중에 접속한 사용자가 접속되도록 한다. expiredSessionStrategy()을 통해 세션이 종료되었을 경우의 처리를 할 수 있도록 하였다.

82번 라인의 configure(AuthenticationManagerBuilder auth) 메소드는 로그인 처리를 위한 구성을 수행한다. 자세한 내용은 아래서 설명할 것이다.




2. configure(AuthenticationManagerBuilder auth)

configure(AuthenticationManagerBuilder auth)에서는 인증 처리를 위한 AuthenticationProvider를 구현한 구현체와 UserDetailsService를 구현한 구현체를 조합한다. AuthenticationProvider의 구현체의 주요 역할은 사용자 정보를 조회하고, 로그인시 입력 받은 패스워드를 확인하고, 사용자의 권한을 확인하여 인증토큰을 발행하는 역할을 한다. UserDetailsService의 구현체는 사용자 정보를 조회하는 역할을 하며, UserDetails 구현체를 반환 한다.


여기서는 AuthenticationProvider의 구현체로 SecurityAuthenticationProvider를 만들어 사용한다. 아래는 SecurityAuthenticationProvider의 소스이다. 25번 라인과 같이 AuthenticationProvider를 implements 하면서 시작한다. 44번 라인에서는 사용자 정보를 조회한다. 여기서는 UserDetails 구현체를 받는데 본인 나름대로 편리하다고 생각되는 방향으로 수정해서 사용했다. 46번 라인은 패스워드를 비교하는 부분으로 현재의 passwordEncoder는 사실상 패스워드를 암호화 하거나 하지는 않는다. 이것을 암호화 모듈로 바꿔주려면 SpringSecurityConfig.java의 90번 라인의 NoOpPasswordEncoder.getInstance() 수정하면 된다. 51번 라인은 본인이 추가한 부분으로 사용자에게 지정된 IP에서 접속했는가를 확인하는 부분이다. 사용가능 IP 목록이 없으면 이 부분은 그냥 넘어가도록 했다. 사용가능 IP 목록은 관리자와 같은 특수한 사용자 위한 것으로 특정한 PC에서만 접속할 권한을 부여할 경우 사용한다. 68번 라인은 사용자가 사용할 수 있는 권한(ROLE)을 부여하는 부분으로 사용자 정보에서 가져왔다. 마지막으로 80번 라인에서는 스프링에서 사용할 수 있도록 사용자 정보와 권한 정보 등으로 Authentication 구현체를 반한다.



사용자 정보는 UserDetailsService를 구현한 SecurityUserDetailsService로 하였다. UserDetailsService의 필수 조건은 사용자의 권한 정보를 포함한 사용자 정보이다. 하지만 SecurityUserDetailsService는 메뉴 정보까지 포함하여 받을 수 있도록 수정했다.


20번 라인21번 라인은 UserDetailsService 기능을 수행할 수 있도록 @Service 추가와 UserDetailsService 인터페이스를 추가하였다. 26번 라인의 loadUserByUserName() 메소드는 AuthenticationProvider 구현체에서 호출하는 부분으로 반드시 구현되어야 하는 부분이다. 이곳에서 사용자 정보 조회와, 사용자의 권한 그룹 조회, 사용자에게 할당된 메뉴 정보를 조회한다. 이 부분은 28번 라인, 35번 라인, 38번 라인에서 구현되어 있으며, 특히 38번 라인의 사용자 메뉴 목록 조회 부분에서는 내부에서 메소드(56번 라인)를 구현하여 각 메뉴의 부모와 자식 관계를 연결하였다. 

이렇게 한 이유는 나중에 화면을 구현할 때 각 화면별 화면 정보 조회와 화면 경로 표현을 위해 미리 만들어 둔 것이다.

40번 라인은 SecurityUser Object를 생성하는 부분으로 우리의 필요 용도에 맞게 UserDetails를 수정하여 구현 하였다.


마지막은 UserDetails를 구현한 SecurityUser다. SecurityUsers는 Spring Security에 사용자 정보를 제공하는 용도와 더불어 개인화된 화면을 제공하기 위한 부가 정보로 구성했다.

SecurityUser는 UserDetails의 구현체이므로 29라인 처럼 UserDetails을 추가해야 하며, Serializable도 추가해야 한다. 41번 라인은 생성자로써 사용자 정보과 권한 정보, 메뉴 목록 등을 받아 인스턴스 내에 저장하는 역할을 한다. 특히 50번 라인에서는 최상위 메뉴를 topMenuMaps에서 저장하며, 54번 라인에서는 사용자에게 할당된 메뉴를 authMenuUrlMap라는 인스턴스 변수에 url + menu 정보로 저장하여 둔다. 71번 라인84번 라인은 메뉴 목록을 출력하는 용도와 화면 정보를 표시하는 용도로 사용된다. 자세한 것은 interceptor(Logging 파트)를 다룰 때 설명할 것이다. 89번 라인과 103번 라인, 112번 라인은 UserDetails 인터페이스가 요구하는 항목으로 각각 사용자의 권한 정보와 패스워드, 로그인 아이디를 반환한다.

117번 라인, 126번 라인, 131번 라인, 149번 라인은 UserDetails 인터페이스가 요구하는 구현 항목으로 각각 계정의 만료 여부, (일정 수의 로그인 처리 오류의 경우 등으로)계정 잠김, 패스워드 만료, 유효 계정 여부 등을 묻는 기능으로 원칙적으로는 구현하는 것이 올바르지만, 이런 기능들을 사용하지 않을 true를 반환해도 된다. 





3. 인증과 처리에 대한 각종 핸들러

SpringSecurityConfig 설정에는 총 6종의 핸들러가 선언되어 있다. 이중에 SecurityAuthenticationSuccessHandler만 설명하고 나머지는 Logging 파트에서 설명할 것이다. SecurityAuthenticationSuccessHandler의 역할은 로그인 처리가 완료되었을 경우 로그인한 사용자가 보게될 첫번째 화면으로 안내하는 것이다.

67번 라인의 onAuthenticationSuccess() 메소드에서 사용자에게 보여질 첫번째 화면을 지정해 준다. 그러나 우선적으로 처리해 주어야할 일이 있다. 70번 라인에 호출된 clearAuthenticationAttributes() 메소드는 로그인 시 발생했던 세션에 저장된 오류를 지워주는 역할을 수행한다. 그리고 72번 라인의 decideRedirectStrategy() 메소드를 통해 어떤 화면으로 이동할 것인지를 지정한다. 판단 기준은 targetUrlParameter 값이 있을 경우 그것을 1순위로 하여  리다이렉션하며, 1순위 URL이 없을 경우 Spring Security가 세션에 저장한 URL을 2순위로 한다. 2순위 URL이 없을 경우 Request의 REFERER을 3순위로 하여 그 REFERER URL로 이동하며,  3순위도 없을 경우 Default URL을 4순위로 한다. (SecurityAuthenticationSuccessHandler의 소스 내용은 [http://zgundam.tistory.com/52]를 차용하였다. 보다 상세한 내용도 이곳에서 확인할 수 있다.)



지금까지의 코드로는 결과를 확인하기는 좀 어렵다. 다음 포스트에서 Apache Tiles의 사용과 더불어 결과를 확인해 보도록 하겠다.


demo.zip

 

05. flyway 설정과 DAO 테스트

본인은 Dao에 대한 테스트 코드를 만드는 것에 부정적이다. Dao 레벨의 테스트는 의미를 찾기가 어렵기 때문이다. 어떤 기능을 한다는 것은 Dao 레벨에서 만들어지는 단순한 기능이 아니라 여러가지 단순한 기능들을 엮어 하나의 의미있는 동작을 만들어야 하기 때문이다. 따라서 테스트의 단위는 의미있는 기능이 만들어지는 서비스 레벨에서 부터 이루어져야 하지 않을까? 더구나 Spring Data가 만들어내는 자동화 코드를 테스트할 의미가 있을까 싶다. 그러나 여기서는 Dao를 테스트 하므로써 Spring Data가 만들어내는 코드와 QueryDSL이 실제 동작하는지 살펴보고, flyway가 어떻게 개발환경 구축에 도움을 주는지 살펴볼 것이다.



1. flyway 설정과 테스트 데이터 입력


flyway는 기초 데이터를 넣어주는 유틸리티라고 생각하면 된다. 프로그램의 구동이 시작될 때 주어진 스크립트 파일을 하나씩 순서대로 실행해 준다. 여기서는 첫번째 파일에 DDL 스크립트를 넣고, 두번째 파일에는 기초데이터를 넣었다. 이 기능을 위해 대상 DB에 스크립트의 버전을 관리하는 테이블을 flyway가 스스로 만들어 둔다. 아래는 실행 스크립트를 만드는 방법이다.


flyway에 실핼 시킬 스크립트는 일반적으로 /resources/db/migration 아래 넣는 것이 기본 설정 사항이지만, 여기서는 따로 설정할 수 있도록 filesystem에 지정하였다. 50번 라인이 그것이다.

스크립트는 DDL 부분과 기초 데이터 insert 부분을 각각 따로 만들었다. 파일명은 규칙에 따라 V1__demo-initial-schema.sql 과 V2__demo-initial-data.sql으로 나눴다. ddl 스크립트는 hibernate 구동 시 생성되는 스크립트를 로그에서 복사하여 붙였으며, insert 문장은 ddl 문장에 맞춰서 작성 하였다.

V1__demo-initial-schema.sql 

V2__demo-initial-data.sql



2.DAO 테스트

아래 테스트하는 코드는 SpringSecurity에 사용할 메소드들이다. 더 추가되거나 변경될 수 있지만, 일단 생각나는 대로 작성했다. 테스트 코드를 보면 매우 엉성해 보일지도 모르겠다. 그러나 테스트 코드는 이정도 수준으로 작성하는 것이 옳다는 생각을 한다. 더 정교하고 완벽한 테스트 코드는 많은 시간과 노력이 들어가기 때문에 잘못하면 배보다 배꼽이 더 커지는 현상이 있다. 테스트에서 해야할 것은 원하는 동작을 오류 없이 수행 가능한 것인지를 확인하는 것 뿐이다.


테스트 코드를 작성할 때 주의할 점이 있다. 테스트 메소드는 다른 메소드와 연동되어 수행되면 안되고, 반드시 그 자체로 완결되어야 한다. 그렇지 않으면 작업이 진행되면서 계속 변화하고 추가되면 기능들로 인하여 빈번히 테스트를 수정해야 하는 상황이 발생한다. 그런 상황들은 테스트 코드를 만드는 것을 지치게 한다.


24번/25번 라인은 SpringBoot 환경에서 테스트할 환경을 조성해 주는 어노테이션이다. 26번 라인은 Lombok에서 제공하는 어노테이션으로 간편하게 로그를 찍을 수 있게 도와준다. 27번 라인의 @Transactional 어노테이션은 테스트를 수행하면서 더러워진(?) 데이터를 처음 상태로 만들어준다. 즉, 수행된 쿼리를 롤백 시켜준다. @Transactional 어노테이션을 지우면 테스트로 인해 변경된 데이터가 그대로 남게 된다. 28번 라인의 @FixMethodOrder 어노테이션은 테스트 메소드를 이름 순서대로 실행 시켜준다. 이것이 없을 경우는 어떤 테스트가 먼저 실행될 것인지 알 수 없다. 37번 라인의 @Test 어노테이션은 테스트 대상 메소드라는 것을 알린다. 

38번 라인에 선언된 메소드를 주의깊게 봐줬으면 한다. 앞 부분은 테스트 번호를 붙인 것이지만 뒤에는 한글로 테스트 명을 입력하였다. 이렇게 한글로 메소드명을 입력하면 따로 주석을 달지 않아도 직관적으로 무슨 테스트인지 확인할 수 있기 때문에 편리하다. 

40번과 65번 라인은 수행된 결과를 확인하는 문장이다. 이런 Assert 메소드는 여러가지 종류가 있지만, 본인을 이 두 메소드 외에는 별로 사용하지 않는다. 이유는 앞에서도 설명 했듯이 테스트 코드에 많은 시간과 노력을 들이고 싶지 않기 때문이다. 처한 여건이나 상황에 따라 다르겠지만, 적당한 테스트 수준을 유지하는 것이 테스트 코드의 진짜 핵심이지 않을까 싶다.



첨부파일은 본 페이지를 만들기 위한 소스다.

demo.zip