(25.06.02) Thymleaf를 통한 회원가입/로그인 FE 구현
(25.05.30) ToyTalk WebSocket을 활용한 Chat 기능 구현 (Spring Boot
• 키덜트 정보를 플랫폼 구애없이 대화방에서 나눌수 있는 특화된 채팅 플랫폼의 니즈• WebSocket 프레임 구현 서비스 구축기획목적">키덜트 사용자들이 공통 주제로 자유롭게 대" data-og-host="andr
andrew75313.tistory.com
이전에 진행했던 WebSocket을 통한 대화방을 작동시키기 위해서는
사용자들이 인증받은 사용자 -> 즉, 회원가입과 로그인을 통해 JWT 발급이 되어 인증될 수 있는 사용자 여야했다.
따라서,
채팅방 FE 단을 구현하기 위해서는 회원가입, 로그인 FE 구현을 통해서 사용자가 등록되어야해, 작성을 했다.
ToyTalk 의 SSR(Server Side Rendering) 을 위한 Thymleaf 사용
- Thymeleaf는 HTML 템플릿 엔진으로, ToyTalk의 Spring MVC구조와 빠르게 통합되며 서버사이드 렌더링에 적합
- 별도의 React 등 프론트엔드 프레임워크 없이도 전체 페이지를 서버에서 렌더링하여 사용자에게 전달 용이
- 추후 Docker를 통해 간단하게 배포하고자 했으며, 프론트엔드와 백엔드가 통합된 SSR 구조가 배포 및 유지보수 측면에서 효율적
- 로그인, 회원가입 등 인증 처리가 서버에서 이루어지므로 쿠키 기반 JWT 인증과도 자연스럽게 연동 가능
- JWT 인증을 쿠키 기반으로 구성할 때, SSR 구조가 인증 흐름 관리에 더 적합하다고 판단
- Docker 배포를 고려한 구조 설계
- Spring Boot + Thymeleaf 구조는 FE/BE가 통합된 형태로, Docker 이미지로 쉽게 컨테이너화로 간단한 프로젝트 구현 가능
FORM을 통한 회원가입 FE 구현
- Controller에서 기존의 ResponseEntity를 통해 JSON 타입으로 응답했던 것을 Model 객체로 데이터 → HTML 템플릿으로 전달해서 랜더링 할 수 있도록 변경
@Controller
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {
...
@GetMapping("/signup")
public String signupPage() {
return "signup";
}
@PostMapping("/signup")
public String signup(@Valid SignupRequestDTO requestDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes,
Model model) {
if (bindingResult.hasErrors()) {
model.addAttribute("errorMessage", bindingResult.getAllErrors().get(0).getDefaultMessage());
return "signup";
}
try {
userService.createUser(requestDTO);
} catch (Exception e) {
model.addAttribute("errorMessage", e.getMessage());
return "signup";
}
redirectAttributes.addFlashAttribute("successMessage", "회원가입이 완료되었습니다. 로그인해주세요.");
return "redirect:/api/users/login";
}
- model을 통한 FORM구조를 사용하기 때문에 RestController, @RequestBody 모두 사용 할 수 없음
- model.addAttribute를 통해, 에러가 발생하더라도 Console로 찍히게 하는 것 외에, 알럿으로 사용자에게 보여질 수 있도록 모델로 전달
- 완료가 되었을 경우 login.html 페이지인 /api/users/login 로 redirect할 수 있도록 구현
@GetMapping("/signup")
public String signupPage() {
return "signup";
}
- 특정 엔드포인트를 요청하면 signup.html이 보여질 수 있도록 Controller를 추가해야함
- SecurityConfig에서 해당 접근은 회원가입이기 때문에 Authenticated 설정을 추가로 진행
signup.html
<body>
<div id="login-form">
<div id="login-title">ToyTalk 회원가입</div>
<form action="/api/users/signup" method="post">
<div class="login-id-label">아이디</div>
<input type="text" name="username" placeholder="USERNAME" class="login-input-box">
<div class="login-id-label">비밀번호</div>
<input type="password" name="password" placeholder="PASSWORD" class="login-input-box">
<div class="login-id-label">이메일</div>
<input type="text" name="email" placeholder="E-MAIL" class="login-input-box">
<button id="login-id-submit">회원 가입</button>
</form>
</div>
<!-- 에러 메시지 알림 -->
<script th:if="${errorMessage}">
alert('[[${errorMessage}]]');
</script>
- SignupRequestDTO로 요청하기 윟새 해당 form을 작성해서 회원가입 요청
- 위에서 언급했듯이 에러메시지 알럿 처리
FORM+AJAX 를 통한 일반 회원가입 FE 구현
- 폼 로그인으로 요청을 했지만, 받아오는 내용은 Spring Security가 받아오면서 Header에 Access Token과 Refresh Token을 넣어서 JSON으로 응답하므로 AJAX로 응답을 받게 됨
- FORM은 데이터를 전달하기 때문에, JWT 가 쿠키로 전달되지 않는 이상 헤더로 요청할 수 없음
참고) Spring Security의 AuthenticationSuccessHandler
- HttpServletResponse을 통해서 JWT를 전달하고 있음
...
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, accessToken);
response.addHeader(JwtUtil.REFRESHTOKEN_HEADER, refreshToken);
response.setContentType("application/json;charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(
Map.of("statusCode", 200, "msg", "로그인 성공")
);
response.getWriter().write(json);
JWT 추출, 저장 Script
$(document).ready(function () {
// 로그인 시 응답 헤더에서 토큰 저장
$(document).ajaxSuccess(function (event, xhr, settings) {
const accessToken = xhr.getResponseHeader('Authorization');
const refreshToken = xhr.getResponseHeader('refreshToken');
if (accessToken) {
localStorage.setItem('accessToken', accessToken);
}
if (refreshToken) {
localStorage.setItem('refreshToken', refreshToken);
}
});
// 모든 요청에 accessToken 자동 첨부
$.ajaxSetup({
beforeSend: function (xhr, settings) {
const excludedUrls = [
'/api/login',
'/api/users/signup',
'/', // 랜딩 페이지
'/api/logout' // 필요 시 로그아웃도 제외 가능
];
const isExcluded = excludedUrls.some(url => settings.url.startsWith(url));
if (!isExcluded) {
const token = localStorage.getItem('accessToken');
if (token) {
xhr.setRequestHeader('Authorization', token);
}
}
}
});
});
- Redirect 가 되어버리면 Header로 전달받은 JWT들이 전부 없어지게 되어버림
- 따라서, 로컬 저장소에 저장해 이를 사용할 수 있는 전역 스크립트를 작성
로그인 FLOW
- 클라이언트에서 /api/login으로 로그인 요청
- 서버에서 로그인 성공 시, 응답 헤더에 Authorization (Access Token)과 refreshToken을 담아서 클라이언트로 보냄
- jQuery의 ajaxSuccess 핸들러가 이 응답을 가로채서 실행
- 핸들러 내부의 코드 (xhr.getResponseHeader('Authorization'), xhr.getResponseHeader('refreshToken'))를 통해 응답 헤더에서 Access Token, Refresh Token 토큰 값을 읽어오기
- 읽어온 토큰 값을 클라이언트의 localStorage.setItem()을 사용하여 저장
- 저장된 토큰은 앞으로 ToyTalk 서버에 요청 시 사용
최종 일반 회원가입~로그인 화면 구현
Thymleaf를 사용할 수는 있었지만,
OAuth 2.0를 포함해 HttpOnly 여도 쿠키를 통한 JWT 전달 방법이 보안상 제한적일 수 있다는 추세로,
쿠키대신 Header로 발급받은 JWT를 Thymleaf 렌더링 HTML에서 가지고 오는 것을 찾고, 구현하는데 꽤 애를 먹었다.
특히, 카카오 로그인일 경우 백엔드로 구성되어있으나, FE에서 Kakao 인증화면에서 다시 ToyTalk Redirect 할 때마다, 응답받은 내용을 동일 출처 정책 (Same-Origin Policy, SOP)에 의해 전달할 수 없기 때문에.. 계속 오류가 발생해 이는 추후 메인 화면(대화방 리스트 확인 화면) 과 추가할 계획 이다.