SrpingSecurity 실습 중 회원가입 폼에서 아무것도 입력하지 않고 submit 버튼을 누르면 @Valid를 통한 유효성 검증이 이루어지지 않고 해당 오류가 발생하였다.
단, 해당 오류는 첫번째 시도에만 발생하고 두번째 시도부터는 제대로 작동하였다.
org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "_csrf.parameterName" (template: "member/memberForm" - line 55, col 30)
SpringEL은 Spring Expression Language의 약자로 런타임 시 메서드 호출 및 기본 문자열 등의 템플릿을 제공하는 표현식이다.
템플릿 엔진인 thymeleaf 에서 사용하는${ }
변수 표현식도 SpringEL 이다.
저 오류를 읽어보면 회원가입 폼의 ${_csrf.parameterName}
부분에서 오류가 발생했다고 한다.
<div style="text-align: center">
<button type="submit" class="btn btn-primary" style="">Submit</button>
</div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
내가 추측하기엔 memberForm에서 submit 버튼을 눌렀을 때 csrf 토큰이 전달되지 않아 템플릿에서 저 오류가 발생한 것 같다.
찾아보니 csrf 토큰이 전달되지 않아 발생한 것이 맞았다.
지금까지 csrf 토큰 관련해서 아무 설정을 하지 않았었는데 갑자기 오류가 발생해서 굉장히 당황스러웠다.. 이유는 내가 항상 build.gradle에 devtools 의존성을 추가한 채로 프로젝트를 시작했기 때문이었다.
1.근본적인 해결 방법은 아니지만 build.gradle 에 springboot-devtools 의존성 추가
단, 해당 설정은 개발 환경에서만 적용되고 실제 배포 시 .jar, .war 파일에서는 적용되지 않기 때문에 사용하면 안된다.(developmentOnly: 개발 과정에만 적용되는 의존성)
build.gradle
developmentOnly 'org.springframework.boot:spring-boot-devtools'
Spring Boot DevTools 가 제공하는 기능:
- 자동 재시작: 코드 변경 시 애플리케이션을 자동으로 재시작
- 라이브 리로딩: 리소스 파일이 변경되면 자동으로 브라우저를 새로고침
- 캐싱 비활성화: 템플릿, 리소스 파일 등의 캐싱을 비활성화하여 변경 사항이 즉시 반영
DevTools는 개발 환경에서 템플릿 캐싱을 비활성화한다. 이는 템플릿이 매번 새로 컴파일되도록 하여 변경된 내용을 즉시 반영한다.
템플릿 캐싱이 비활성화되면, csrf 토큰을 포함한 동적인 콘텐츠가 매번 새로고침될 때마다 새로 렌더링되므로 csrf 토큰이 초기화되지 않을 때 발생하는 문제를 회피할 수 있다.
첫번째 시도는 오류가 발생하고 두번째 실행부터 오류가 발생하지 않는 이유는 첫번째 오류 시 csrf 토큰이 초기화됐기 때문이다. devtools 의존성 추가 시 첫번째 시도에서 토큰이 초기화 되지 않아도 템플릿 캐싱이 비활성화되어 오류가 발생하지 않았던 것이다.
2.Security Config 의 CSRF 설정 및 컨트롤러에서 모델에 CSRF 토큰 추가
직접 csrf 설정을 통해 근본적인 문제를 해결할 수 있다.
SecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
// CSRF 보호 기능을 활성화하고, CSRF 토큰을 쿠키에 저장하도록 구성
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
MemberController.java
// 요청 시 회원가입 폼 페이지로 이동
@GetMapping(value="/new")
public String memberForm(Model model, HttpServletRequest request) {
// 코드 추가
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
model.addAttribute("_csrf", csrfToken);
//
model.addAttribute("memberFormDto", new MemberFormDto());
return "member/memberForm";
}
// submit 버튼 누르면 호출되어 회원 생성
@PostMapping(value="/new")
public String memberForm(@Valid MemberFormDto memberFormDto, BindingResult bindingResult, Model model, HttpServletRequest request) {
// 코드 추가
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
model.addAttribute("_csrf", csrfToken);
//
if (bindingResult.hasErrors()) {
return "member/memberForm";
}
try {
Member member = Member.createMember(memberFormDto, passwordEncoder);
memberService.saveMember(member);
} catch (IllegalStateException e) {
model.addAttribute("errorMessage", e.getMessage());
return "member/memberForm";
}
return "redirect:/";
}
빈 유효성 검증과 회원가입 모두 첫번째 시도에 잘 실행된다.
스프링부트 프로젝트 생성 시 당연하게 아무 생각 없이 devtools 의존성을 추가했었는데 무슨 기능을 하는지도 모르고 추가했었다. 이번 오류를 통해 해당 의존성이 어떤 기능을 하는지 알게되었다. 또한 의존성 추가 시 Gradle 설정 종류가 어떤 것을 의미하는지 생각해보지 않았는데 developmentOnly
와 같이 개발 시에만 적용되는 설정이 있다는 것도 알게되었다.
해당 프로젝트를 실제로 배포할 때, devtools 의존성이 적용되지 않아 이 오류가 분명히 발생할텐데 개발 중에 문제점을 알게되어서 다행인 것 같다.