본문 바로가기

SpringBoot

스프링 시큐리티 5.X에서 6버전으로 변경

1. 인증과 인가

인증이란 유저가 누군지인지 확인하는 절차

인가는 인증이 된 사람들을 대상으로 특정 기능에 대한 권한여부를 확인하는 절차

 # 토큰 방식인 JWT가 유행하게 된 이유?

더보기

1. JWT는 인증 절차를 통해 유저 정보를 기반으로 권한을 확인하기에 인가가 쉽다.

2. 세션 기반은 세션을 서버 1개당 1개임으로 두 개 이상의 병렬로 연결되는 경우, 하나의 서버에서 발급한 세션을 다른 서버에서는 쓸 수 없다는 단점이 있습니다.따라서 병렬 방식으로 진행되는 서버 확장에서 문제가 발생할 수 있습니다. 

예를 들면 결제 서버와 상품 서버가 분리되어 있다고 가정하면 세션 인증 기반은 각각 API에서 인증을 해야 하지만 토큰 기반 인증에서는 토큰을 가지는 주체는 서버가 아니라 클라이어늩이기 때문에 가지고 있는 하나의 토큰으로 결제 서버와 주문 서버에게 요청을 보낼 수 있습니다.

 

2. 스프링 시큐리티 흐름

스프링 시큐리티는 스프링 기반 애플리케이션의 보안을 담당하는 스프링 하위 프레임워크입니다. CSRF 공격, 세션 고정 공격을 방어해주고, 요청 헤더도 보안 처리를 해줍니다.

필터 종류

 SecurityContextPersistenceFilter부터 시작해서 아래로 내려가며 FilterSecurityInterceptor까지 순서대로 필터를 거칩니다.원하는 때에는 특정 필터를 제거하거나 필터 뒤에 커스텀 필터를 넣는 등의 설정도 가능합니다.

(1번)UsernamePasswordAuthenticationFilter는 아이디와 패스워드가 넘어오면 인증 요청을 위임하는 인증 관리자 역할을 합니다. (2번) FilterSecurityInterceptor는 권한 부여 처리를 위임해 접근 제어 결정을 쉽게 하는 접근 결정 관리자 역할을 합니다.                                                                         

(1) 사용자가 폼에 아이디와 패스워드를 입력하면, HTTPServletRequest에 아이디와 비밀번호 정보가 전달됩니다. 이때 AuthenticationFilter가 넘어온 아이디와 비밀번호의 유효성 검사를 합니다. (2) 유효성 검사가 끝나면 실제 구현체인 UsernamePasswordAuthentication Token을 만들어 넘겨줍니다. (5)사용자 아이디를 UserDetailService에 보냅니다. UserDetailService는 사용자 아이디로 찾은 사용자의 정보를 UserDetail객체로 만들어 AuthenticationProvider에게 전달합니다. (6)DB에 있는 사용자 정보를 가져옵니다. (7)입력 정보와 UserDetails의 정보를 비교해 실제 인증 처리를 합니다. (8)~(10)까지 인증이 완료되면 SecurityContextHolder에 Authentication를 저장합니다. 위 화면에는 없지만 인증 성고 여부에 따라 성공하면 AuthenticationSuccessHandler, 실패하면 AuthenticationFailuerHandler 핸들러를 실행합니다.

 

 

 

3.Spring Security config  버전

과거 스프링 시큐리티가 6.1이 적용되기 전까지는 메서드 체이닝을 이용해서 다음과 같이 설정하는 경우가 일반적이었습니다.

package com.example.userservice.controller;

import com.example.userservice.dto.RequestCreateUserDto;
import com.example.userservice.dto.ResponseFindUserDto;
import com.example.userservice.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
// MVC 패턴에서 @RequestMapping("user-service")가 없어도 빌드 오류가 발생하지는 않지만
// 게이트웨이서  predicates: - Path=/user-service/** 로 등록되었기 때문에 @RequestMapping("user-service")가 필요함!!
@RequestMapping("user-service")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("health-check")
    public String healthCheck(){
        return "Server is available!";
    }

    // 접속 제한 확인용
    @GetMapping("health-check2")
    public String healthCheck2(){
        return "Server is available!";
    }

 

A. Config 이전 버전

package com.example.userservice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    // @Configuration이 붙은 클래스에서 내부 메소드를 한번씩 호출하고 리턴된 자료를 @Bean에 등록
    @Bean
    public SecurityFilterChain security(HttpSecurity http) throws Exception{
        return http
                .authorizeRequests() // 인증, 인가 설정을 시작합니다.
                .requestMatchers("/user-service/health-check").permitAll()// 해당 주소는 인증, 인가 없이 접속 가능하며
                .anyRequest().hasRole("ADMIN")
                .and()// 그리고 다음 설정으로
                .csrf()
                .disable()
                .build();
    }
}

이 방식은 언제 .and를 진행해야 하는지에 대한 부분을 모르는 객체지향적인 부분이 있어서 개선되었습니다.

그러나 6.1버전이 되며 .and()를 이용해 메서드 체이닝 형태로 설정하는 시큐리티 설정은 deprecated 되었다는 에러 메시지가 발생합니다.

따라서 해당 레퍼런스들을 따라 코딩을 해서 시큐리티 기능을 설정해도 실행은 되겠지만 문제는 deprecated 되었다며 경고를 계속 뱉어내기 때문에 이를 방지하기 위해서는 새롭게 바뀐 방식을 따라가야 합니다.

새 방식에서는 아래와 같이 람다식을 이용해 마치 자바스크립트의 콜백함수처럼 설정해야 합니다.

 

B.Spring Security config 6.1 버전

package com.example.userservice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    // @Configuration이 붙은 클래스에서 내부 메소드를 한번씩 호출하고 리턴된 자료를 @Bean에 등록
    @Bean
    public SecurityFilterChain security(HttpSecurity http) throws Exception{
        // 람다식을 사용한 코드로 바꿔서 deprecated 요소를 없애보세요.
        return http
                .authorizeHttpRequests(a->a
                        .requestMatchers("/user-service/health-check").permitAll()
                        .anyRequest().hasRole("ADMIN"))
                .csrf(csrf-> csrf
                        .disable())
                .build();
    }
}