- 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 에서는 페이지 로드 시 토큰 값을 백엔드 측으로 보내 관리자인지 검증하게끔 하는 코드를 추가하여

관리자 경로의 일반 유저의 접근을 차단하는 방법의 구현이 필요하다.

 

 

 

728x90

 

 

 

728x90

+ Recent posts