자바/스프링 시큐리티

스프링 시큐리티 기본 API 및 Filter 이해

UroJem 2023. 6. 11. 15:09

스프링 시큐리티란?

스프링 시큐리티는 스프링 기반 애플리케이션의 인증과 권한, 인가 등을 담당하는 스프링의 하위 프레임 워크이다.

  • 접근 주체(Principal)는 보호된 리소스에 접근하는 유저
  • 인증(Authenticate)은 보호된 리소스에 접근한 유저가 누구인지, 애플리케이션의 작업을 수행해도 되는 주체인지 확인하는 과정인 로그인을 의미한다.
  • 인가(Authorize)는 해당 리소스에 접근 가능한 권한을 가지고 있는지 확인하는 과정 (After Authentication, 인증 이후) 인증된 사용자가 어떤 것을 할 수 있는지를 의미한다.
  • 권한은 인증된 사용자가 애플리케이션의 동작을 수행할 수 있도록 제한된 최소한의 권한을 가졌는지 확인한다.
  • 주로 서블릿 필터(filter)와 이들로 구성된 필터체인으로 구성된 위임 모델을 사용한다.
  • 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있다.

*서블릿 필터 : 디스패쳐 서블릿 실행 전 실행되는 클래스

 

스프링 시큐리티 특징과 구조

  • 보안과 관련하여 체계적으로 많은 옵션을 제공하여 편리하게 사용할 수 있음
  • Filter 기반으로 동작하여 MVC와 분리하여 관리 및 동작
  • 어노테이션을 통한 간단한 설정
  • Spring Security는 기본적으로 세션 & 쿠키 방식으로 인증
  • 인증 관리자(Authentication Manager)와 접근 결정 관리자(Access Decision Manager)를 통해 사용자의 리소스 접근을 관리
  • 인증 관리자는 UsernamePasswordAuthenticationFilter, 접근 결정 관리자는 filterSecurityInterceptor가 수행

각 필터 기능 설명

  • SecurityContextPersistenceFilter
    • SecurityContextRepository에서 SecurityContext를 로드하고 저장하는 일을 담당함
  • LogoutFilter
    • 설정된 로그아웃 URL로 오는 요청을 감시하며, 요청온 해당 사용자를 로그아웃 처리
  • UsernamePasswordAuthenticationFilter
    • 사용자명과 비밀번호로 이뤄진 폼 기반 인증에 사용하는 로그인 URL요청을 감시하고 요청이 있으면 사용자의 인증을 진행함
      1. AuthenticationManager를 통한 인증 실행
      2. 인증 성공 시, 얻은 Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
      3. 인증 실패 시, AuthenticationFailureHandler 실행
  • DefaultLoginPageGeneratingFilter
    • 폼기반 또는 OpenID 기반 인증에 사용하는 로그인 URL에 대한 요청을 감시하고 로그인 폼 기능을 수행하는데 필요한 HTML을 생성함
  • BasicAuthenticationFilter
    • HTTP 기본 인증 헤더를 감시하고 이를 처리함
  • RequestCacheAwareFilter
    • 로그인 성공 이후 인증 요청에 의해 가로채어진 사용자의 원래 요청을 재구성하는데 사용됨 SecurityContextHolderAwareRequestFilter : HttpServletRequestWrapper를 상속하는 하위 클래스 SecurityContextHolderAwareRequestWrapper로 HttpServletRequest 정보를 감싸서 필터 체인상 하단에 위치한 요청 프로세서에 추가 컨텍스트를 제공
  • AnonymousAuthenticationFilter
    • 이 필터가 호출되는 시점까지 사용자가 아직 인증을 받지 못했다면 요청 관련 인증 토큰에서 사용자가 익명 사용자로 나타나게 됨
  • SessionManagementFilter
    • 인증된 사용자를 바탕으로 세션 트래킹을 처리해 사용자와 관련한 모든 세션들이 추되도록 도움
  • ExceptionTranslationFilter
    • 이 필터는 보호된 요청을 처리하는 동안 발생할 수 있는 기대한 예외의 기본 라우팅과 위임을 처리함
  • FilterSecurityinterceptor
    • 이 필터는 권한부여와 관련된 결정을 AccessDecisionManager에게 위임해 권한부여 결정 및 접근 제어 결정을 쉽게 만들어 줌

https://devuna.tistory.com/55

 

[Spring Security] 스프링시큐리티의 기본 개념과 구조

[Spring Security] 스프링시큐리티의 개념/시작하기 /기본세팅 💡 스프링시큐리티(Spring Security)란? 스프링 시큐리티는 스프링 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링

devuna.tistory.com

https://sjh836.tistory.com/165

 

spring security 파헤치기 (구조, 인증과정, 설정, 핸들러 및 암호화 예제, @Secured, @AuthenticationPrincipal,

참조문서https://docs.spring.io/spring-security/site/docs/4.2.7.RELEASE/reference/htmlsingle/#getting-startedhttp://springsource.tistory.com/80https://okky.kr/article/3827381. 스프링 시큐리티란?스프링 시큐리티는 스프링 기반의 어

sjh836.tistory.com

 

 

 

프로젝트 생성

강의에서는 자바 8 / Maven 빌드 / 스프링 웹 종속성만 체크하여 프로젝트를 생성한다.

별도로 자바 17 / Gradle 빌드 / 스프링 웹 종속성 프로젝트로 별도 학습해 볼 예정이다.

 

프로젝트 세팅 후 WAS 가동시키면 localhost:8080 로 브라우저에 띄울 수 있다.

 

루트 경로 지정. 후 루트 경로 페이지 접속 확인.

 

현재 시스템은 정상적으로 작동하지만 아무나 자원에 접근할 수 있어 보안이 취약한 구조를 가지고 있다.

예제를 진행하며 차차 보안이 적용된 시스템으로 작업을 진행한다.

 

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

pom.xml에 스프링 시큐리티 의존성을 추가하고 Maven 설치 버튼을 누르면 시스템에서 즉시 보안이 작동된다.

 

WAS 재기동시 시큐리티가 적용된 임시 비밀번호가 뜨는 것을 볼 수 있다.

이 비밀번호는 WAS가 재기동 될 때마다 변경되니 그 때 그 때 변경된 비밀번호를 사용해야 한다.

 

다시 루트 페이지로 이동하면 이제는 login 페이지로 넘어가게 되고 우리가 만든 적 없는 로그인 페이지가 적용되있는 것을 볼 수 있다. 보안설정으로 인해 시스템 내 어떠한 경로이든 보안과 권한처리가 된 사용자만 접근이 되게 구조가 변경되었다. Username에 'user' Password에 콘솔에 적힌 임시 비밀번호(서버 재기동시 매번 변경 됨) '1c66bdfe-4e47-4927-9d9a-b5ada77bbf6e'를 적고 로그인을 하면 보안이 통과되어 인덱스 페이지에 접근할 수 있다.

  • 스프링 시큐리티 의존성 추가 시 일어나는 일들
    • 서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정이 이루어진다.
    • 별도의 설정이나 구현을 하지 않아도 기본적인 웹 보안 기능이 현재 시스템에 연동되어 작동함
      1. 모든 요청은 인증이 되어야 자원에 접근이 가능하다.
      2. 인증 방식은 폼 로그인 방식과 httpBasic 로그인 방식을 제공한다.
      3. 기본 로그인 페이지를 제공한다.
      4. 기본 계정 한 개 제공한다. (Username : user / Password : 콘솔에 찍힌 임시 비밀번호)
  • 문제점
    • 계정 추가, 권한 추가, DB 연동 등
    • 기본적인 보안 기능 외에 시스템에서 필요로 하는 더 세부적이고 추가적인 보안기능이 필요

 

 

사용자 정의 보안 기능 구현

현재 시스템은 사용 계정이 1개 밖에 없고 권한을 추가하거나 제거하는 기능이 없다.

해커의 침입에 대응할 수 있는 보안옵션도 없는 최소한의 보안 시스템이다. 이러한 문제점을 해결할 기능을 구현해 본다.

  • WebSecurityConfigurerAdapter
    • 스프링 시큐리티의 웹 보안 기능을 초기화하고 설정하는 핵심적인 클래스이다. 스프링 시큐리티 의존성 추가 후 시큐리티가 초기화 되면 호출하는 클래스로 가장 기본적인 웹 보안기능을 활성화 하고 시스템에 보안 기능을 작동하게 설정하는 모든 처리를 하는 클래스이다.
  • HttpSecurity
    • 스프링 시큐리티의 세부적인 보안 기능을 설정할 수 있는 API를 제공하는 클래스이다.
    • WebSecurityConfigurerAdapter를 상속받은 설정 클래스에 configure(HttpSecurity http) 메서드를 Overriding 하여 설정한다.
    • WebSecurityConfigurerAdapter 클래스와 마찬가지로 시스템에 가장 기본이 되는 웹 보안기능 활성화하고 작동하게 하는 역할을 한다.
    • 인증 API
      • http.formLogin() - form 을 통한 로그인 방식에 대해 설정
      • http.logout() - 로그아웃에 대한 설정
      • http.csrf() - 서버에 요청 시 서버에서 발급해준 토큰을 HTTP 파라미터로 보내 보안을 강화하는 기능
      • http.httpBasic() - Http basic Auth 기반으로 로그인 창이 뜸
      • http.SessionManagement() - 세션 설정. 최대 허용 가능 세션수나 동시 로그인 설정, 세션 만료시 페이지 이동 등의 설정을 할 수 있다.
      • http.RememberMe() - 로그인한 유저의 SessionId가 서버에서 만료되었더라도 remember-me 쿠키값이 유효하면 로그인을 유지시켜주는 기능(ex. 자동로그인)
      • http.ExceptionHandling() - 예외 설정 처리 기능 작동
      • http.addFilter() - 스프링 시큐리티에 filterComparator에 등록되어 있는 Filter들을 활성화 할 때 사용 가능
    • 인가 API
      • http.authorizeRequests() - http 방식으로 인가를 요청할 때 접근하는 url에 따라 인가를 설정한다.
      •       .antMatchers(/admin) - 접근한 url이 인자값 패스와 일치하는치 검사? 하는 기능 Ant 스타일 와일드카드
        를 사용할 수 있다.
      •       .anyRequest() - 어떠한 요청이라도
      •       .hasRole(USER) - 사용자가 주어진 역할이 있다면 접근을 허용
      •       .permitAll() - 무조건 접근 허용
      •       .authenticated() - 인증된 사용자 접근을 허용
      •       .fullyAuthentication() - 인증된 사용자의 접근을 허용, rememberMe 인증 제외
      •       .acess(hasRole(USER))  - 주어진 SpEL 표현식의 평가 결과가 true이면 접근 허용
      •       .denyAll() - 무조건 접근을 허용하지 않음
  • SecurityConfig
    • 사용자 정의 보안 설정 클래스로 WebSecurityConfigurerAdapter를 상속받아 설정한다.
    • 클래스 내에 HttpSecurity를 사용하여 세부적이고 추가적으로 설정할 수 있는 API를 활용하여 사용자 정의 보안 기능을 구현한다.

 

WebSecurityConfigurerAdapter.java

스프링 부트 실행시 스프링 시큐리티가 초기화 되며 WebSecurityConfigurerAdapter를 호출하면 스프링 시큐리티 초기화 작업을 여러개 진행한다.

HttpSecurity 객체를 생성하고 해당 클래스를 활용하여 세부 보안 설정 API를 호출하여 설정 초기화 작업 진행한다.

그 후 configure메서드 호출하여 추가적인 보안 설정을 진행한다. 

이러한 설정으로 인해 루트 경로로 접근하더라도 인증을 받지 않으면 form 로그인 방식으로 로그인 하도록 페이지가 제공되는 것.

 

 

커스텀한 보안 설정을 위해 WebSecurityConfigurerAdapter를 확장한 설정 클래스를 만든다.

configure 메서드를 Override 하여 모든 요청에 대한 보안 인증 검사를 하고 form 로그인 방식으로 API를 설정한다.

@Configuration은 스프링의 환경설정 파일임을 의미하는 애너테이션이다. 여기서는 스프링 시큐리티의 설정을 위해 사용되었다.

@EnableWebSecurity는 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 애너테이션이다.

WebSecurityconfigureAdapter를 상속하는 설정 객체에 붙여주면 SpringSecurityFilterChain에 등록된다.

 

매번 임시 비밀번호를 사용하여 로그인하기 불편하니 환경설정 파일에 이름과 비밀번호를 설정한다.

 

 

 

Form Login 인증

클라이언트 서버 인증처리 프로세스

  • 클라이언트에서 Get방식으로 /home url로 서버내 자원 접근 시도. 서버의 보안정책은 인증된 사용자만이 서버 자원의 접근을 허용했을 경우
  • 요청한 사용자가 인증 받은 사용자가 아닐 경우 로그인 페이지로 이동시킴
  • 사용자는 Username과 Password를 입력하고 POST 방식으로 인증을 시도한다.
  • 서버에서 스프링 시큐리티가 Session을 생성하여 최종 성공한 인증 결과를 담은 Authentication 타입의 인증 토큰 객체를 생성한다.
  • 인증 토큰 객체를 SecurityContext에 담아 Session에 저장하여 인증처리가 이루어진다.
  • 인증 받은 이후 사용자가 다시 Get 방식으로 /home url로 서버내 자원 접근을 시도하게 되면 세션에 저장된 인증토큰 존재 여부를 판단하여 자원 접근할 수 있게된다.

 

Form Login 인증 방식 API

  • 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()") - 로그인 실패 후 핸들
  •       .loginPage("/login.html")

 

form 로그인 인증 방식 API 설정
익명 클래스를 람다식으로 변경
커스텀 로그인 페이지 controller mapping 설정
부트 재작동 후 루트페이지 접근시 커스텀 로그인 페이지로 잘 넘어오는 것을 확인할 수 있다.
커스텀 페이지로 잘 넘어오는 것을 확인했으니 추후 UI를 만들어 사용해보기로 하고 시큐리티 기본제공 로그인 사용을 위해 주석처리
API에서 설정된 값으로 적용된 것을 확인할 수 있다.
로그인 성공시 디버깅 툴로 확인하면 인증에 성공하 successHandler를 호출하여 콘솔에 username을 출력하고 루트 경로로 이동된다.
로그인 실패시 디버깅 툴로 확인하면 인증에 실패하여 예외 메세지를 출력하고 로그인 페이지로 이동된다.

 

 

 

인증 API - Login Form 인증

UserNamePasswordAuthenticationFilter

사용자가 Form based Authentication 방식으로 Login을 시도할 때 보내지는 요청에서 아이디(username)와 패스워드(password) 데이터를 가져온 후 인증을 위한 토큰을 생성 후 인증을 다른 쪽에 위임하는 역할을 하는 필터이다.

 

AntPathRequestMatcher

사용자가 요청한 요청 정보 url이 올바르게 들어왔는지 확인하는 객체이다.

기본 초기설정은 '/login' 인데 SecurityConfig에서 loginProcessingUrl 설정을 변경해주면 url을 변경할 수 있다.

만약 요청 url이 아닐경우 다음 Filter로 이동한다.

요청 정보 url을 'login_proc'으로 설정

 

Authentication

사용자의 인증 정보를 저장하는 토큰 개념으로 인증 용도 또는 인증 후 세션에 담기위한 용도로 사용된다.

  1. 인증시 username과 password를 담고 인증 검증을 위해 전달되어 사용된다.
  2. AuthenticationManager 객체에 인증 후 최종 인증결과 (사용자 객체, 권한 정보)를 담고 SecurityContext에 저장되어 아래와 같은 코드로 전역으로 참조가 가능하다.
    • Authentication authentication = SecurityContextHolder.getContext().getAuthentication()

 

AuthenticationManager

필터로부터 인증객체를 전달받아 인증에 관련한 책임을 수행하는 객체이다.

AuthenticationProvider와 협력하여 인증을 진행한다.

 

AuthenticationProvider

AuthenticationManager로 부터 인증처리를 위임 받아 실제 인증처리를 담당하는 클래스이다.

인증에 실패하게 되면 AuthenticationException 인증 예외를 발생시켜 UsernamepasswordAuthenticationFilter가 받아서 예외에 대한 후속 조치를 취하게 된다.

인증에 성공하면 Authentication 객체를 생성하게 되고 그 안에 사용자 정보나 권한 정보를 담아 AuthenticationManager에게 전달한다.

 

SecurityContext

Authentication 객체가 저장되는 보관소로 필요시 언제든지 Authentication 객체를 꺼내 쓸 수 있도록 제공되는 클래스

ThreadLocal에 저장되어 아무 곳에서나 참조가 가능하도록 설계됐다.

인증이 완료되면 HttpSession에 저장되어 어플리케이션 전반에 걸쳐 전역적인 참조가 가능하다. 

 

SuccessHandler

인증 성공 후 작업들을 처리하는 객체이다.

 

 

인증 API - Logout, LogoutFilter

클라이언트에서 로그아웃 요청이 오면 서버에 스프링 시큐리티가 로그아웃 처리를 진행하게 된다.

세션을 무효화 시키고 인증객체 토큰을 삭제하고 인증객체 토큰이 담긴 SecurityContext도 삭제한다.

쿠키정보까지 삭제하고 로그아웃이 성공되면 로그인 페이지로 이동하게 된다.

 

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

 

logout API 설정
기본 post 방식과 API값 적용 확인
로그아웃 버튼 누르면 커스텀 핸들러로 들어오는 것 확인

 

LogoutFilter

로그아웃에 대한 처리를 담당하는 필터로 사용자가 로그아웃 요청을 했을때만 적용되는 필터이다.

AntPathRequestMatcher

LogoutFilter로부터 로그아웃 요청 url이 들어왔는지 확인하고 만약 해당 url이 아닐 경우 다음 Filter로 이동하여 로그아웃처리를 하지 않는다.

Authentication

SecurityContext에서 사용자 인증정보를 찾아서 LogoutHandler로 넘겨준다.

SecurityContextLogoutHandler

기본적인 LogoutHandler는 4개가 있는데 그 중 SecurityContextLogoutHandler는 세션 무효화, 쿠키삭제, SecurityContextHolder내용 삭제의 작업을 하는 핸들러이다.

프로그래머가 구현한 핸들러로도 처리할 수 있다.

SimpleUrlLogoutSuccessHandler

LogoutHandler가 성공적으로 처리가 되면 SuccessHandler를 호출하는데 SimpleUrlLogoutSuccessHandler는 간단한 페이지 이동하는 핸들러로 로그인 페이지로 이동하게 된다.

이 역시 프로그래머가 구현한 핸들러로도 처리할 수 있다.

 

 

인증 API - Remember Me 인증

  1. 세션이 만료되고 웹 브라우저가 종료된 후에도 어플리케이션이 사용자를 기억하는 기능
  2. Remember-Me 쿠키에 대한 Http 요청을 확인한 후 토큰 기반 인증을 사용해 유효성을 검사하고 토큰이 검증되면 사용자는 로그인 된다.
  3. 사용자 라이프 사이클
    • 인증 성공(Remember-Me 쿠키 설정)
    • 인증 실패(쿠키가 존재하면 쿠키 무효화)
    • 로그아웃(쿠키가 존재하면 쿠키 무효화)
  • http.rememberMe() - rememberMe 기능이 작동함
  •       .rememberMeParameter("remember") // 기본 파라미터명은 remember-me
  •       .tokenValiditySeconds // Default는 14일
  •       .alwaysRemember(true) // 리멤버 미 기능이 활성화되지 않아도 항상 실행
  •       .userDetailsService(userDetailsService) // 리멤버 미 기능 사용할 때 시스템에 있는 사용자 계정 조회시 필요한 클         래스

 

rememberMe API 설정
remember-me 체크박스가 생기고 API 적용 값 확인

 

RememberMeAuthenticationFilter

Authentication 인증객체가 null일 경우 동작하게 되는 필터로 로그인 후 인증을 받았을 때 SecurityContext 안에 Authentication 인증객체가 저장되는데 세션이 만료되었거나 브라우저가 종료되어 세션이 끊겨서 SecurityContext를 찾지 못하고 사용자의 인증 객체를 가져오지 못 할경우 RememberMeAuthenticationFilter가 동작하게 된다.

Authentication 인증객체가 null이 아니라면 동작하지 않는데 이미 인증을 받았고 그 인증객체가 존재하기 때문에 다시 인증받을 필요가 없기 때문이다.

그 후 사용자가 처음 로그인시 remember-me 기능을 활성화하여 로그인을 하게되면 서버로부터 remember-me 쿠키가 발급되는데 인증 요청 헤더에 remember-me 쿠키 값으로 서버에 접속할 경우 세션이 없어도 인증을 시도하게 된다. 

사용자의 의도된 로그아웃이 아닌경우 세션 만료나 끊김으로 인증 객체가 없을 때 다시 로그인 인증과정을 거치지 않고 인증객체를 다시 생성하도록 처리되게 하는 필터이다.

 

RememberMeService

2개의 구현체가 있는데 각각의 구현체가 실제 remember-me 인증처리 역할을 하는 클래스이다. TokenBasedRememberMeService는 메모리에서 저장된 토큰과 사용자가 요청할 때 들고온 쿠키 토큰을 비교하여 인증처리를 하게 된다. 인증 토큰 만료기간은 기본 14일이며 별도로 설정해줄 수 있다.

PersistentTokenBasedRememberMeServices 영구적인 방식으로 DB에 서비스 발급 토큰을 저장하고 사용자가 들고온 쿠키 토큰을 비교하여 인증처리를 한다.

 

처리되는 로직은 토큰 쿠키를 추출하여 사용자가 가지고 있는 토큰이 remember-me라는 이름을 가진 토큰인지 검사하고 존재하면 다음 로직처리로 넘어가고 존재하지 않으면 다음 필터로 넘어가게 된다.

토큰이 존재하면 해당 토큰의 포멧이 정상적인 규칙의 토큰인지 검사 후 정상이 아니라면 예외를 던지고 정상적인 토큰이라면 사용자의 토큰 값과 서버에 저장된 토큰값이 일치하는지 검사 후 일치하지 않으면 예외처리 일치하면 사용자 요청 토큰에 포함된 사용자 정보로 DB에 저장된 사용자인지 조회하여 존재하지 않으면 예외처리 존재하면 새로운 Authentication 인증객체를 생성하여 AuthenticationManager에게 전달하여 인증처리를 하게된다.

 

remember-me 기능 활성화 로그인
브라우저 쿠키에 remember-me와 JSESSIONID 쿠키 값이 저장되어 있다.
Authentication 인증 객체 정보를 가진 JSESSIONID 쿠키를 삭제한다.
새로고침 해보면 JSESSIONID가 다시 발급되어 서버에 접근 가능하다. remember-me 쿠키값이 없다면 다시 로그인을 진행해야 한다.

 

 

 

인증 API - AnonymousAuthenticationFilter

사용자가 인증을 받게되면 세션에 사용자 객체를 저장하게 된다.

그 후 인증을 필요로 하는 서버의 자원에 접근하려면 세션에서 해당 사용자의 사용자 객체가 존재하는지 여부를 판단하게 된다.

만약 사용자 객체가 null 이라면 인증을 받지 않은 사용자로 판단을 하여 서버 자원에 접근하지 못 하도록 한다.

스프링 시큐리티에서는 사용자 객체를 null로 판단하여 처리하지 않고 별도의 AnonymousAutehnticationFilter에서 익명사용자 객체로 만들어서 처리를 하는데 사용자의 요청을 AnonymousAuthenticationFilter가 받게되면 처음에 요청한 사용자가 SecurityContext에 Authentication 객체가 있는 인증된 객체라면 다음 필터처리로 넘어가게 되고 인증객체가 없다면 익명의 사용자로 판단하여 AnonymousAuthenticationToken 인증객체를 생성하고 SecurityContextHoler안에 SecurityContext에 익명객체를 저장하게 된다.

그래서 익명의 사용자를 null로 체크하는 것이 아닌 여러 필터나 화면에서 인증여부를 구현할 때 isAnonnymous()와 isAuthenticated로 해당 사용자가 인증을 받은 객체인지 아닌지 SecurityContext에 저장된 인증 객체로 여부를 판단하게 된다. 익명 객체라면 사용자 객체를 못 만들기 때문에 세션에 따로 저장할 필요가 없다.

 

 

 

인증 API - 동시 세션 제어

동일한 계정으로 인증을 받을 때 생성되는 세션 갯수를 설정할 수 있다.

스프링 시큐리티에서는 2가지 방법으로 동시 세션 제어를 할 수 있다.

  1. 갯수가 초과되었을 때 이전 사용자의 세션을 만료시킨다.
    1. 세션 최대 허용갯수가 1개라고 가정하고 사용자 1이 로그인을 한다. 서버에서 인증 처리되어 사용자1의 세션이 생성된다.
    2. 사용자 2가 동일한 계정으로 로그인을 한다. 서버에서 인증 처리되어 사용자2의 세션이 생성된다.
    3. 서버에서 동일한 계정으로 2개의 계정이 생성되었다. 최대 세션 허용개수 설정이 초과되어 이전 사용자1의 세션이 만료가 된다.
    4. 사용자 1이 서버 리소스에 접근할 때 세션이 만료되어 만료시 지정된 페이지로 이동된다.
  2. 갯수가 초과되었을 때 현재 인증을 받으려는 사용자의 인증을 실패시킨다.
    1. 세션 최대 허용갯수가 1개라고 가정하고 사용자 1이 로그인을 한다. 서버에서 인증 처리되어 사용자1의 세션이 생성된다.
    2. 사용자 2가 동일한 계정으로 로그인을 한다.  서버에서 이미 사용자 1의 세션이 생성되어 있어 인증 예외를 발생시켜 로그인을 차단 후 지정된 페이지로 이동한다.

 

  • http.sessionManagement() - 세션 관리 기능이 작동함
  •       .maximumSessions(1) - 최대 허용 가능 세션 수, -1 : 무제한 로그인 세션 허용
  •       .maxSessionPreventsLogin(true) - 동시 로그인 차단함, false : 기존 세션 만료(default)
  •       .invalidSessionUrl("/invalid") - 세션이 유효하지 않을 때 이동 할 페이지
  •       .expiredUrl("/expired") - 세션이 만료된 경우 이동 할 페이지
  • invalidSessionUrl, expiredUrl 두 개 동시에 적었을 경우에는 invalidSessionUrl이 우선된다.

 

동시 세션 제어 API 설정
서로 다른 브라우저를 띄워서 로그인을 해보면 나중에 로그인 하려는 사용자는 로그인이 되지 않는다.

 

 

인증 API - 세션 고정 보호

웹 서버를 공격하려는 공격자가 웹서버에 접속한다. 로그인 페이지 접근시 JSESSIONID를 서버로부터 발급받으면

사용자에게 공격자의 쿠키를 심고 로그인을 했을 때 공격자의 쿠키 값으로 인증되어 있기 때문에 공격자는 사용자의 정보를 공유하여 인증이 필요한 웹서버 리소스에 접근할 수 있다. 이를 세션 고정 공격이라 한다.

이를 방지하기 위해 스프링 시큐리티는 사용자가 인증에 성공하면 세션값을 변경하는 세션 고정 보호 기능을 제공한다.

 

  • http.sessionManagement() - 세션 관리 기능이 작동함
  •       .sessionFixation().changeSessionId() // 사용자 인증 성공시 기존 사용자의 세션 ID만 바꾼다. 서블릿 3.1 이상의 기본값
  •       .sessionFixation().none() // 사용자 인증 성공시 기존 세션 ID를 그대로 사용한다. 공격에 취약
  •       .sessionFixation().migrateSession() // 사용자 인증 성공시 새로운 세션이 생성됨 기존 세션의 속성이 새로운 세션으로 이동한다. (서블릿 3.1 이전의 기본 값)
  •       .sessionFixation().newSession() // 사용자 인증 성공시 새로운 세션이 생성됨. 기존 세션의 속성이 사라진다.

세션 공격에 취약한 API 설정

만약 공격자의 쿠키값을 사용자에 심어 로그인을 하게 될 경우 공격자는 별도의 로그인과정을 거치지 않고 서버 리소스에 접근이 가능하다.

세션 고정 보호 API 설정은 default 값으로 아얘 설정하지 않거나 migrateSession 혹은 newSession 으로 설정해야 한다.

 

 

 

인증 API - 세션 정책

  • http.sessionManagement() - 세션 관리 기능이 작동함
  •       .SessionCreationPolicy.Always - 스프링 시큐리티가 항상 세션 생성
  •       .SessionCreationPolicy.If_Required - 스프링 시큐리티가 필요 시 생성(기본값)
  •       .SessionCreationPolicy.Never - 스프링 시큐리티가 생성하지 않지만 이미 존재하면 사용
  •       .SessionCreationPolicy.Stateless - 스프링 시큐리티가 생성하지 않고 존재해도 사용하지 않음. 세션을 사용하지 않는 인증방식을 사용할 때 사용. JWT 같이 사용자 정보를 별도 토큰에 담아서 사용할 때 사용한다.

 

 

 

인증 API - SessionManagementFilter, ConcurrentSessionFilter

SessionManagementFilter

  1. 세션 관리
    • 인증 시 사용자의 세션 정보를 등록, 조회, 삭제 등의 세션 이력을 관리
  2. 동시적 세션 제어
    • 동일 계정으로 접속이 허용되는 최대 세션수를 제한
    • ConcurrentSessionFilter와 연계하여 동시적 세션 제어 처리
  3. 세션 고정 보호
    • 인증 할 때마다 세션 쿠키를 새로 발급하여 공격자의 쿠키 조작을 방지
  4. 세션 생성 정책
    • Alwats, If_Required, Never, Stateless

 

ConcurrentSessionFilter

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

 

세션 처리 과정

user1이 로그인 요청을 하면 UsernamePasswordAuthenticationFilter가 인증처리를 진행하면서 각각 필요한 객체들을 호출하여 인증과정을 거치게 되는데 첫번째로 동시적 세션 제어를 하는 ConcurrentiSessionControlAuthenticationStrategy 클래스를 호출한다.

ConcurrentiSessionControlAuthenticationStrategy 클래스는 현재 인증 요청한 사용자가 계정으로된 세션이 몇 개인지 확인한다. 현재는 user1이 로그인한 계정이 처음 로그인 중인 것으로 가정하니 세션 카운트는 0개이다. 최대 세션 허용수는 1개로 설정되었다고 했을 때 카운트가 0개로 문제없이 다음으로 넘어간다.

ChangeSessionIdAuthenticationStrategy 클래스가 호출되어 세션 고정 보호 처리를 진행한다. 

현재 기본 설정으로 세션 고정 보호는 인증시 세션 Id가 변경되는 session.changeSessionId() 이어서 새롭게 세션을 생성하고 새로운 세션 쿠키를 발급처리를 하는 클래스가 ChangeSessionIdAuthenticationStrategy 이다.

그 후 RegisterSessionAuthenticationStrategy 클래스가 사용자의 세션을 등록 처리를 하면 인증에 성공하고 session count가 1이 된다.

 

이제 user2가 user1과 동일한 계정으로 로그인 요청을 한다.

ConcurrentiSessionControlAuthenticationStrategy 클래스가 호출되면 인증 요청한 사용자 계정으로 세션을 확인하게 되는데 user1과 동일한 계정으로 이미 1개가 있어서 최대 세션 허용수와 동일하게 된다.

이 때 maxSessionPreventsLogin(true) 설정으로 인증 실패 전략인 경우 SessionAuthenticationException 예외가 발생되어 인증 실패하게 된다.

maxSessionPreventsLogin(false) 설정으로 세션 만료 전략인 경우 session.expireNow()로 이전 사용자인 user1의 세션을 만료시키고 user2가 같은 흐름으로 ChangeSessionIdAuthenticationStartegy 클래스를 호출하여 쿠키를 발급받고  session.chageSessionId()로 새로운 세션을 생성하고 RegisterSessionAuthenticationStrategty 클래스로 user2의 세션을 등록하여 인증에 성공한다.

서버에는 user2의 세션이 생성되어 있고 user1의 세션도 생성되어 있어(만료는 시켰지만 아얘 사라진건 아닌듯) 같은 계정의 갯수는 2개이다.

그 후 user1이 서버 리소스에 접근하게 되면 사용자의 요청마다 세션을 체크하는 ConcurrentSessionFilter가 user1의 세션 만료 여부에 대해 체크하는데 SessionManagementFilter에서 세션이 만료된 것을 session.isExpired()가 true인 것을 확인 하고 user1을 Logout 처리를 하고 오류 페이지를 응답한다.

 

 

 

인가 API - 권한 설정 및 표현

  • 선언적 방식
    • 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/**").permitAll() // 해당 경로에 대한 모든 접근을 허용한다.
            .antMatchers("/shop/mypage").hasRole("USER") // 해당 경로는 USER권한을 가진 사용자만 허용한다.
            .antMatchers("/shop/admin/pay").access("hasRole('ADMIN')"); // 해당 경로는 ADMIN권한을 가진 사용자만 허용한다.
            .adtMatchers("/shop/admin/**").access("hasRole('ADMIN') or hasRole('SYS')"); // 해당 경로는 ADMIN, SYS 권한을 가진 사용자만 허용한다.
            .anyRequest().authenticated(); // 위에 설정되지 않은 모든 접근에 인증받은 사용자만 허용한다.
}

* 주의 사항 - 설정 시  구체적인 경로가 먼저 오고 그것 보다 큰 범위의 경로가 뒤에 오도록 해야한다. 스프링 시큐리티가 코드 위에서부터 아래로 인가처리를 하기 때문

 

표현식

메서드 동작
authenticated() 인증된 사용자의 접근을 허용
fullyAuthenticated() 인증된 사용자의 접근을 허용, rememberMe 인증 제외. form 로그인 방식으로만 인증받았을 때
permitAll() 무조건 접근을 허용
denyAll() 무조건 접근을 허용하지 않음
anonymous() 오직 익명사용자의 접근을 허용
rememberMe() 자동 로그인을 통해 인증된 사용자의접근을 허용 
access(String) 주어진 SpEL 표현식의 평가 결과가 true이면 접근을 허용
hasRole(String) 사용자가 주어진 역할이 있다면 접근을 허용
hasAuthority(String) 사용자가 주어진 권한이 있다면
hasAnyRole(String...) 사용자가 주어진 권한이 있다면 접근을 허용
hasAnyAuthority(String...) 사용자가 주어진 권한 중 어떤 것이라도 있다면 접근을 허용
hasIpAddress(String) 주어진 IP로부터 요청이 왔다면 접근을 허용

 

권한 API 설정

{noop} no-operation 뜻으로 비밀번호에 passwordEncoding없이 일반 텍스트 비밀번호를 사용하기 위해 사용한다.

컨트롤러 맵핑 추가

각 권한에 따른 인가설정을 테스트 하기 위해 임시로 USER, SYS, ADMIN 권한의 유저를 인메모리 방식으로 설정한다.

유저 로그인
user 경로 접속
admin 경로 403 권한 에러
admin/pay 경로 403 권한 에러
admin 로그인
user 경로 403 권한 에러
admin 경로 접속
admin/pay 경로 접근
sys 로그인
user 경로 403 권한 에러
admin 경로 접근
admin/pay 경로 403 권한 에러

인가 설정에 맞게 경로 접근권한 설정이 잘 적용되었다.

관리자는 어플리케이션 내 모든 일을 관리하는 역할을 하니 모든 역할의 권한 부여해 주면 admin 사용자는 모든 페이지에 접근할 수 있다.

 

 

인증 / 인가 API - ExceptionTranslationFilter, RequestCacheAwareFilter

ExceptionTranslationFilter

인증 예외, 인가 예외를 처리하는 필터로 사용자 요청을 try-catch 안에서 호출하고 있는 FilterSecurityInterceptor로 전달하여 처리하게 된다. 스프링 시큐리티 보안 필터중 가장 마지막에 위치하는 필터가 바로 FilterSecurityInterceptor이며, 이 필터에 발생하는 인증 예외, 인가 예외를 ExceptionTranslationFilter로 던져서 각 인증, 인가 예외를 처리한다.

 

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

 

인증을 받지 않은 사용자가 /user 경로로 서버내 자원을 요청하면 FilterSecurittyInterceptor 인증처리 권한 필터가 요청을 받을 때 인증 받지 않은 Anonymous 사용자여서 인가 예외를 발생시킨다.

ExceptionTranslationFilter가 예외를 받아 AccessDeniedExcepton 인가 예외처리로 넘어가게 되는데 Anonymous 익명사용자와 remember-me 인증 사용자의 경우에는 인증 예외 처리인 AuthenticationException 로 넘어가게 된다.

AuthenticationException 에서는 SecurityContext 안에 인증객체를 null로 만든 후  2가지 처리를 하게되는데 AuthenticationEntryPoint 인터셉터 구현체를 호출해서 접근한 사용자가 다시 인증을 받을 수 있게 로그인페이지로 보내버린다.

 

두 번째는 사용자가 원래 가고자 했던 리소스 경로 넘어온 파라미터 등등 사용자의 요청 관련 정보는 DefaultSavedRequest 객체 안에 저장이 되고 DefaultSavedRequest 객체는 다시 Session에 저장이 되는데 저장하는 역할을 HttpSessionRequestCache가 처리하게 된다.

 

인증을 받은 사용자가 /user 경로로 서버 내 자원을 요청하면 자원에 접근 가능한 권한이 ADMIN 권한만 가능하고 인증받은 사용자는 USER권한이라고 했을 때 FilterSecurittyInterceptor 인증처리 권한 필터가 인가 예외를 발생시킨다.

ExceptionTranslationFilter는 인가 예외를 처리하기 위해 AccessDeniedException 를 호출하고 해당 객체 안에는 AccessDeniedHandler를 호출하여 접근 할수 없는 경로라고 알려주는 페이지로 이동시킨다.

 

  • http.exceptionHandling() // 예외처리 기능이 작동함
  •       .authenticationEntryPoint(authenticationEntryPoint()) // 인증실패 시 처리
  •       .accessDeniedHandler(accessDeniedHandler()) // 인가실패시 처리

 

SecurityConfig 클래스에 인증, 인가 예외 API 설정
인증 성공시 사용자가 인증전 요청하던 페이지로 이동하기 위해 successHandler 구현 로직 구현
커스텀 로그인 페이지는 인증없이 접근 가능
SecurityController에 접근 경로 추가
인증 없는 사용자가 루트 페이지 접근
커스텀 로그인 페이지로 리다이렉트
사용자 요청 정보 url 이동 확인을 위해 시큐리티 기본 제공 로그인 페이지 사용. 커스텀 구현 주석처리
인증없는 사용자가 /user 경로 접근
로그인 페이지 이동
로그인 후 /user 경로로 바로 접속
USER 사용자가 /admin 페이지 접근 시도
/denied 페이지로 이동

 

 

 

Form 인증 - CSRF, CsrfFilter

CSRF 공격(Cross Site Request Forgery)은 웹 어플리케이션 취약점 중 하나로 인터넷 사용자(희생자)가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 만드는 공격.

 

CsrfFilter

  • 모든 요청에 랜덤하게 생성된 토큰을 HTTP 파라미터로 요구
  • 요청 시 전달되는 토큰 값과 서버에 저장된 실제 값과 비교한 후 만약 일치하지 않으면 요청은 실패한다.
  •  Client
    • <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
    • HTTP 메소드 : PATCH, POST, PUT, DELETE
  • Spring Security
    • http.csrf() - 기본 활성화 되어있음
    • http.csrf().disabled() - 비활성화 아얘 CsrfFilter를 거치지 않는다.

 

스프링 시큐리티엔 기본적으로 CSRF가 활성화 되어있다.

사용자가 쇼핑몰에 접근하여 로그인 후 CsrfFilter가 작동하여 csrfToken 쿠키를 발급받게 되고 사용자는 쇼핑몰에 접근할 때마다 해당 토큰을 가지고 가기 때문에 자기가 발급한 토큰값과 일치한지 검사한 후 요청을 수락하게 된다.

공격자는 토큰 인증을 받은 사용자에게 링크를 전달하여 공격용 웹 페이지에 접속하게 한 다음 쇼핑몰 리소스를 요청하거나 수정, 삭제, 등록 등의 의도하지 않은 행위를 하게 만들 수 있는데 이 때 csrf 토큰이 없어 공격 행위를 수행할 수 없다.

운영시 csrf 기능은 활성화하여 이런 공격에 노출되지 않게 하는 것이 좋다.

타임리프 같은 view 템플릿, 스프링 form 태그 에서는 기본적으로 POST 방식 요청에서 csrf 토큰을 생성해준다.

 

 

 

더보기

스프링 부트 3으로 시큐리티 셋팅

https://spring.io/guides/gs/securing-web/

 

Getting Started | Securing a Web Application

Suppose that you want to prevent unauthorized users from viewing the greeting page at /hello. As it is now, if visitors click the link on the home page, they see the greeting with no barriers to stop them. You need to add a barrier that forces the visitor

spring.io

 

스프링 부트 2와의 포트 충돌을 피하기 위해 7070 포트로 변경
정상 작동 클라이언트 페이지
루트 경로 Controller 생성
build.gradle에 스프링 시큐리티 스타터 의존성 추가후 코끼리 아이콘을 눌러야 라이브러리가 설치된다.
WAS 재기동시 발급되는 임시 비밀번호 콘솔에 출력
보안설정으로 인한 로그인 페이지 기본 접속 아이디는 'user' / 비밀번호는 콘솔에 찍힌 비밀번호로 로그인할 수 있다.
로그인 완료 후 루트 경로 접속

 

스프링부트 3.0 / 스프링 시큐리티 6 버전부터 WebSecurityCnfigurerAdapter는 deprecated 되어 사용할 수 없다.

WebSecurityCnfigurerAdapter 대신 SecurityFilterChain 빈을 생성하여 설정할 수 있다.

https://velog.io/@kose/SecurityFilterChain-%EB%8B%A4%EC%A4%91-%ED%95%84%ED%84%B0-%EA%B4%80%EB%A6%AC

 

SecurityFilterChain 다중 필터 관리

안녕하세요.! 개발이 즐거운 코세입니다.!이번 포스팅은 SecurityFilterChain에 대해 탐구해본 내용을 정리하여 작성하겠습니다.!SecurityFilterChain을 이해하는 데 도움이 될 수 있는 두 가지 사진을 먼저

velog.io

시큐리티 5버전에서는 EndPoint로 authorizeRequests API를 사용했지만 시큐리티 6버전 부터는  deprecated 되어 authorizeHttpRequests 사용을 권장하고 있다.

https://whatistudy.tistory.com/entry/%EC%B6%94%EA%B0%80-AuthorizeRequests-vs-AuthorizeHttpRequests

 

[추가] AuthorizeRequests vs AuthorizeHttpRequests

목차 [이론] 스프링 시큐리티 1 [이론] 스프링 시큐리티2 [실습] 스프링 시큐리티 Form Login [추가] CustomAuthenticationProvider vs DaoAuthenticationProvider [이론] 스프링 시큐리티3 [이론] 스프링 시큐리티4 [추

whatistudy.tistory.com

 

환경설정에 로그인 정보 설정
form 로그인 인증 방식 API 설정
로그아웃, rememberMe API 설정
세션 관리 API 설정

시큐리티 5 버전에서는 각 API 별로 설정했지만 시큐리티 6 버전에서는 람다식 내부에 설정하는 방식으로 변경되었다.

 

configure(AuthentivationManagerBuilder) 메서드를 override 할 수 없어서 InMemoryUserDetailsManager를 Bean으로 등록하여 임시 권한 유저들을 추가한다.

버전 업으로 설정 API가 변경되었다.

.authorizeRequests() → .authorizeHttpRequests() 
.antMatchers() → .requestMatchers()
.access("hasAnyRole('ROLE_A','ROLE_B')") → .hasAnyRole("A", "B")

 

인증, 인가 예외 API 설정 각 인터페이스를 함수로 구현한다. 인증 예외는 기존 시큐리티 기본 기능을 사용한다.
로그인 successHandler 재정의 성공시 무조건 루트페이지로 이동

 

SecurityController 에 /denied 경로 구현

 

 

 

https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/dashboard