- TIL -
구현 목표)
타임리프를 사용해서 간단하게 회원을 관리할 수 있는 백오피스 페이지를 구현해보려 한다.
타임리프(Thymeleaf)란?
타임리프는 Java 기반의 서버사이드 템플릿 엔진으로써 현재 지원이 거의 중단되어버린 JSP 의 대안으로 떠오르는 템플릿 엔진이다.
JSP 가 jar 형태의 빌드를 지원하지 않고 war 형태의 빌드만 지원했던 반면 타임리프는 jar 와 war 형태의 빌드를 모두 지원한다.
또한 서버가 없는 서버리스 형태로도 템플릿을 실행시킬 수 있다는 장점이 있다.
구현 코드)
- login.html, login.js -
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<title>Admin Login</title>
<link rel="stylesheet" th:href="@{/css/login/login.css}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="login-container">
<h2>Admin Login</h2>
<form id="login-form">
<div class="input-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="input-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
<div id="error-message" class="error-message" style="display: none;"></div>
</div>
<script th:src="@{/js/login/login.js}"></script>
</body>
</html>
login.html
( 로그인 정보를 form 태그로 담아 전송 )
document.getElementById("login-form").addEventListener("submit", async function (event) {
event.preventDefault();
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
try {
const response = await fetch("https://api.random-chat.site/admin/login", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `username=${username}&password=${password}`
});
if (response.ok) {
const token = response.headers.get("Authorization").replace("Bearer ", "");
localStorage.setItem("jwtToken", token); // JWT 저장
window.location.href = "/admin/dashboard"; // 대시보드로 리디렉션
} else {
const errorMessage = document.getElementById("error-message");
errorMessage.textContent = "Invalid username or password";
errorMessage.style.display = "block";
}
} catch (error) {
console.error("Error during login:", error);
}
});
login.js
( 로그인 정보를 담아서 성공 시 토큰을 반환받아 토큰정보를 Local Storage 에 jwtToken 키로 저장 후 /admin/dashboard 경로로 리디렉션 )
- LoginController -
package com.randomchat.main.controller.backOffice;
import com.randomchat.main.domain.users.Users;
import com.randomchat.main.jwt.JWTUtil;
import com.randomchat.main.service.backOffice.AdminLoginService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequiredArgsConstructor
public class LoginController {
private final AdminLoginService adminLoginService;
private final JWTUtil jwtUtil;
@GetMapping("/admin/login")
public String showLoginPage() {
// template 디렉토리 하위의 login.html 렌더링
return "login/login";
}
// 로그인 처리
@PostMapping("/admin/login")
public ResponseEntity<String> handleLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpServletResponse response) {
Users user = adminLoginService.adminLogin(username, password);
if(user != null) {
// jwt 생성하여 반환
String jwtToken = jwtUtil.createJwt(user.getEmail(), user.getNickname(), user.getRole().name(), user.getGender().name(), 60*60*1000L);
response.addHeader("Authorization", "Bearer " + jwtToken);
return ResponseEntity.ok("Success login");
}else {
// 로그인 실패
return ResponseEntity.status(401).body("Fail login");
}
}
}
api.random-chat.site/admin/login 경로로 접근 시 login.html 리턴
유저 정보가 있는 경우 jwtToken 정보를 생성하여 접두사 Bearer 를 붙여 Header 에 담아 리턴
- AdminLoginService -
package com.randomchat.main.service.backOffice;
import com.randomchat.main.domain.users.Role;
import com.randomchat.main.domain.users.Users;
import com.randomchat.main.repository.users.UsersRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class AdminLoginService {
private final UsersRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public Users adminLogin(String username, String password) {
Optional<Users> optionalUser = userRepository.findByEmail(username);
if(optionalUser.isEmpty()) {
System.out.println("AdminLoginService.class >>> 해당 이메일로 가입된 로그인 정보가 없음");
return null;
}else {
Users user = optionalUser.get();
if(bCryptPasswordEncoder.matches(password, user.getPassword()) && user.getRole().equals(Role.ADMIN)) {
System.out.println("AdminLoginService.class >>> admin 권한을 가진 유저(" + user.getEmail() + ") 로그인 성공");
return user;
}else {
System.out.println("AdminLoginService.class >>> 로그인 권한을 가지지 않은 유저가 로그인 시도 : " + user.getEmail());
return null;
}
}
}
}
userRepository 를 사용해 입력된 Email 정보로 가입된 유저를 먼저 탐색
> 가입된 유저가 없다면 null 반환
> 가입된 유저가 있지만 Role 값이 ADMIN 이 아닌 경우 null 반환
> 가입된 유저이면서 Role 값이 ADMIN 인 경우 유저 객체 반환
- SecurityConfig -
백오피스 로그인 페이지 경로와 css, js 파일의 경로를 렌더링하기 위해 전체 허용 설정
이후 dashboard 하위의 기능은 /admin/dashboard 하위 경로로 관리할 예정
전체 접근은 가능하게 설정하고 세부 기능 api 에서 Role 값을 기준으로 요청을 걸러낼 예정이다.
그렇다면 실제 관리자 dashboard 에서는 페이지 로드 시 토큰 값을 백엔드 측으로 보내 관리자인지 검증하게끔 하는 코드를 추가하여
관리자 경로의 일반 유저의 접근을 차단하는 방법의 구현이 필요하다.
'TIL' 카테고리의 다른 글
[TIL] 2025.01.03 - 서버 실행 시 관리자 Default 관리자 계정 생성하기 (0) | 2025.01.03 |
---|---|
[TIL] 2024.12.30 - JDBC 드라이버 표준 시간대 설정 (0) | 2024.12.30 |
[TIL] 2024.12.26 - AuthenticationManager stackOverFlow 문제 해결(feat. 커밋로그) (1) | 2024.12.26 |
[TIL] 2024.12.25 - Nginx 를 사용해 React 세부 페이지 서빙하기 (2) | 2024.12.25 |
[TIL] 2024.12.24 - nginx 포트 충돌 오류 해결 (docker, docker compose 충돌) (1) | 2024.12.24 |