현 시대 대부분 시스템에서는 회원제를 운영 중이다. 회원제를 이용한 관리는 그 사람들의 정보를 수집하고 운영하는 것으로 관리의 주체가되는 회사(시스템)가 그에대한 중요한 책임을 가지기때문에 시간이 흐를수록 정보 보안과 관련된 중요도가 높아지고있다. 이에 Spring에서는 Spring Security라는 별도의 프레임워크에서 관련된 기능을 제공하고 있다.
오늘의 포스팅에서는 그 Spring Security에 대해 기술할 예정이다.
Spring기반 애플리케이션의 보안을 담당하는 스프링 하위 프레임워크로 '인증(Authentication)'과 '권한(Authorization)'에 대한 부분을 Filter의 흐름에 따라 처리를 하고 있다. Spring Security는 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있다.
참으로 수 많은 로직 그림 중 내가 정확하게 이해한 단순한 그림..
JinjuLog님의 Security 로직 필기 그림
☝위의 필기 그림 링크를 통해 보는 것이 더 이해하기 수월하다.
보통 Username - Password 패턴의 인증방식을 거친다면 Spring Security에서는 이러한 인증과 권한 부여를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 Credential 기반의 인증 방식을 사용한다. Spring Security를 이해하기 위해서는 애플리케이션 보안을 구성하는 인증과 권한 이 두 가지 영역에 대해서 알아야한다.
Principal(접근 주체) : 보호받는 Resource에 접근하는 대상
Credential(비밀번호) : Resource에 접근하는 대상의 비밀번호
인증(Authentication) : 사용자의 Identification을 확인하는 절차이다.
- Session관리, Token관리를 통한 UsernamePassword를 통한 인증을 할 수 있다.
인가-권한부여(Authorize) : 현재 유저가 어떤 서비스, 페이지에 접근할 수 있는 권한이 있는지 검사한다.
- Session관리, Token관리를 통한 UsernamePassword를 통한 인증을 할 수 있다.
- 특정 페이지/리소스에 접근할 수 있는지 권한을 판단한다.
- Secured, PrePostAuthorize 어노테이션으로 쉽게 권한 체크를 할 수 있지만 비즈니스 로직이 복잡한 경우 AOP를 이용해 권한 체크를 해야한다.
위 그림은 Spring 프레임워크 요청에 대한 Life Cycle을 나타낸 것이다. 우리가 잘 아는 MVC의 Request 처리 전에 Filter를 거쳐서 DispatcherServlet에 의해 컨트롤러에 매핑된다. 또한 Filter는 필터체인(Filter Chain)을 통해 여러 필터가 순차적 그리고 연쇄적으로 동작하게도 할 수 있다.
Filter를 사용하기 위해서는 Filter Class를 만들어야 하나 Spring에서는 필터체인을 구성하는 Configuration 클래스인 WebSecurityConfigurerAdapter
를 상속하여 해당 클래스의 상속을 통해 Filter Chain을 구성할 수 있다. 그 후 @EnableWebSecurity
애노테이션을 붙혀주면 SpringSecurityFilterChain에 등록된다.
- HeaderWriterFilter : HTTP 헤더를 검사하는 필터
- CorsFilter : 허가된 사이트나 클라이언트의 요청 여부 체크
- CsrfFilter : 내보낸 리소스에서 올라온 요청 여부 체크
- LogoutFilter : 로그아웃 URL로 지정된 가상 URL에 대한 요청을 감시하고 매칭되는 요청이 있으면 사용자를 로그아웃 시킨다
- UsernamePasswordAuthenticationFilter : 사용자명과 비밀번호로 이루어진 폼기반 인증에 사용하는 가상 URL 요청을 감시하고 요청이 있으면 사용자의 인증을 진행한다.
- ConcurrentSessionFilter : 요청마다 현재 사용자의 세션 만료 여부 체크
- BearerTokenAuthenticationFilter : Authorization 헤더에 BearerToken 존재 시 인증 처리
- BasicAuthenticationFilter : Authorization 헤더에 BasicToken 존재 시 인증 처리
- RequestCacheAwareFilter : request 이력 캐시에 저장
- SecurityContextHolderAwareRequestFilter : 보안 관련 Servlet3 스펙 지원을 위한 필터
- RememberMeAuthenticationFilter : 아직 Authentication 인증이 안된 경우 RememberMe 쿠키 검사를 통한 인증 처리
- AnonymousAuthenticationFilter : 이 필터가 호출되는 시점까지 사용자가 인증을 받지 못했다면 요청 관련 인증 토큰에서 사용자가 익명 사용자로 나타난다.
- SessionManagementFilter : 서버에서 지정한 세션 정책 체크
- ExceptionTranslationFilter : 인증 및 권한의 예외 발생 시 캐치하여 처리
- FilterSecurityInterceptor : 권한 부여와 관련한 결정을 AccessDecisionManager에게 위임해 권한 부여 결정 및 접근 제어 결정을 쉽게 만들어 준다.
- DefaultLoginPageGeneratingFilter : 폼기반 또는 OpenID 기반 인증에 사용하는 가상 URL에 대한 요청을 감시하고 로그인 폼 기능을 수행하는데 필요한 HTML을 생성한다.
※ @EnableWebSecurity
애너테이션은 웹 보안을 활성화하지만 그 자체로 유용하지 않고 Spring Security가 WebSecurityConfigurer를 구현하거나 컨텍스트의 WebSecurityConfigurerAdapter를 상속(extends)한 빈으로 설정되어 있어야 한다. 대체적으로 WebSecurityConfigurerAdapter를 상속하여 클래스를 설정하는 것이 가장 편하고 자주 쓰이는 방법이다.
※ @EnableWebMvcSecurity
애너테이션은 스프링 MVC 인수 결정자를 설정해 핸들러 메소드가 @AuthenticationPrincipal 애너테이션이 붙은 인자를 사용하여 인증한 사용자 주체를 받는다. 또 자동으로 숨겨진 사이트 간 요청 위조(CSRF, Cross-Site Request Forgery) 토큰 필드(Token Field)를 스프링의 폼 바인딩 태그 라이브러리를 사용하여 추가하는 빈을 설정한다.
※ @AuthenticationPrincipal
애너테이션은 UserDetails 타입을 가지고, 이를 구현한 PrincipalDetails 클래스를 받아 User object를 얻는다. 즉, UserDetailsService에서 Return한 객체를 파라미터로 직접 받아 사용할 수 있다.
WebSecurityConfigurerAdapter 클래스는 세 가지 configure() 메소드를 오버라이딩하고 동작을 설정하는 것으로 웹 보안을 설정할 수 있다.
configure(WebSecurity web) : Spring Security의 필터 연결을 설정하기 위한 오버라이딩이다. Spring Security 앞단의 설정들을 하는 객체이며 antMatchers에 파라미터로 넘겨주는 endpoints는 Spring Security Filter Chain을 거치지 않기 때문에 '인증', '인가' 서비스가 모두 적용되지 않는다. WebSecurity가 HttpSecurity보다 우선적으로 고려되기 때문에 둘을 모두 설정할 경우 HttpSecurity에 인가 설정은 무시된다.
configure(HttpSecurity http) : 스프링시큐리티의 각종 설정은 HttpSecurity로 대부분 하게된다. 인터셉터로 요청을 안전하게 보호하는 방법을 설정하며
(1) 리소스(URL) 접근 권한 설정
(2) 인증 전체 흐름에 필요한 Login, Logout 페이지 인증 완료 후 페이지 인증 실패 시
이동 페이지 등 설정
(3) 인증 로직을 커스텀하기위한 커스텀 필터 설정
(4) 기타 csrf, 강제 https 호출 등 거의 모든 Spring Security 설정을 한다.
configure(AuthenticationManagerBuilder auth) : 사용자 상세 서비스를 설정하기 위한 오버라이딩으로 인증에 대한 지원을 설정하는 몇 가지 메소드를 가지고 있다. 그 중 inMemoryAuthentication() 메소드로는 활성화 및 설정이 가능하고 선택적으로 인메모리 사용자 저장소에 값을 채울 수 있어서 간단히 호출하는 것으로 인메모리 사용자 저장소가 활성화 된다.
withUser() 메소드는 UserDetailsManagerConfigurer.UserDetailBuilder를 반환하여 UserDetailBuilder가 지원하는 몇 가지 사용자 메소드를 제공한다. 사용자 암호를 설정하는 password()와 권한에 대한 역할을 부여해주는 roles() 등이 있다.
리소스(URL)의 권한 설정 - 특정 리소스의 접근 허용 또는 특정 권한을 가진 사용자만 접근을 가능하게 할 수 있다.
메소드 명 | 설명 |
---|---|
.authorizeRequests() | Security 처리에 HttpServletRequest를 이용하고 ExpressionUrlAuthorizationConfigurer을 불러온다. |
.antMatchers("경로") | 특정 리소스에 대하여 권한을 설정한다. |
.anyRequest() | 설정한 경로 외의 모든 경로에 대한 권한 |
.authenticated() | 인증된 사용자만 접근할 수 있다. |
.permitAll() | 인증 절차 없이 리소스에 접근 가능하다. |
.denyAll() | 인증 없이 리소스에 접근 불가능하다. |
.access(String str) | SpEL표현식의 결과가 true이면 접근할 수 있다. |
.hasRole(String role) | 사용자가 해당되는 Role이 있다면 접근할 수 있다. |
.hasAnyRole(String role) | 사용자가 가진 Role 중 해당되는 Role이 하나라도 존재한다면 접근할 수 있다. |
.anonymous() | 익명사용자가 접근할 수 있다. |
.rememberMe() | rememberMe인증 사용자가 접근할 수 있다. |
로그인처리 설정 - 가장 일반적인 로그인 방식인 로그인 Form 페이지를 이용하여 로그인 하는 방식을 사용하려할때 이용하는 설정으로 커스텀 필터를 적용할 때 혹은 여러가지 설정이 중복되거나 서로 상관없는 설정이 겹치는 것에 주의해야 한다.
메소드 명 | 설명 |
---|---|
.formLogin() | form기반으로 로그인 할 경우의 설정을 추가할 수 있고 FormLoginConfigurer를 불러온다. * http.formLogin() 를 호출하지 않으면 커스텀 필터를 만들고 설정하지 않는 이상 로그인 페이지 및 기타 처리를 할 수 없다. |
.loginPage(String url) | default 값은 "/login"이지만, 사용자가 커스텀한 로그인 페이지 url을 넣어 호출할 수 있다. |
.usernameParameter(String str) | 사용자를 구분할 수 있는 값을 가져온다. default 값은 "username"으로 만약 <input>의 name값이 username이 아니라면 그 name 값을 입력해준다. ex) <input name="id"> / .usernameParameter("id") |
.passwordParameter(String str) | 사용자를 인증 할 수 있는 값을 가져온다. default 값은 "password"로 만약 <input>의 name값이 password이 아니라면 그 name 값을 입력해준다. |
.loginProcessingUrl(String url) | default값은 "/login"으로, 로그인 인증을 처리 할 url을 설정한다. form 태그의 action 속성과 맞추어준다. |
.defaultSuccessUrl(String url) | 정상적으로 인증 성공(로그인)하였을 경우 해당 url로 이동한다. |
successHandler | 정상적으로 인증 성공(로그인) 후 별도의 처리가 필요한 경우 커스텀 핸들러를 생성하여 등록할 수 있다. |
.failureUrl(String url) | 인증 실패하였을 경우 해당 url로 이동한다. default값은 "/login?error"이다. |
.failureHandler | 인증 실패 후 별도의 처리가 필요한 경우 커스텀 핸들러를 생성하여 등록할 수 있다. |
.logout() | 로그아웃 시 설정을 추가할 수 있고 LogoutConfigurer을 불러온다. |
.logoutUrl(String url) | default값은 "/logout"으로 로그아웃을 처리할 url을 설정한다. 로그아웃의 href 속성과 맞추어준다. |
.logoutSuccessUrl(String url) | 로그아웃 성공 시 해당 url로 이동한다 |
아직 인증에 관련된 클래스를 정의하기 전에 먼저 Filter 로직을 설명하려고 한다.
나도 위 그림만 보아서는 어떤 로직인지 이해 할 수 없었기 때문에 다른 블로그들의 순서 풀이를 보고 이해하였다(감사합니다..!). 풀이는 다음과 같다.
(1) 사용자가 로그인 정보와 함께 인증 요청(Http Request)을 하면 AuthenticationFilter가 요청 정보를 인터셉트한다.
(2) AutenticationFilter는 요청 정보를 통해 UsernamePasswordAutehnticationToken의
인증용 객체를 생성한다.
(3) 이 인증용 객체를 ProviderManager를 구현한 AuthenticationManager 인터페이스에
위임한다.
(4) AuthenticationManager는 자신이 가진 AuthenticationProvider에게 (조회하여) 인증을
요구한다.
(5) AuthenticationProvider는 DB에서 사용자 인증 정보를 조회할 UserDetailsService
객체에게 사용자 정보(사용자 아이디, 암호화된 패스워드, 권한 등)를 넘겨준다.
(6) UserDetailService는 넘겨받은 사용자 정보를 통해 DB에 저장된 사용자 정보를 가져와
UserDetails 객체를 만든다. (인증용 객체와 도메인 객체를 분리하지 않기 위해서
실제 사용되는 도메인 객체에 UserDetails를 상속하기도 한다.)
(7) AuthenticationProvider는 전달받은 UserDetails 객체와 사용자의 입력정보를 비교
(판단)하여 인증처리를 한다.
(8) 인증이 완료되면 사용자 정보를 담은 Authentication 객체를 반환하고
AuthenticationFilter까지 전달한다.
(9) 사용자 정보를 가진 Authentication 객체를 SecurityContextHolder에 담은 이후
AuthenticationSuccessHandle를 실행한다.(실패시 AuthenticationFailureHandler를
실행한다.)
SecurityContextHolder
SecurityContextHolder는 보안 주체의 세부 정보를 포함하여 응용플그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장된다. 기본적으로 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 방법과SecurityContextHolder.MODE_THREADLOCAL 방법을 제공한다.
SecurityContext
Authentication을 보관하는 역할을 하며 SecurityContext를 통해 Autentication 객체를 저장하거나 꺼내올 수 있다.
Authentication
현재 접근하는 주체의 정보와 권한을 담는 인터페이스이다. Authentication 객체는 SecurityContext에 저장되며 SecurityContextHolder를 통해 SecurityContext에 접근하고 다시 SecurityContext를 통해 Authentication객체에 접근할 수 있다.
UsernamePasswordAuthenticationToken
Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로 User의 ID가 Principal 역할을 하고 Password가 Credential 역할을 한다. 이 클래스의 첫 번째 생성자는 인증 전의 객체를 생성하고 두 번째는 인증이 완료된 객체를 생성한다.
AuthenticationManager
인증에 대한 부분은 AuthenticationManager를 통해서 처리하게 되는데 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리된다. 인증에 성공하면 두 번째 생성자를 이용해 객체를 생성하여 SecurityContext에 저장한다. 인증 상태를 유지하기 위해 세션에 보관하며, 인증이 실패한 경우에는 AuthenticationException를 발생시킨다.
AuthenticationProvider
실제 인증에 대한 부분을 처리하는데 인증 전의 Authentication 객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다. AuthenticationProvider 인터페이스를 구현해서 커스텀한 AuthenticationProvider를 작성해서 AuthenticationManager에 등록한다.
ProviderManager
AuthenticationManager를 implements하고 실제 인증 과정에 대한 로직을 가지고 있는 AuthenticaionProvider를 List로 가지고 있으며 for문을 통해 모든 provider를 조회하면서 Authenticate 처리를 한다.
UserDetails
인증에 성공하여 생성된 UserDetails는 Authentication 객체를 구현한 UsernamePasswordAuthenticationToken을 생성하기 위해 사용된다. 정보를 반환하는 메소드를 가지고 있으며 직접 개발한 UserVO 모델에 UserDetails를 implementes하여 이를 처리하거나 UserDetailsVO에 UserDetails를 implements하여 처리할 수 있다.
UserDetailsService
UserDetailsService 인터페이스는 UserDetails 객체를 반환하는 단 하나의 메소드를 가지고 이를 구현한 클래스의 내부에 UserRepository를 주입받아 DB와 연결하여 처리한다.
GrantedAuthority
현재 사용자(Principal)가 가지고 있는 권한을 의미한다. ROLE_*의 형태로 사용하며 보통 'roles' 라고 한다. UserDetailsService에 의해 불러올 수 있고 특정 자원에 대한 권한이 있는지 검사하여 접근 허용 여부를 결정한다.
https://okky.kr/articles/382738 (초보가 이해하는 Spring Security)
http://zgundam.tistory.com/43 (Spring Security 실전 적용 - xml)
https://sjh836.tistory.com/165 (Spring Security 정의)
https://velog.io/@seongwon97/Spring-Security-Spring-Security%EB%9E%80 (Spring Security 정의)
https://dev-coco.tistory.com/174 (Filter 종류)
https://velog.io/@jinjukim-dev/Spring-Security (Security 로직 필기 그림)
https://velog.io/@allen/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0%EC%99%80-%EC%9D%B8%EC%A6%9D (인증 Architecture 설명)
https://velog.io/@seongwon97/Spring-Security-Filter%EB%9E%80 (FilterChain 확인 법)
https://m.blog.naver.com/kimnx9006/220633299198 (Spring Security 모듈)
https://kimchanjung.github.io/programming/2020/07/02/spring-security-02/ (http 설정 정보)
https://lotuus.tistory.com/78 (http 설정 정보)