Spring Security 기본 API 및 Filter 이해

Gyeongjae Ham·2023년 8월 3일
0

Spring Security

목록 보기
1/2
post-thumbnail

이 시리즈는 Spring Security에 대한 학습을 한 후 기록하는 시리즈입니다. 해당 시리즈는 Spring Security 5.7 버전이므로 현재 6.x 버전과 설정방법은 다르지만 개념적 이해는 같으므로 5.7로 진행했습니다.

시리즈에서 다루는 내용

  1. 스프링 시큐리티의 보안 설정 API와 이와 연계된 각 Filter들에 대한 학습
    • API의 개념과 사용법, 처리과정, 동작방식
    • API 설정 시 생성 및 초기화 되어 사용자의 요청을 처리하는 Filter
  2. 스프링 시큐리티 내부 아키텍처와 각 객체의 역할 및 처리과정을 학습
    • 초기화 과정, 인증 과정, 인가 과정 등을 학습
  3. 인증 기능 구현: Form 방식, Ajax 인증 처리
  4. 인가 기능 구현: DB와 연동해서 권한 제어 시스템 구현(URL 방식, Method 방식)

스프링 시큐리티 의존성 추가

스프링 시큐리티 의존성 추가 시 일어나는 일들

  • 서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정이 이루어집니다
  • 별도의 설정이나 구현을 하지 않아도 기본적인 웹 보안 기능이 현재 시스템에 연동되어 작동합니다
    1. 모든 요청은 인증이 되어야 자원에 접근이 가능하다
    2. 인증 방식은 폼 로그인 방식과 httpBasic 로그인 방식을 제공한다
    3. 기본 로그인 페이지를 제공한다
    4. 기본 계정 한 개를 제공한다 - user, 랜덤 문자열 비밀번호

인증 API들

Form Login

  • http.formLogin(): Form 로그인 인증 기능이 작동합니다
  • .loginPage("/login.html"): 사용자 정의 로그인 페이지
  • .defaultSuccessUrl("/home"): 로그인 성공 후 이동 페이지
  • .failureUrl("/login.html?error=true"): 로그인 실패 후 이동 페이지
  • .usernameParameter("username"): 아이디 파라미터명 설정
  • .passwordParameter("password"): 패스워드 파라미터명 설정
  • .loginProcessingUrl("/login"): 로그인 Form Action Url
  • .successHandler(loginSuccessHandler()): 로그인 성공 후 핸들러
  • .failureHandler(loginFailureHandler()): 로그인 실패 후 핸들러

인증 처리 흐름

  1. usernamePasswordAuthenticationFilter를 시작으로 인증처리를 시작합니다
  2. AntPathRequestMatcher(/login)에서 요청 정보가 매칭되는지 확인합니다. /login 부분은 loginProcessingUrl 설정에서 변경이 가능합니다
  3. 요청정보가 매칭되지 않으면 chain.doFilter로 다음 filter가 수행되고, 요청 정보가 매칭되면 Authentication으로 넘어가게 됩니다.
  4. Authentication에서는 유저가 입력한 usernamepasswordAuthentication 객체를 생성하고 이를 AuthenticationManager에 인증을 요청합니다
  5. AuthenticationManager는 인증을 AuthenticationProvider에 위임하고 여기서 인증에 실패하면 AuthenticationException이 반환됩니다. 추후 이 예외에 대한 후처리를 진행합니다.
  6. 인증에 성공하게 되면 AuthenticationProvideruser 객체 정보, authority 권한 정보 등을 담은 Authentication 객체를 생성해서 AuthenticationManager에게 다시 반환합니다
  7. AuthenticationManagerAuthenticationProvider에게 받은 최종적인 인증 객체를 다시 usernamePasswordAuthenticationFilter에게 반환합니다
  8. usernamePasswordAuthenticationFilterUser 객체 정보와 Authorities 정보를 담은 객체로 최종적인 Authenticaton 객체를 만들고 이 객체를 SecurityContext에 저장합니다
    • SecurityContext는 인증 객체를 저장하는 보관소입니다
    • 나중에는 SecurityContextSession에 저장되고, 전역적으로 사용할 수 있게 합니다
  9. 이 후에 SuccessHandler로 성공 후처리를 하게 됩니다

FilterChainProxy

  • 여러 필터들을 가지고 있는 클래스입니다.
  • 우리가 설정한 값에 맞게 필터를 구성해서 순서대로 실행하게 해줍니다
  • 예를 들어 formLogin()을 설정했다면,
    1. WebAsyncManagerIntegrationFilter
    2. SecurityContextPersistenceFilter
    3. HeaderWriterFilter
    4. CsrfFilter
    5. LogoutFilter
    6. UsernamePasswordAuthenticationFilter
    7. DefaultLoginPageGeneratingFilter
    8. DefaultLogoutPageGeneratingFilter
    9. SecurityContextHolderAwareRequestFilter
    10. AnonymousAuthenticationFilter
    11. SessionManagementFilter
    12. ExceptionTranslationFilter
    13. FilterSecurityInterceptor
  • 위의 방식처럼 순서대로 filter들이 실행되게 됩니다

Logout

  • 유저로부터 로그아웃 요청을 받게 되면, Spring Security세션 무효화시키고, 사용자가 로그인할 때 생성한 인증 객체 토큰을 삭제하고, 인증 객체가 저장되어 있는 Security Context 객체도 삭제합니다. 쿠키가 설정되어 있다면 쿠키 정보 또한 삭제한 후 로그인 페이지로 리다이렉트합니다.

Logout API

  • http.logout(): 로그아웃 기능이 작동합니다.
  • .logoutUrl("/logout"): 로그아웃 처리 URL
  • .logoutSuccessUrl("/login"): 로그아웃 성공 후 이동할 페이지
  • .deleteCookies("JSESSIONID", "remember-me"): 로그아웃 후 쿠키 삭제
  • .addLogoutHandler(logoutHandler()): 로그아웃 핸들러
  • .logoutSuccessHandler(logoutSuccessHandler()): 로그아웃 성공 후 핸들러

Spring Security는 원칙적으로 로그아웃은 POST 방식으로 구현합니다.

LogoutFilter 동작 흐름

  1. POST 방식으로 요청을 LogoutFilter가 받습니다.
  2. 현재 요청 정보가 매칭되는지 확인하고 매칭되지 않으면 다음 필터가 실행됩니다.
  3. 요청 정보가 매칭되면, SecurityContext로부터 현재 인증된 인증 객체를 꺼내옵니다.
  4. 꺼내온 객체를 SecurityContextLogoutHandler에게 전달합니다.
  5. SecurityContextLogoutHandler가 세션 무효화, 쿠키 삭제, SecurityContextHolder.clearContext()로 해당 SecurityContext를 삭제하고, 인증 객체도 null로 처리합니다.
  6. 로그아웃처리가 성공적으로 완료되면, LogoutFilterSimpleUrlLogoutSuccessHandler를 호출해서 로그인 페이지로 이동하게 처리합니다.

Remember Me 인증

  • 세션이 만료되고 웹 브라우저가 종료된 후에도 어플리케이션이 사용자를 기억하는 기능입니다.
  • Remember-Me 쿠키에 대한 http 요청을 확인한 후 토큰 기반 인증을 사용해서 유효성을 검사하고, 토큰이 검증되면 사용자가 로그인됩니다.
  • 사용자 라이프 사이클
    • 인증 성공(Remember-Me 쿠기 설정)
    • 인증 실패(쿠키가 존재하면 쿠키 무효화)
    • 로그아웃(쿠키가 존재하면 쿠키 무효화)

Remember Me API

  • http.rememberMe(): rememberMe 기능이 작동합니다.
  • .rememberMeParameter("remember"): rememberMe를 활성화할 때 사용할 파라미터명
  • .tokenValiditySeconds(3600): 토큰 유효기간 기본은 14일
  • .alwaysRemember(true): rememberMe 기능이 활성화되지 않아도 항상 실행
  • userDetailsService(userDetailsService): rememberMe를 인증할 때 사용자 계정을 조회하는 설정

RememberMeAuthenticationFilter 동작 흐름

  • RememberMeAuthenticationFilter가 동작하는 첫번째 조건은 Authentication 객체의 값이 null인 경우입니다. 이미 Authentication이 값을 가지고 있다면 인증되어 있다는 뜻이므로 굳이 RememberMeAuthenticationFilter가 동작할 이유가 없는 것이죠
    • 따라서 RememberMeAuthenticationFilter는 사용자가 세션이 만료되었거나, 사용중 브라우저 종료로 세션이 끊기는 등으로 인증 객체를 SecurityContext안에서 찾지 못하는 경우에 사용자의 인증을 유지하기 위해서 이 필터가 동작해서 인증을 시도합니다.
    • 그래서 사용자는 인증이 유지된채로 계속 서버에 요청을 할 수 있도록 유지하는 기능을 제공하는 필터입니다.
  • 두번째 조건은 당연하게도 사용자가 로그인시 Remember Me기능을 켠 채로 로그인해 Remember me 토큰이 발급된 상태여야 합니다.
  1. 조건이 충족되어 RememberMeAuthenticationFilter가 동작하게 되면 RememberMeServices가 동작하게 됩니다.
  2. RememberMeService는 인터페이스로 구현체는 두 가지가 있습니다. TokenBasedRememberMeServicesPersistentTokenBasedRememberMeServices입니다.
  • TokenBasedRememberMeServices는 사용자의 요청 토큰과 메모리상에 저장되어 있는 토큰을 비교하는 구현체입니다.
  • PersistentTokenBasedRememberMeServices는 말 그대로 DB에 토큰 내용을 저장해서 사용자의 값과 비교하는 구현체입니다.
  1. RememberMeServices가 토큰을 추출해서 사용자가 가지고 있는 토큰이 Remember me라는 이름을 가진 토큰인지 확인합니다. 해당 토큰이 없다면 다음 필터로 넘어갑니다.
  2. 해당 토큰을 가지고 있다면 Decode Token에서 해당 토큰이 규칙을 지키고 있는 토큰인지 확인합니다.(정상 유무 판단)
  3. 정상적인 토큰이라면, 사용자의 토큰과 가지고 있는 토큰이 일치하는지 확인하게 됩니다.
  4. 토큰이 일치한다면, 토큰에 있는 User 정보가 DB에 존재하는지 확인합니다.
  5. 존재한다면 새로운 Authentication을 생성해서 AuthenticationManager에게 전달해서 인증을 처리합니다.

AnonymousAuthenticationFilter

  • 사용자가 인증을 받게되면 세션에 User 객체를 저장한 후에 사용자가 자원에 접근하려고 하면, 세션에서 해당 Usernull인지 아닌지 여부를 판단해서 null이면 자원에 접근하지 못하게 합니다.
  • AnonymousAuthenticationFilter는 인증을 받지 않은 사용자를 null로 처리하는 것이 아닌 익명사용자로 처리하는 것입니다.
  1. 사용자의 요청이 들어왔을 때, 처음에 AnonymousAuthenticationFilter는 사용자의 Authentication이 존재하는지 확인합니다.(SecurityContext에서 확인)
  2. 인증 객체가 존재한다면 다음 필터로 넘어갑니다.
  3. 인증 객체가 존재하지 않는다면, null이 아닌 AnonymousAuthenticationToken을 생성합니다.
  4. 그 후에 SecurityContextHolder안의 SecurityContext에 해당 토큰을 저장합니다.
  5. 해당 인증 객체는 세션에 저장하지 않습니다

동시 세션 제어 / 세션고정보호 / 세션 정책

동시 세션 제어

  • 동일한 계정으로 인증받을 때 생성되는 세션의 허용 개수가 초과되었을 경우에 어떻게 세션을 초과하지 않고, 세션을 유지하는지에 대한 제어(스프링 시큐리티는 두 가지 방법으로 제어를 하고 있습니다)

이전 사용자 세션을 만료시키는 방법

  • 최대 세션 허용 개수를 초과하는 경우에 이전 사용자 세션을 만료시키는 방법입니다.
  • 이 방법으로 최대 세션의 개수를 유지합니다.

현재 사용자 인증 실패시키는 방법

  • 최대 세션 허용 개수만큼 세션이 생성되어 있는 상태에서 로그인을 시도한다면, 인증 예외를 발생시켜서 로그인을 차단하는 방법입니다.

동시 세션 제어 API

  • http.sessionManagement(): 세션 관리 기능이 작동합니다.
  • .maximumSessions(1): 최대 허용 가능 세션 수, -1을 설정하면 무제한 로그인 세션 허용
  • .maxSessionsPreventsLogin(true): true로 설정하면 동시 로그인을 차단합니다.(현재 사용자 인증을 실패시키는 방법), false라면 기존 세션을 만료시킵니다.
  • .invalidSessionUrl("/invalid"): 세션이 유효하지 않을 때 이동할 페이지
  • .expiredUrl("/expired"): 세션이 만료된 경우 이동할 페이지

.invalidSessionUrl("/invalid").expiredUrl("/expired")를 동시에 설정한다면 invalidSessionUrl 설정이 우선시되어 해당 URL로 이동합니다

세션 고정 보호

  • 공격자가 자신의 세션 아이디를 사용자에게 심어서 사용자가 로그인하면 자신도 인증받도록 유도해 해당 사용자의 정보를 볼 수 있는 공격을 세션 고정 공격이라고 합니다.
  • Spring Security는 해당 공격을 방지하기 위해서 보호 기능을 제공합니다.
    • 인증에 성공할 때마다 새로운 세션을 생성하고, 세션 아이디를 발급해서 공격자가 자신의 세션 정보를 활용할 수 없도록 합니다.

세션 고정 보호 API

  • http.sessionManagement()
  • .sessionFixation().changeSessionId(): 기본값으로 사용자가 인증을 시도하면 세션은 그대로 유지한채 세션 아이디만 변경합니다.
    • migrateSession(): 세션과 세션 아이디 모두 새로 생성합니다.
      changeSessionId와 migrateSession은 이전 세션에서 설정한 옵션을 그대로 사용할 수 있습니다.
    • newSession(): 세션과 세션 아이디 모두 새로 생성하지만 이전 세션의 옵션을 사용할 수 없습니다.
    • none(): 세션과 세션 아이디 모두 그대로 두는 설정이므로 세션 고정 공격에 노출될 위험이 있습니다.

세션 정책 API

  • .sessionCreationPolicy(4가지 정책 설정이 가능합니다.)
    • SessionCreationPolicy.Always: Spring Security가 항상 세션 생성
    • SessionCreationPolicy.If_Requried: Spring Security가 필요시 생성(기본값)
    • SessionCreationPolicy.Never: Spring Security가 생성하지 않지만 이미 존재하면 사용
    • SessionCreationPolicy.Stateless: Spring Security가 생성하지 않고 존재해도 사용하지 않음, 세션 자체를 사용하지 않으므로, JWT 토큰 방식을 사용할 때 사용

ConcurrentSessionFilter

  • 매 요청마다 현재 사용자의 세션 만료 여부 체크
  • 세션이 만료되었을 경우 즉시 만료 처리
  • session.isExpired() == true
    • 로그아웃 처리
    • 즉시 오류 페이지 응답

SessionManagementFilter와 ConcurrentSessionFilter 동시 적용되었을 경우

  • 위 그림에서와 같이 SessionManagementFilter에서 확인했을 때, 이미 세션 최대 허용 개수가 가득 차 있다면 세션을 만료시키고 이 후 ConcurrentSessionFilter에서 해당 내용을 확인해서 만료상태라면 로그아웃처리와 오류 페이지 응답을 처리합니다.

  • 해당 흐름을 전체적으로 살펴보면 이렇습니다.
  1. user1이 로그인을 시도하면 ConcurrentSessionControlAuthenticationStrategy에서 현재 사용자의 세션 개수를 확인합니다.
  2. 위 예제에서는 아직 세션이 생성된 적이 없으므로, 세션 고정 보호처리를 한 후에 세션의 정보를 등록하고 인증이 성공됩니다.
  3. 그 후 user2가 인증을 시도하면 똑같이 ConcurrentSessionControlAuthenticationStrategy에서 세션 개수를 확인하고 이 때는 이미 최대 허용 개수인 1개가 생성중이므로 두 전략 중 선택한 전략대로 움직입니다.
    • 인증 실패 전략일 경우 바로 인증을 실패하고 끝납니다.
    • 세션 만료 전략인 경우 user2의 인증을 똑같이 세션 고정 보호처리를 한 후에 세션 정보에 등록하고, user1의 세션을 만료시킵니다.
  4. 이 후 user1이 서버에 자원을 요청하면 ConcurrentSessionFilter는 만료된 것을 확인하고, 바로 로그아웃 처리하고 오류 페이지를 응답합니다.

인가 API

권한 설정

Spring Security에서 하는 권한 설정은 두가지 방식으로 할 수 있습니다

선언적 방식

  • URL
    • http.antMatchers("/users/**").hasRole("USER")
  • Method
@PreAuthorize("hasRole('USER')")
public void user() {
    System.out.println("user")
}

동적 방식 - DB 연동 프로그래밍

  • URL
  • Method

권한 설정

주의 사항
설정 시에 구체적인 경로가 먼저 오고 그것보다 큰 범위의 경로가 뒤에 오도록 설정해야 합니다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
       .antMatcher("/shop/**")  // 인가를 하려는 특정 경로, 설정하지 않으면 모든 경로가 해당됩니다.
       .authorizeRequests()
           .antMatchers("/shop/login", "/shop/users/**").permiAll() // 해당하는 경로의 요청은 허용하겠다는 설정입니다.
           .antMatchers("/shop/mypage").hasRole("USER") // 이 경로는 USER 권한을 가져야 합니다.
           .antMatchers("/shop/admin/pay").access("hasRole('ADMIN')")
           .antMatchers("/shop/admin/**").access("hasRole('ADMIN') or hasRole('SYS')")
           .anyRequest().authenticated()
}

인가 API 표현식

인증/인가 API

ExceptionTranslationFilter

AuthenticationException

  • 인증 예외 처리
  1. AuthenticationEntryPoint 호출
    • 로그인 페이지 이동, 401 오류 코드 전달 등
  2. 인증 예외가 발생하지 전의 요청 정보를 저장
    • RequestCache: 사용자의 이전 요청 정보를 세션에 저장하고 이를 꺼내 오는 캐시 메카니즘
      • SavedRequest: 사용자가 요청했던 request 파라미터 값들, 그 당시의 헤더값들 등이 저장

AccessDeniedException

  • 인가 예외 처리
    • AccessDeniedHandler에서 예외 처리

ExceptionTranslationFilter API

  • http.exceptionHandling(): 예외 처리 기능이 동작합니다.
  • .authenticationEntryPoint(authenticationEntryPoint()): 인증 실패 시 처리
  • .accessDeniedHandler(accessDeniedHandler()): 인가 실패 시 처리

인증 프로세스

CSRF, CsrfFilter

CSRF

CsrfFilter

  • 모든 요청에 랜덤하게 생성된 토큰을 HTTP 파라미터로 요구합니다
  • 요청 시 전달되는 토큰 값과 서버에 저장된 실제 값과 비교한 후 만약 일치하지 않으면 요청은 실패합니다.

Client

  • <input type="hidden" name="${csrf.parameterName}" value="${_csrf.token}">
  • HTTP 메소드: PATCH, POST, PUT, DELETE

Spring Security

  • http.csrf(): 기본 활성화되어 있습니다.
  • http.csrf().disabled(): 비활성화
profile
Always be happy 😀

0개의 댓글