<Spring Security> 기본 API 및 Filter 이해(1)

라모스·2022년 2월 13일
0

Spring Security🔐

목록 보기
1/6
post-thumbnail

프로젝트 구성 및 의존성 추가

dependency 추가

  • Maven project: pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • Gradle project: build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'

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

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

📌 참고: 랜덤으로 생성되는 문자열 password을 고정 값으로 설정할 수 있다.
application.properties에 다음과 같이 기본 username/password 값을 설정할 수 있다.

spring.security.user.name=user
spring.security.user.password=1234

문제점

  • 계정 추가, 권한 추가, DB 연동 등
  • 기본적인 보안 기능 외에 시스템에서 필요로 하는 더 세부적이고 추가적인 보안 기능이 필요하다.

사용자 정의 보안 기능 구현

WebSecurityConfigurerAdapter: 스프링 시큐리티의 웹 보안 기능 초기화 및 설정

  • Security Dependency를 추가한 이후 기본적인 security를 설정 및 구현하는 클래스
  • HttpSecurity라는 세부적인 보안 기능을 설정할 수 있는 API를 제공한다.

제공 API

인증 API인가 API(chain method)
http.formLogin()http.authorizeRequests().anyMatchers(/admin)
http.logout()http.authorizeRequests().hasRole(USER)
http.csrf()http.authorizeRequests().permitAll()
http.httpBasic()http.authorizeRequests().authenticated()
http.SessionManagement()http.authorizeRequests().fullyAuthentication()
http.RememberMe()http.authorizeRequests().access(hasRole(USER))
http.ExceptionHandling()http.authorizeRequests().denyAll()
http.addFilter()

다음과 같이 SecurityConfig 설정 클래스를 만들어 인증/인가 API를 추가하여 보안성을 높일 수 있다.

@Configuration
@EnableWebSecurity // 웹 보안 활성화를 위한 annotation
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests() // 요청에 의한 보안 검사 시작
                .anyRequest().authenticated() // 어떤 요청에도 보안 검사를 한다.
                .and()
                .formLogin(); // 보안 검증은 formLogin 방식으로 하겠다.
    }
}
  • @EnableWebSecurity 애노테이션을 WebSecurityConfigurerAdapter를 상속하는 설정 객체에 붙여주면 SpringSecurityFilterChain에 등록된다.

📌 참고: @EnableWebSecurity
스프링 MVC에서 웹 보안을 활성화하기 위한 애노테이션으로 핸들러 메소드에서 @AuthenticationPrincipal 애노테이션이 붙은 매개변수를 이용해 인증 처리를 수행한다. 그리고 자동으로 CSRF 토큰을 스프링의 form binding tag library를 사용해 추가하는 빈을 설정한다.

Form Login 인증


로직 플로우는 다음과 같다.

  1. Client에서 Get 방식으로 /home URL 자원 접근 요청
  2. Server에서는 인증된 사용자만 접근 가능하다고 판단하여 인증이 안되면 로그인 페이지로 리다이렉트 처리
  3. Client는 로그인 페이지에서 username/password를 입력하여 Post 방식으로 인증 시도
  4. Server에서는 Session ID 생성 후 인증 결과를 담은 인증 토큰(Authentication) 생성 및 저장
  5. Client에서 /home 접근 요청 시 세션에 저장된 인증 토큰으로 접근 및 인증 유지

Security Config 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
        .and()
                .formLogin()
                .loginPage("/loginPage") // 사용자 정의 로그인 페이지
                .defaultSuccessUrl("/") // 로그인 성공 후 이동 페이지
                .failureUrl("/login") // 로그인 실패 후 이동 페이지
                .usernameParameter("userId") // 아이디 파라미터명 설정
                .passwordParameter("passwd") // 패스워드 파라미터명 설정
                .loginProcessingUrl("/login_proc") // 로그인 Form Action Url
                .successHandler(new AuthenticationSuccessHandler() { // 로그인 성공 후 핸들러
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        System.out.println("authentication = " + authentication.getName());
                        response.sendRedirect("/");
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() { // 로그인 실패 후 핸들러
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                        System.out.println("exception = " + exception.getMessage());
                        response.sendRedirect("/login");
                    }
                })
                .permitAll(); // 사용자 정의 로그인 페이지 접근 권한 승인
    }
}

Form Login 인증 필터

UsernamePasswordAuthenticationFilter

로그인 인증처리를 담당하고 인증처리에 관련된 요청을 처리하는 필터이다.

Login Form 인증 로직 플로우는 다음과 같다.

  1. AntPathRequestMatcher(/login)
    → 사용자가 요청한 요청정보를 확인하여 요청정보 URL이 /login으로 시작하는지 확인한다.
    → 일치한다면 다음단계로(인증처리) 진행되고, 일치하지 않는다면 다음 필터(chain.doFilter)로 진행된다.
    // /login URL은 .loginProcessingUrl()로 변경 가능하다.
  2. Authentication에서 실제 인증처리를 하게 되는데, 로그인 페이지에서 입력한 Username과 Password를 인증객체(Authentication)에 저장해서 인증처리(AuthenticationManager)를 맡기는 역할을 한다.
    여기까지가 인증처리를 하기 전 필터가 하는 역할이다.
  3. 인증관리자(AuthenticationManager)는 내부적으로 AuthenticationProvider에게 인증처리를 위임하게 된다. 해당 Provider가 인증처리를 담당하는 클래스로써 인증 성공/실패를 반환하는데 실패할 경우, AuthenticationException 예외를 반환하여 UsernamePasswordAuthenticationFilter로 돌아가 예외처리를 수행하고, 인증에 성공할 경우 Authentication 객체를 생성하여 User 객체와 Authorities 객체를 담아 AuthenticationManager에게 반환한다.
  4. AuthenticationManager는 Provider로부터 반환받은 인증객체(인증결과 유저, 유저 권한정보)를 SecurityContext객체에 저장한다.
  5. SecurityContext는 Session에도 저장되어 전역적으로 SecurityContext를 참조할 수 있다.
  6. 인증 성공 이후에는 SuccessHandler에서 인증 성공 이후의 로직을 수행하게 된다.

정리

인증처리 필터(UsernamePasswordAuthenticationFilter)는 Form 인증처리를 하는 필터로써 해당 필터는 크게 두 가지로 인증 전과 인증 후의 작업들을 관리한다.

인증처리 전에는 사용자 인증 정보를 담아서 전달하면서 인증 처리를 맡기고(AuthenticationManager) 성공한 인증객체를 반환받아서 전역적으로 인증 객체를 참조할 수 있도록 설계된 SecurityContext에 저장하고, 그 이후 SuccessHandler를 통해 인증 성공 후의 후속 작업들을 처리한다.

// AbstractAuthenticationProcessingFilter.javadoFilter(), attempAuthentication(), ProviderManager.java를 확인해보자.

Logout, LogoutFilter

  1. Client에서 POST 방식의 /logout 리소스 호출
  2. Server에서 세션 무효화, 인증토큰 삭제, 쿠키정보 삭제 후 로그인 페이지로 리다이렉트

설정 코드

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .logout() // 로그아웃 처리
                .logoutUrl("/logout") // 로그아웃 처리 URL
                .logoutSuccessUrl("/login") // 로그아웃 성공 후 이동 페이지
                .addLogoutHandler(new LogoutHandler() { // 로그아웃 핸들러
                    @Override
                    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
                        HttpSession session = request.getSession();
                        session.invalidate();
                    }
                })
                .logoutSuccessHandler(new LogoutSuccessHandler() { // 로그아웃 성공 후 핸들러
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.sendRedirect("/login");
                    }
                })
                .deleteCookies("remember-me"); // 로그아웃 후 쿠키 삭제
    }
}

Logic Flow

  1. 요청이 Logout URL인지 확인
  2. 맞을 경우 SecurityContext에서 인증 객체(Authentication)를 꺼내온다.
  3. SecurityContextLogoutHandler에서 세션 무효화, 쿠키 삭제, clearContext() 를 통해 SecurityContext 객체를 삭제하고 인증 객체도 null로 만든다.
  4. SimpleUrlLogoutSuccessHandler를 통해 로그인 페이지로 리다이렉트 시킨다.

Sequence Diagram

Remember Me 인증

  • 세션이 만료되고 웹 브라우저가 종료된 후에도 애플리케이션이 사용자를 기억하는 기능
  • Remember-Me 쿠키에 대한 HTTP 요청을 확인한 후 토큰 기반 인증을 사용해 유효성을 검사하고 토큰이 검증되면 사용자는 로그인 된다.
  • 사용자 라이프 사이클
    • 인증 성공(Remember-Me 쿠키 설정)
    • 인증 실패(쿠키가 존재하면 쿠키 무효화)
    • 로그아웃(쿠키가 존재하면 쿠키 무효화)
  • SessionID 쿠키를 삭제하더라도 Remember-Me가 있다면 해당 쿠키를 decoding한 다음 로그인 상태를 유지할 수 있도록 한다. (EditThisCookie 확장 프로그램으로 테스트 해보면 알 수 있다.)

설정 코드

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .rememberMe() // Remember-Me 기능 작동
                .rememberMeParameter("remember") // 기본 파라미터명은 remember-me
                .tokenValiditySeconds(3600) // Default는 14일
                .alwaysRemember(true) // 리멤버 미 기능이 활성화되지 않아도 항상 실행
                .userDetailsService(userDetailsService);
    }
}

Remember Me 인증 필터

RememberMeAuthenticationFilter

  1. Client에서 요청(세션이 만료되었고, 사용자는 Form 인증 받을 당시 Remember Me를 사용하였기에 RememberMe Cookie를 가지고 있음)
  2. RememberMeAuthenticationFilter가 동작
  3. RememberMeService 인터페이스의 구현체 동작
    TokenBasedRememberMeService : 메모리에서 저장한 토큰과 사용자가 가져온 토큰을 비교(default는 14일 보존)하는 구현체
    PersistentTokenBasedRememberMeService: DB에 발급한 토큰과 사용자가 가져온 토큰을 비교해서 인증 처리하는 구현체
  4. Token Cookie 추출
  5. Token이 존재하는지 검사 → 만약 없다면 다음 필터 동작
  6. Decode Token으로 Token의 format이 규칙에 맞는지 유효성 검사
    → 유효성이 invalidate 하다면 Exception 발생
  7. 토큰이 서로 일치하는지 검사
    → 토큰이 일치하지 않을 경우 Exception 발생
  8. 토큰에 User 계정이 존재하는지 검사
    → 없으면 Exception 발생
  9. 새로운 Authentication Object를 생성하여 인증처리
  10. AuthenticationManager 인증 관리자에게 전달하여 인증처리 수행

Sequence Diagram

익명 사용자 인증 필터

AnonymousAuthenticationFilter

  • 인증객체가 없는 익명사용자 인증 처리 필터
  • 익명사용자와 인증 사용자를 구분해서 처리하기 위한 용도로 사용: 특정 자원(페이지)에 접근시도시 인증 객체를 검사하는데 만약 session을 발급받은 인증객체가 있는 사용자일 경우 해당 객체를 가지고 다음 필터를 동작하지만, 인증 객체가 없을 경우 익명사용자용 인증 객체를 생성하기 위해 사용되고 있다.
  • 화면에서 인증 여부를 구현할 때 isAnonymous()isAuthenticated()로 구분해서 사용
  • 인증 객체를 세션에 저장하지 않는다.

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

A 컴퓨터에서 로그인하여 서비스를 사용하다가 태블릿PC 혹은 다른 컴퓨터 등에서 같은 서비스를 이용하기 위해 로그인을 시도할 수 있다. 하지만 이런 다중 접속시도가 무한정 허용될 경우, 여러 문제점을 발생시킬 수 있다. 다중 로그인을 허용해 동시 접속이 된다면, 한 명이 서비스 결제를 한 뒤 모두가 공유해서 보는 문제가 발생하는 것이 그 예시이다.

Netflix같은 OTT 서비스에선 다중 접속의 경우 인원 제한을 두고 과금 모델을 만들어 제한적인 동시 세션을 허용해주고 있다. Spring Security에서는 이런 세션에 대한 관리 기능도 다음과 같이 제공한다.

  • 세션 관리: 인증 시 사용자의 세션 정보를 등록, 조회, 삭제 등의 세션 이력을 관리
  • 동시적 세션 제어: 동일 계정으로 접속이 허용되는 최대 세션 수를 제한
  • 세션 고정 보호: 인증할 때마다 세션 쿠키를 새로 발급하여 공격자의 쿠키 조작을 방지
  • 세션 생성 정책: ALWAYS, IF_REQUIRED, NEVER, STATELESS

동시 세션 제어

동시 세션 제어란 같은 계정(세션)을 동시에 몇 개까지 유지할 수 있게 할 지에 대한 제어를 의미한다.
기존 접속해있는 계정이 있다고 할 때, 새로운 사용자가 동일한 계정으로 접속을 시도했을 때 어떻게 대응할지에 대한 방법으로 기존 사용자를 로그아웃 시키거나 현재 사용자가 접속을 할 수 없게 막는 식이다.

최대 세션 허용 개수를 초과하였을 경우의 처리 로직 전략 2가지

  • 이전 사용자 세션 만료 전략: 신규 로그인 시 기존 로그인 계정의 세션이 만료되도록 설정하여 기존 사용자가 자원 접근시 세션 만료가 된다.
  • 현재 사용자 인증 실패 전략: 신규 사용자가 로그인 시도시 인증 예외 발생

동시 세션 제어 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement() // 세션 관리 기능이 작동함
                .maximumSessions(1) // 최대 허용 가능 세션 수, -1: 무제한 로그인 세션 허용
                .maxSessionsPreventsLogin(true) // 동시 로그인 차단함, false: 기존 세션 만료(default)
                .invalidSessionUrl("/invalid") // 세션이 유효하지 않을 때 이동할 페이지
                .expiredUrl("/expired"); // 세션이 만료된 경우 이동할 페이지
    }
}

세션 고정 보호

사용자가 공격자 세션 쿠키로 로그인을 시도하더라도 로그인시마다 새로운 세션ID를 발급하여 제공하게 되면, JSESSIONID가 다르기 때문에, 공격자는 같은 쿠키값으로 사용자 정보를 공유받을 수 없게 된다.

📌 세션 고정 공격?
공격자가 서버에 접속해서 JSESSIONID를 발급받아 사용자에게 자신이 발급받은 세션 쿠키를 심어놓게 되면 사용자가 세션 쿠키로 로그인을 시도했을 경우 공격자는 같은 쿠키값으로 인증되어 있기때문에 공격자는 사용자 정보를 공유하게 된다.

세션 고정 보호 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement()
                .sessionFixation().changeSessionId(); // 기본값: 세션은 유지하되 세션 아이디는 계속 새로 발급
                // (Servlet 3.1 이상 기본값)
                // none, migrateSession, newSession
    }
}
  • none(): 세션이 새로 생성되지 않고 그대로 유지되기 때문에 세션 고정 공격에 취약하다.
  • migrateSession(): 새로운 세션도 생성되고 세션아이디도 발급된다. 추가로 이전 세션의 속성값들도 유지된다.
  • newSession(): 세션이 새롭게 생성되고, 세션아이디도 발급되지만, 이전 세션의 속성값들을 유지할 수 없다.

세션 정책

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.If_Required);
    }
}
  • SessionCreationPolicy.Always: 스프링 시큐리티가 항상 세션 생성
  • SessionCreationPolicy.If_Required: 스프링 시큐리티가 필요 시 생성(기본값)
  • SessionCreationPolicy.Never: 스프링 시큐리티가 생성하지 않지만 이미 존재하면 사용
  • SessionCreationPolicy.Stateless: 스프링 시큐리티가 생성하지 않고 존재해도 사용하지 않음
    → JWT 토큰방식을 사용할 때는 Stateless 정책을 사용한다.

References

profile
Step by step goes a long way.

0개의 댓글