Spring Security - 인증, 인가, Session, Filter Chain, CSRF, CORS

햇슈·2025년 4월 1일

springboot

목록 보기
2/4

Spring Security


✅ Spring Security란?


  • 인증(Authentication)인가(Authorization)을 처리하기 위한 보안 프레임워크

✅ Spring Security의 주요 기능


1. 인증(Authentication)

  • 사용자가 자신이 주장하는 신원을 확인하는 과정
  • 주요 지원 방식으로는:
    • 로그인, OAuth 2 로그인(소셜 로그인), JWT(JSON Web Token) 인증, 등

2. 인가(Authorization)

  • 인증과 인가가 헷갈릴 수 있는데, 인가 = 권한 부여 라고 생각하면 쉽다.
  • 인증된 사용자특정 자원에 접근할 권한이 있는지 결정
  • 주요 접근 제어 방식으로는:
    • antMatchers 같은 URL 별 접근 제한
    • @PreAuthorize , @Secured 같은 메소드 레벨 보안
    • 아니면 Role(Admin, user)이나 권한 기반 접근 관리를 해 준다.

3. Session 관리

  • 로그인 후 사용자 정보를 저장하고 관리
  • 세션 생성, 유지, 만료를 제공한다.

**[어떻게 Spring Security에서 session을 관리할까?]

  • Session 관리 components
    • SecurityContextHolderFilter
    • SecurityContextPersistenceFilter
    • SessionManagementFilter
      • 현재 요청 중 사용자가 인증되었는가?를 확인
      • SecurityContextRepository에 저장된 내용과 SecurityContextHolder에 저장된 현재 내요(Thread local)을 비교
      • SecurityContextRepository 에 보안 컨텍스트가 있다면 → 필터는 아무것도 하지 않는다.
      • 보안 컨텍스트가 없지만, SecurityContextHoler 에 익명 사용자가 아닌 Authentication 객체가 있다면 → 필터는 해당 사용자가 이전 필터에서 인증되었다고 간주
      • 이후 설정된 SessionAuthenticationStrategy 를 호출
    [참고 : Spring Security 5 vs Spring Security 6]
Spring Security 5Spring Security 6
인증 시점 감지SessionManagementFilter 로 감지인증 메커니즘이 직접 처리
세션 읽기매 요청마다 HttpSession 조회필요 시에만 조회
전략 호출필터에서 SessionAuthenticationStrategy 호출인증 로직 내부에서 직접 호출
주의 사항DSL 일부 무효화 가능성 존재sessionManagement() 일부 기능 동작 안 함

참고 자료

https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html

4. 보안 필터 체인

  • request가 처리되는 과정에서 계속적으로 보안 필터가 실행
    • 각 요청은 이 필터 체인을 거치면서:
      • 인증 되었는가?
      • 권한(인가)가 있는가?
      • CSRF, Session 등 보안 체크를 거쳐서
      • 최종적으로 컨트롤러나 리소스로 넘어가게 해 준다!
  • 예를 들어
    • UsernamePasswordAuthenticationFilter : 로그인 처리 필터
    • ExceptionTranslationFilter : 보안 예외 처리 필터

**[코드로 보는 필터 체인 구성 예시]

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
            )
            .csrf(csrf -> csrf
                .disable()
            );

        return http.build();
    }
}
  • 위의 코드를 분석해 보자!
    • 보통 위와 같이 관리를 하게 되는데,
    • 사용자가 /admin/page 로 접근하면?
    • Security Filter Chain 시작!
      • SecurityContextPersistenceFilter 을 통해 기존 로그인 정보가 있는지 확인하고
      • UsernamePasswordAuthenticationFilter 을 통해 로그인 했는지 확인
      • ExceptionTranslationFilter 을 통해 인증 실패면 로그인 페이지로 리다이렉트
      • FilterSecurityInterceptor 을 통해 권한(ADMIN) 체크 한다.
    • 권한이 OK 이면 컨트롤러 진입, 아니면 403 error

5. CSRF(Cross-Site Request Forgery) 방어

  • CSRF 공격으로부터 보호를 해 준다.
  • CSRF 공격이란?
    • 사용자가 의도하지 않은 요청을 전송하도록 유도하는 웹 보안 위협
  • HttpSecurity 설정을 통해 customizing 가능하다.
  • 보호 방식은 요청마다 고유 토큰을 발행하고 이를 검증해 유효한 요청만 처리한다.

**실제 사례로 알아보기


// 실제 폼
<form method="post"
	action="/transfer">
<input type="text"
	name="amount"/>
<input type="text"
	name="routingNumber"/>
<input type="text"
	name="account"/>
<input type="submit"
	value="Transfer"/>
</form>

// 문제가 있는 코드
<form method="post"
	action="https://bank.example.com/transfer">
<input type="hidden"
	name="amount"
	value="100.00"/>
<input type="hidden"
	name="routingNumber"
	value="evilsRoutingNumber"/>
<input type="hidden"
	name="account"
	value="evilsAccountNumber"/>
<input type="submit"
	value="Win Money!"/>
</form>
  • 사용자가 송금액, 은행 번호, 계좌 번호를 직접 입력하고 송금하는 정상적인 요청

  • 아래는 공격자가 만든 폼:

    • 입력값이 hidden 으로 되어 있고 자동으로 악의적인 계좌로 송금을 요청
    • 사용자가 이를 사용한다면??
    • 브라우저가 이미 인증된 은행 사이트 쿠키(JESSIONID)를 함께 보내기 때문에 사용자의 동의 없이 송금이 발생되게 됩니다.. 이러면 당연히 문제가 발생하겠죠?
  • 그렇기에 Spring이 2가지 CSRF 공격에 대응하는 mechanism을 제공합니다

    • Synchronizer Token pattern
      • 서버가 각 세션별로 고유 csrf 토큰을 발급하고, 이를 폼에 포함해, 요청 시 서버가 토큰을 검증해 csrf 공격을 방지
    • 세션 쿠키에 대한 특정 SameSite Attribute
      • 쿠키의 SameSite 속성을 이용해 다른 도메인에서 보내는 요청에 쿠키가 전송되지 않도록 제한해 csrf 공격을 방지
  • 추가적으로 GET, HEAD, OPTIONS, TRACE같은 메서드는 application을 바꾸면 안되기에 read-only로 관리하는 것을 권장합니다.

**[그 외에 구체적인 주의사항]

  • Stateless 앱에서도 쿠키 등 상태 정보가 자동으로 전송되므로 CSRF 취약점이 존재
  • 로그인/로그아웃 요청에도 CSRF 보호를 적용해야 세션 만료와 관련된 문제를 방지할 수 있음
  • 세션 타임아웃 시 CSRF 토큰 처리를 위해 JavaScript 를 사용하거나 쿠키에 토큰을 저장할 수 있지만, 보안상 단점 존재
  • 파일 업로드(multipart) 요청의 경우, 토큰을 URL이나 요청 본문에 넣는 등 특별한 처리가 필요

참고 자료

https://docs.spring.io/spring-security/reference/features/exploits/csrf.html

6. CORS(Cross-Origin Resource Sharing) 설정

  • 다른 출처 간의 리소스를 공유할 지 말지를 설정해주는 보안 정책
  • 다른 출처란?
    • 도메인 : hyttsu02.com
    • 프로토콜 : http, https
    • 포트 번호 : 8080, 443 등
    • 만약 내가 프론트랑 백을 연결하려고 하는데

[CORS 오류 예시]

Access to XMLHttpRequest at 'http://localhost:8080/api/data' 
from origin 'http://localhost:3000' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • 서버가 이 요청을 허용한다는 헤더가 없기에 오류가 생김

[참고 코드를 넣어 놓을게요! : Sesssion 사용 안하는 JWT 서버 ]

    // JWT 서버를 만들 예정, Session 사용 안함.
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        log.debug("디버그 : filtterChain 등록함");
        // iframe 허용 안 함
        http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable));

        // enable 이면 postMan 작동 안 함
        http.csrf(AbstractHttpConfigurer::disable);
        http.cors(cors -> cors.configurationSource(configurationSource()));

        //httpBasic은 브라우저가 팝업창을 이용해서 사용자 인증을 진행
        http.httpBasic(AbstractHttpConfigurer::disable);

        // 필터 적용
        http.with(new CustomSecurityFilterManager(), c -> c.getClass());

        // 인증 실패
        http.exceptionHandling((exceptionConfig) ->
                exceptionConfig.authenticationEntryPoint((request, response, authException) -> {
                    CustomResponseUtil.fail(response, "로그인을 진행해 주세요", HttpStatus.UNAUTHORIZED);
                }));

        // 권한 실패
        http.exceptionHandling((exceptionConfig) -> exceptionConfig.accessDeniedHandler((request, response, e) -> {
            CustomResponseUtil.fail(response, "권한이 없습니다.", HttpStatus.FORBIDDEN);
        }));

        http.authorizeHttpRequests(auth -> auth
                .requestMatchers(new AntPathRequestMatcher("/api/s/**")).authenticated()
                .requestMatchers(new AntPathRequestMatcher("/api/admin/**")).hasRole("" + UserEnum.ADMIN)
                .anyRequest().permitAll());

        // react, 앱으로 요청할 예정
        http.formLogin(AbstractHttpConfigurer::disable);

        // jSessionId를 서버쪽에서 관리 안 하겠다는 뜻!
        http.sessionManagement(sessionManagement ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }

    public CorsConfigurationSource configurationSource() {
        log.debug("디버그 : ConfigurationSource cors 설정이 SecurityFilterChain에 등록함");
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*"); //GET, POST, PUT, DELETE (Javascript 요청 허용)
        configuration.addAllowedOriginPattern("*"); // 모든 IP 주소 허용 (프론트 엔드 IP만 허용 react)
        configuration.setAllowCredentials(true); // 클라이언트에서 쿠키 요청 허용

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;

    }
  • Security Filter Chain에서 CORS 설정하는 방법
    • 아래에 CorsConfigurationSource 보이시나요?
    • 이를 통해 CORS 를 허용해 줍니다!

[Global으로 CORS 허용 하는 방법 + 예시 코드]

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 모든 경로
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true);
    }
}
  • 이것 외에도 컨트롤러 레벨에서 CORS 허용 가능
profile
~ velog 새 단장중 ~

0개의 댓글