Backend/Spring

[Spring Security] 스프링 시큐리티 기본 API 및 Filter 이해 - 1

김세진 2022. 11. 7. 20:26
반응형

 

 

 

서론


 최근 진행 중인 프로젝트에서 Spring Security를 이용한 로그인 인증 파트를 맡게 되었다. 따라서 관련 스프링 강의를 보고 정리할 겸 포스팅을 진행해보려고 한다. IDE는 IntelliJ IDEA 2022.2.2 Ultimate 를 사용하였다.

 

참조한 강의 링크는 아래와 같다.

https://inf.run/wJBU

 

스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 인프런 | 강의

초급에서 중.고급에 이르기까지 스프링 시큐리티의 기본 개념부터 API 사용법과 내부 아키텍처를 학습하게 되고 이를 바탕으로 실전 프로젝트를 완성해 나감으로써 스프링 시큐리티의 인증과

www.inflearn.com

 

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


Spring Security 적용

 

현재 프로젝트는 maven으로 진행 중이므로 pom.xml에 아래와 같이 의존성을 추가하면 Spring Security가 적용된다.

 

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

 

Spring Security 적용 시 변화

  • 모든 요청은 인증을 받은 상태에서만 자원이 주어지도록 변경된다. (Spring Security에서 요청을 가로채 인증이 없는 사용자일 경우 인증 페이지로 자동으로 이동)
  • 인증 방식은 Form 로그인 방식과 *httpBasic 로그인 방식이 주어진다.
  • 아래와 같이 기본 로그인 페이지가 주어진다.

  • 개발자용 기본 계정이 주어진다. Username: user / Password: 랜덤 문자열 (서버 기동 시 매번 달라짐)

 

 

*httpBasic 로그인: 세션을 구성하는 대신 매 요청에 사용자 이름과 비밀번호를 암호화하지 않고 헤더에 직접 포함하여 전송하는 방식. 이러한 인증 방식은 보안 측면에서 취약하기 때문에 https(SSL/TLS)를 사용하여 전송해야 한다.

 

로그 중간에 password가 랜덤 문자열로 뜨는 것을 확인할 수 있다.

 

application.properties 에서 설정 가능

spring.security.user.name=user
spring.security.user.password=1111

 

남은 과제

  • 계정 추가, DB 연동, 권한 관리 등 
  • 실제로 서비스를 하기 위해서는 기본 보안 외 다양한 기능을 요구

 

 

 

사용자 정의 보안 기능


 

 

WebSecurityConfigurerAdapter

  • 기본적인 웹 보안 기능의 초기화와 설정을 담당
  • 이 클래스를 상속받아 SecurityConfig 생성, configure 메서드를 오버라이드하여 사용자 정의 보안 설정 구성
  • HttpSecurity 클래스에서 제공하는 다양한 인증/인가 API를 이용
  • 단, 이 클래스를 상속받아 Override 하는 방식은 현재 Deprecated. 현재는 해당 방식이 아닌 SecurityFilterChain 을 Bean으로 등록해 사용하는 방식을 권장한다.

 

SecurityConfig.java 기본 설정

package io.security.basicsecurity;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity  // 웹 보안 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http // 인가 정책
                .authorizeRequests()  // 요청에 의한 보안검사
                .anyRequest().authenticated();  // 어떤 요청에도 인증 요청

        http // 인증 정책
                .formLogin();  // 폼 로그인 방식 사용
    }
}

 

Deprecated 대응

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    	http // 인가 정책
                .authorizeRequests()  // 요청에 의한 보안검사
                .anyRequest().authenticated();  // 어떤 요청에도 인증 요청

        http // 인증 정책
                .formLogin();  // 폼 로그인 방식 사용
        
        return http.build();
}

 

 

Form Login 인증


인증 API - FORM 인증

 

  1. /home url로 접근 요청
  2. 인증 정보가 없어 로그인 페이지로 Redirect
  3. 유저가 username과 password를 form에 작성하여 POST로 요청
  4. DB와 비교하여 맞는 정보일 경우 Session을 생성하며, 동시에 Authentication 타입의 클래스로 인증 토큰을 생성(User + Authorities). 이 Authentication 은 SecurityContext에 보관되어 사용
  5. 또한 SecurityContext는 Spring Security의 *인메모리 세션 저장소SecurityContextHolder에 저장
  6. 이 인증 토큰의 존재 여부를 판별하여 사용자의 인증을 지속적으로 유지

In-Memory DB: RAM을 사용하는 휘발성 DB로, I/O 작업으로 인한 지연 속도가 거의 없다.

 

SecurityContextHolder

 

SecurityContextHolder 는 Spring Security가 인증한 내용들을 가지고 있고 SecurityContext 는 유저의 인증 정보를 포함한다.

 

  • Principal : 인증 주체, username
  • Credentials : 자격 증명 정보, password
  • Authorities : 허용된 권한들, roles (복수 가능)

 

 

SecurityConfig.java

package io.security.basicsecurity;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http // 인가 정책
                .authorizeRequests()
                .anyRequest().authenticated();

        http // 인증 정책
                .formLogin()
                .loginPage("/loginPage")                    // 실제 로그인이 이루어지는 페이지 (default: /login)
                .defaultSuccessUrl("/")                     // 로그인 성공 후 이동 페이지
                .failureUrl("/login")                       // 로그인 실패 후 이동 페이지
                .usernameParameter("userId")                // 아이디 파라미터명 설정
                .passwordParameter("passwd")                // 패스워드 파라미터명 설정
                .loginProcessingUrl("/login_proc")          // 로그인 Form Action Url (default: /login)
                .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();
    }
}

 

SecurityController.java (loginPage 테스트용)

package io.security.basicsecurity;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SecurityController {

    @GetMapping("/")
    public String index() {
        return "home";
    }

    @GetMapping("loginPage")
    public String loginPage() {
        return "loginPage";
    }
}

 

성공 시 로그
실패 시. 영문판의 경우 Bad Credentials

 

  • 기본 로그아웃 페이지는 localhost:8080/logout 경로로 접근 가능

 

관리자 모드로 Form을 확인해보면 위와 같이 username 파라미터는 userId로, password 파라미터는 passwd로 설정된 것을 확인할 수 있고, form의 action(데이터가 도착할 url) 또한 /login_proc으로 정상적으로 변경돼있음을 확인할 수 있다.

 

 

 

Form Login 인증 필터 : UsernamePasswordAuthenticationFilter


Form Login의 인증 로직 Flow

 

 

  1. 유저의 요청이 UsernamePasswordAuthenticationFilter에 걸린다
  2. AntPathRequestMatcher(/login)
    요청 정보가 SecurityConfig에서 설정한 loginProcessingUrl과 매칭되는지 비교 (기본값은 /login)
    요청 정보가 /login이 아니면 다른 필터(chain.doFilter)로 이동, 맞으면 인증 처리 시작
  3. Authentication 객체에 사용자가 입력한 Username, Password를 저장하여 AuthenticationManager에 인증 처리를 요구함
  4. AuthenticationManagerAuthenticationProvider에 인증 처리를 위임하는데, AuthenticationProvider에서 실제적인 인증 처리가 이루어짐
  5. 인증에 실패할 경우 AuthenticationException 예외를 반환하여 UsernamePasswordAuthenticationFilter 에서 예외 처리를 수행하고, 인증에 성공할 경우 Authentication 객체를 생성하여 User 정보(Username, password)와 Authorities를 담아 최종 반환한다.
  6. 인증 성공 후 SecurityContext에 이 인증 정보를 저장한 뒤 SuccessHandler를 수행한다.

 

*antMatcher의 ant는 Apache Ant Project에서 유래되었으며, 와일드카드를 사용하는 /users/** 꼴의 패턴 스타일을 의미한다.

 

 

Logout 처리, LogoutFilter


Logout Flow

 

  • Client가 logout 요청 *(GET으로도 로그아웃 처리를 많이 하지만, 원칙적으로는 POST로 해야 함)
  • Server에서 세션 무효화, 인증토큰 삭제, SecurityContext 삭제, 쿠키정보 삭제 후 로그인 페이지로 리다이렉트

* 보안에 관련된 작업은 브라우저에 저장된 캐시나 기록 등이 완전히 지워지는 것이 좋다. 그러나 GET 요청은 캐싱되기 쉽고 브라우저 기록에 저장될 수 있으므로 보안에 취약할 수 있다.

 

LogoutFilter

 

  1. Client의 요청이 LogoutFilter에 도달
  2. AntPathRequestMatcher에서 현재 요청이 /logout인지 판별 후, 맞으면 로그아웃 처리를 진행하고 아니면 다음 필터로 넘어감
  3. SecurityContext에서 현재 요청을 보낸 Client의 Authentication 인증 객체를 가져와 SecurityContextLogoutHandler에 전달
  4. SecurityContextLogoutHandler 에서 세션 무효화, 쿠키 삭제, SecurityContext 제거 등 로그아웃 처리 진행
  5. 로그아웃 처리가 끝나면 SimpleUrlLogoutSuccessHandler를 호출하여 로그인 페이지로 리다이렉트 (Security에서 제공하는 기본 LogoutSuccessHandler)

 

 

SecurityConfig.java

@Configuration
@EnableWebSecurity  // 웹 보안 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http // 인가 정책
                .authorizeRequests()
                .anyRequest().authenticated();

        http // 인증 정책
                .formLogin();

        http // 로그아웃
                .logout()
                .logoutUrl("/logout")           // 로그아웃 페이지
                .logoutSuccessUrl("/login")     // 로그아웃 후 이동할 페이지, 아래에 logoutSuccessHandler가 존재하므로 무시된다.
                .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"); // Remember Me 쿠키 삭제
    }
}

 

  • 기본 SecurityContextLogoutHandler에서도 logout 처리를 할 때 세션 무효화 및 SecurityContext 제거 등의 처리가 이루어짐
  • 단, 사용자 정의 Configuration가 우선 순위

 

 

Remember Me 인증


Remeber Me 란?

 

  • 세션 만료 및 웹 브라우저 종료 후에도 어플리케이션이 사용자를 기억하는 기능
  • Remember-Me 쿠키에 대한 http 요청을 확인 후 토큰 기반 유효성 검증에 성공하면 로그인

 

 

사용자 라이프 사이클

  • 인증 성공: 사용자가 인증에 성공하면 Remember-Me 쿠키를 생성하여 브라우저나 세션에 저장한다. Remember-Me 쿠키에는 인증에 사용된 사용자의 ID와 Remember-Me Token이 포함되어 있고, 다음 접속 시 Remember-Me 쿠키를 확인하여 자동으로 로그인 처리를 수행한다.
  • 인증 실패: Remember-Me 쿠키가 있어도 인증에 실패하면 Remember-Me 쿠키를 삭제한다(쿠키 만료 등). 이후 사용자는 수동으로 다시 로그인을 해야한다.
  • 로그아웃: 사용자가 로그아웃을 요청하면 Remember-Me 쿠키를 삭제한다. 이후 사용자는 다시 수동으로 로그인해야 한다.

 

SecurityConfig.java

@Configuration
@EnableWebSecurity  // 웹 보안 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http()
                .rememberMe()
                .rememberMeParameter("remember") // 기본값: remember-me
                .tokenValiditySeconds(3600)      // 기본값: 14일
                .alwaysRemember(true)            // 리멤버 미 기능 항상 실행 (기본값: false)
                .userDetailsService(userDetailsService);  // 리멤버 미 기능 이용 시 인증 계정 조회를 위해 필요
    }
}

 

* UserDetailsService는 사용자의 아이디를 이용해 사용자의 정보를 DB 등에서 가져오는 클래스

 

 

 

Remember Me 인증 필터: RememberMeAuthenticationFilter


Remeber Me Flow

 

동작 조건

  1. 세션이 만료되거나 브라우저가 종료되는 등의 이유로 사용자의 SecurityContext 및 Authentication이 null일 경우 (있다면 이미 인증을 받은 것이므로 필터 동작 X)
  2. Remeber Me 쿠키가 존재

 

동작 Flow

  1. RemeberMeAuthenticationFilter 동작
  2. RemeberMeServices 인터페이스의 두 구현체 동작
    - TokenBasedRemeberMeServices: 서버 메모리에 저장된 토큰과 사용자에게 있는 토큰을 비교하여 인증 (default 14일)
    - PersistentTokenBasedRemeberMeServices: DB에 저장된 토큰과 사용자에게 있는 토큰을 비교하여 인증
  3. Token Cookie 추출
  4. 추출한 Token이 Remember Me 쿠키인지 비교
    - 아니라면 다음 필터로 이동
    - 맞다면 계속 진행
  5. Decode Token을 통해 토큰 Format의 유효성 검사 (여기서 Decode의 의미는 Parsing의 개념에 가까움)
    - 유효하지 않을 경우 Exception
  6. 서버의 토큰과 사용자의 토큰이 일치하는지 검사
    - 일치하지 않을 경우 Exception
  7. User 계정 조회
    - 존재하지 않을 경우 Exception
  8. 새로운 Authentication 인증 객체 생성 후 AuthenticationManager에게 전달하여 인증 처리

 

 

 

반응형