본 캠프_67일차

졸용·2025년 5월 31일

TIL

목록 보기
68/144

(6월2일)

✅ CSRF란?

CSRF는 Cross-Site Request Forgery(사이트 간 요청 위조)의 줄임말로,
사용자가 자신의 의지와 무관하게 원치 않는 요청을 보내도록 유도하는 공격이다.

"로그인된 사용자의 권한을 도용해서 공격자가 대신 요청을 보내버리는 것"


☑️ 예시로 이해하기

  1. 당신이 은행 사이트에 로그인해서 세션이 유지되고 있음 (예: https://mybank.com)
  2. 공격자가 만든 악성 웹사이트를 방문함 (예: https://evil.com)
  3. 그 웹사이트 안에 몰래 이런 요청이 들어 있음:
<img src="https://mybank.com/transfer?to=attacker&amount=1000000" />
  1. 브라우저는 이미 로그인된 mybank.com 세션을 자동으로 포함해서 요청 전송
  2. 결과: 당신 계좌에서 돈이 빠져나감 🧨

☑️ 공격의 조건

  • 사용자가 이미 로그인한 상태 (세션/쿠키 유지)
  • 악성 사이트가 피해자 브라우저로 요청을 전송
  • 서버가 해당 요청을 검증 없이 실행하면 성공

☑️ 방어 방법: CSRF Token

Spring Security 등에서는 CSRF Token으로 보호한다.

  • 서버는 임의의 고유 토큰을 발급
  • 클라이언트는 이 토큰을 폼 제출 시 포함
  • 서버는 요청마다 이 토큰을 검증

✔️ 토큰이 없거나 잘못되면 → 요청 거부
✔️ 토큰은 쿠키에만 담으면 안 되고, 폼 필드나 헤더에도 포함되어야 안전


☑️ Spring Boot에서 CSRF 설정

http.csrf().disable(); // CSRF 보호 비활성화

✅ 보통 JWT 기반 API 서버에서는:

  • 세션이 없고, 상태 유지도 안 하므로
  • CSRF 공격이 불가능해서 disable() 해도 OK

🚨 주의

  • 폼 기반 로그인 웹 앱이라면 CSRF를 절대 꺼두면 안 된다.
  • API 서버csrf().disable() 해도 괜찮다 (특히 JWT 사용하는 경우).


✅ .sessionManagement

.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

이 코드는 Spring Security에서 세션 사용 방식을 설정하는 부분이다.

이건 다음과 같은 의미이다:

  • 세션을 생성하지 않음
  • 기존 세션도 사용하지 않음
  • 즉, "상태(State)를 저장하지 않음"

"나는 로그인한 사용자 정보를 세션에 저장하지 않고,
매 요청마다 인증 정보를 받아서 처리할 거야."


✅ 왜 이렇게 설정할까?

이 설정은 JWT 기반 인증 시스템에서 필수이다.

JWT 구조에서는:

  • 클라이언트가 JWT 토큰을 가지고 있고,
  • 서버는 그 토큰만 검증할 뿐,
  • 사용자 상태(Session)를 서버에 저장하지 않음.

→ 따라서 **STATELESS (무상태)**로 설정해야 한다.


☑️ 다른 옵션들과 비교

옵션설명
STATELESS세션 생성 안 함. 매 요청마다 인증 필요 (✅ JWT 방식에 적합)
STATEFUL (기본값)로그인 시 세션 생성, 이후 인증 상태 유지
IF_REQUIRED필요할 때만 세션 생성 (기본값)
ALWAYS무조건 세션 생성
NEVERSpring Security는 세션 안 만들지만, 다른 곳에서 만들어지면 사용

✅ REST API + JWT 인증

http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

➡️ ❌ Spring MVC + 세션 기반 로그인은 세션이 필요하므로 이 설정을 쓰면 안 됨.



✅ .authorizeHttpRequests

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/auth/**", "/users/signup", "/error").permitAll()
    .anyRequest().authenticated()
)

이 코드는 Spring Security에서 요청 URL 별 접근 권한을 설정하는 부분이다.
각 URL에 대해 "어떤 요청을 허용할지, 어떤 요청은 인증이 필요할지"를 지정한다.


☑️ 해석

설정설명
.requestMatchers("/auth/**", "/users/signup", "/error")해당 경로들은 예외로 처리함 (로그인 없이 접근 허용)
.permitAll()누구나 접근 가능
.anyRequest().authenticated()그 외의 모든 요청은 인증된 사용자만 접근 가능

✔️ 예시 URL 처리

요청 URL접근 가능 여부
/auth/login✅ 인증 없이 접근 가능
/users/signup✅ 인증 없이 접근 가능
/error✅ 인증 없이 접근 가능
/posts❌ JWT 토큰 없으면 401 에러
/users/me❌ 인증 필요

☑️ 이렇게 분리하는 이유

  • 회원가입/로그인 페이지는 인증 없이도 접근할 수 있어야 하니까 → permitAll()
  • 나머지 API는 로그인한 사용자만 접근해야 하니까 → authenticated()

✔️ 확장 예시

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/", "/login", "/signup", "/oauth2/**").permitAll()
    .requestMatchers(HttpMethod.GET, "/posts/**").permitAll() // 게시글 조회는 누구나
    .requestMatchers("/admin/**").hasRole("ADMIN") // 관리자만 접근 가능
    .anyRequest().authenticated()
)


✅ .exceptionHandling

.exceptionHandling(ex -> ex
    .authenticationEntryPoint((request, response, authException) -> {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 에러
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"status\":401,\"message\":\"인증이 필요합니다.\"}");
    })
)

이 코드는 Spring Security에서 "인증되지 않은 사용자가 보호된 리소스에 접근하려 할 때" 어떻게 대응할지 정의한 부분이다.

🔒 로그인 없이 접근 가능한 URL이 아닌데,
사용자(JWT 토큰 없음 등)가 요청을 보내면 → 이 코드가 실행됨


☑️ 각 줄 설명

코드설명
authenticationEntryPoint(...)인증이 안 된 사용자의 접근 시 동작 지정
response.setStatus(401)HTTP 상태코드를 401 Unauthorized로 응답
setContentType(...)응답 형식을 JSON으로 설정
getWriter().write(...)사용자에게 JSON 메시지를 응답

✔️ 예시 상황

  1. /posts/1 요청은 인증이 필요함
  2. 사용자가 JWT 토큰 없이 요청
  3. .anyRequest().authenticated() 조건에 걸림
  4. Spring Security는 authenticationEntryPoint()를 호출
  5. 사용자에게 다음 응답 전송됨:
{
  "status": 401,
  "message": "인증이 필요합니다."
}

☑️ 이 설정이 중요한 이유

  • 기본 설정을 쓰면 Spring Security가 기본 HTML 로그인 화면을 리턴함 (REST API에는 부적절)
  • API 서버는 보통 JSON 형태로 에러 응답을 반환해야 하므로 이와 같이 커스터마이징 함


✅ .addFilterBefore

.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)

이 코드는 Spring Security의 필터 체인에 커스텀 JWT 필터(jwtFilter)를 등록하는 부분이다.

jwtFilterUsernamePasswordAuthenticationFilter보다 먼저 실행되도록 등록한다.

"스프링 시큐리티가 본격적으로 로그인 체크하기 전에,
내가 만든 JWT 필터를 먼저 실행시켜서
토큰 파싱 → 인증 처리까지 하고 넘기겠다!"


☑️ JWT 기반 인증 흐름 요약

  1. 사용자가 클라이언트에서 JWT 포함된 요청을 보냄

  2. jwtFilter가 먼저 실행됨

  3. jwtFilter 내부에서:

    • 요청 헤더에 있는 JWT 추출
    • 유효성 검증
    • 인증 정보 (Authentication)를 SecurityContext에 저장
  4. 그 다음 필터들 (UsernamePasswordAuthenticationFilter 등)이 이어서 실행


☑️ 왜 UsernamePasswordAuthenticationFilter 앞에 둬야 하나?

  • UsernamePasswordAuthenticationFilter폼 로그인(아이디/비번) 인증 처리용 필터
  • JWT 인증은 폼 로그인과 별개로 동작하므로, 그 전에 먼저 토큰을 확인해야 함
  • 그렇지 않으면 기본 로그인 방식으로 넘어가버림

☑️ 시큐리티 필터 체인에서 필터의 순서

필터는 순서대로 실행되며, 순서가 매우 중요하다.
예를 들어:

[SecurityContextPersistenceFilter]
↓
[CorsFilter]
↓
[jwtFilter]          ← 우리가 만든 커스텀 필터
↓
[UsernamePasswordAuthenticationFilter]
↓
[ExceptionTranslationFilter]
↓
[FilterSecurityInterceptor]

☑️ 정리

  • addFilterBefore(A, B) → B 필터 전에 A 필터를 추가함
  • JWT 기반 인증을 하려면 UsernamePasswordAuthenticationFilter 전에 jwtFilter를 실행해야 함
  • 이를 통해 Spring Security가 우리 JWT 인증 로직을 반영할 수 있게 됨
profile
꾸준한 공부만이 답이다

0개의 댓글