기록하는 공간
CSRF 방어 방법, 코드 (Security 사용 X) 본문
스프링 시큐리티의 어노테이션인 @EnableWebSecurity 어노테이션은 기본적으로 CSRF 공격을 방지하는 기능을 지원한다.
그래서 우리는 항상 SecurityConfig 클래스에 csrf를 비활성화한다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // csrf 비활성화
...~
...~
...~
.build();
}
이때 CSRF가 무엇인지 살펴보자.
CSRF 란 무엇인가.
CSRF는 Cross Site Request Forgery (사이트 간 요청 위조)의 줄임말.
웹 취약점 중 하나이다.
공격자가 희생자의 권한을 도용하여 특정 웹 사이트의 기능을 실행하게 한다.
CSRF 공격이 이루어지는 과정
실제 인터넷 사용자가 자신의 의지와 관계없이 공격자가 의도한 행위를 특정 웹사이트에 요청하도록 만드는 공격이다.
해결방법
나는 Security없이 이를 해결하기 위해 CSRF 토큰을 사용하여 방지하고자 했다.
먼저 CSRF 토큰을 생성하고, Http 응답에 CSRF-TOKEN이라는 이름으로 Cookie에 추가한다.
- CsrfTokenController.kt
@GetMapping("/csrf-token")
fun getCsrfToken(
response: HttpServletResponse,
): RspTemplate<String> {
val csrfToken = csrfTokenService.createCsrfToken(response)
return RspTemplate(HttpStatus.OK, data = csrfToken)
}
- CsrfTokenService.kt
fun createCsrfToken(response: HttpServletResponse): String {
val csrfToken = UUID.randomUUID().toString()
CookieUtils.addCookie(response, CSRF_TOKEN_SESSION_ATTR, csrfToken, true, secure = true, maxAge = 60 * 60 * 24)
return csrfToken
}
- CookieUtils.kt
fun addCookie(
response: HttpServletResponse,
name: String?,
value: String?,
httpOnly: Boolean,
secure: Boolean,
maxAge: Int
) {
val cookie = Cookie(name, value)
cookie.path = "/"
cookie.isHttpOnly = httpOnly
cookie.secure = secure
cookie.maxAge = maxAge
cookie.setAttribute("SameSite", "None")
response.addCookie(cookie)
}
여기서 addCookie 함수는 HttpServletResponse 객체를 사용하여 클라이언트(웹 브라우저)에 쿠키를 전송한다.
그리고 프론트에서 반환된 CsrfToken을 사용자가 로그인 시에 X-CSRF-TOKEN이라는 이름으로 Cookie에 저장한다.
return axios.get(`${apiBaseUrl}/csrf-token`, {
withCredentials: true,
});
})
.then((response) => {
const csrfToken = response.data.data;
Cookies.set("X-CSRF-TOKEN", csrfToken);
})
이후 api 요청마다 CSRF 토큰을 헤더에 담아 보낸다.
서버는 필터에서 헤더에 담긴 CSRF토큰을 꺼내서 확인한다.
이때, GET요청을 제외한 POST, PUT, DELETE 요청에서만 필터 처리를 하여 토큰을 검사한다.
- CsrfTokenFilter.kt
class CsrfTokenFilter : GenericFilterBean() {
override fun doFilter(servletRequest: ServletRequest?, servletResponse: ServletResponse?, chain: FilterChain) {
val request = servletRequest as? HttpServletRequest ?: return
val response = servletResponse as? HttpServletResponse ?: return
if (request.method in setOf("POST", "PUT", "DELETE")) {
val requestHeaderCsrfToken = request.getHeader("X-CSRF-TOKEN")
val cookieCsrfToken = CookieUtils.getCookie(request, "CSRF-TOKEN")?.value
if (requestHeaderCsrfToken == null && cookieCsrfToken == null) {
throw RuntimeException("CSRF 토큰이 누락되었습니다.")
}
if (requestHeaderCsrfToken != cookieCsrfToken) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "CsrfToken이 일치하지 않습니다.")
return
}
}
chain.doFilter(request, response)
}
}
헤더에 담긴 X-CSRF-TOKEN과 요청에 담겨온 CSRF-TOKEN을 비교한다.
비교하여 아무 문제없으면 다음 필터로 넘긴다.
위의 코드들은 CodeChaining 서비스에서 사용되었다.
GitHub: https://github.com/Code-Chaining
'Spring' 카테고리의 다른 글
NGINX로 Http, Https 적용하기 (0) | 2024.07.02 |
---|---|
빌더 패턴(Builder Pattern)은 무엇인가 (0) | 2023.11.29 |
경로 매개변수(@PathVariable)와 쿼리 매개변수(@RequestParam) (0) | 2023.08.03 |
@ManyToMany를 사용하지 말자! (0) | 2023.07.26 |
intelliJ에서 Api통신을 해보자! (0) | 2023.07.20 |