이번 강의에서 다룬 로그인 페이지의 구조와 작동 방식을 살펴보겠습니다.
단순히 눈에 보이는 화면을 넘어, 사용자가 ID와 비밀번호를 입력하고 서버에 전송할 때 백엔드에서 어떤 과정이 일어나는지
그리고 이 과정에서 HTTP 프로토콜, 보안, 백엔드 아키텍처와 같은 중요한 개념들이 어떻게 연결되는지 정리합니다.
핵심 개념 요약
로그인 페이지의 작동을 이해하기 위해 다음 핵심 개념들을 정리했습니다.
| 개념 | 정의 | 역할 | 키워드 |
| HTML Form | 사용자로부터 데이터를 입력받아 서버로 전송하는 데 사용되는 웹 양식. | ID/PW 입력 필드, 로그인 버튼 등 사용자 인터페이스를 제공하고 입력된 데이터를 묶어 서버로 보냅니다. | <form>, <input>, method, action |
| HTTP POST | 클라이언트가 서버로 데이터를 전송하기 위한 HTTP 요청 메서드. | 로그인 정보를 URL에 노출하지 않고 안전하게 서버로 전송합니다. | HTTP Request, Payload, Body |
| CSRF 보호 | 웹 애플리케이션 취약점인 CSRF(Cross-Site Request Forgery) 공격을 방지하는 기술. | 사용자가 의도치 않은 요청을 보내는 것을 막아 보안을 강화합니다. 숨겨진 토큰(_csrf)으로 유효성을 검증합니다. | _csrf_parameter, X-CSRF-TOKEN, Session fixation |
| JavaScript/jQuery | 웹 페이지에서 동적인 동작을 구현하고 사용자 인터랙션을 처리하는 스크립트 언어/라이브러리. | 사용자 입력 유효성 검사, 비동기 데이터 전송(AJAX), UI 반응성 향상 등 클라이언트 로직을 담당합니다. | Validation, AJAX, DOM Manipulation |
관련 개념 및 확장 설명
로그인 페이지는 단순히 프런트엔드와 백엔드의 상호작용을 넘어, 다양한 기술적, 보안적 고려사항이 복합적으로 적용됩니다.
1. 웹 요청/응답 흐름: 로그인 과정
사용자가 로그인 페이지에서 정보를 입력하고 제출할 때, 다음과 같은 흐름으로 웹 애플리케이션이 동작합니다.
- 클라이언트 (웹 브라우저):
- HTML 폼을 통해 사용자 ID와 비밀번호를 입력받습니다.
- JavaScript를 사용하여 기본적인 유효성 검사(예: 필드 누락 여부)를 수행할 수 있습니다.
- POST 메서드로 로그인 요청(SecurityLoginCheck.do)을 서버에 보냅니다. 이 때, CSRF 토큰도 함께 전송됩니다.
- 서버 (Spring Boot 애플리케이션):
- DispatcherServlet: 모든 요청을 가장 먼저 받아서 적절한 핸들러(Controller)로 라우팅합니다.
- Controller: 로그인 요청(SecurityLoginCheck.do)을 받아 사용자 ID, 비밀번호, CSRF 토큰 등의 요청 파라미터를 처리합니다.
- 여기서 CSRF 토큰의 유효성을 검증합니다. 만약 토큰이 없거나 유효하지 않으면 요청을 거부합니다.
- Service Layer: Controller로부터 받은 로그인 정보를 바탕으로 실제 비즈니스 로직(사용자 인증)을 수행합니다.
- 예: 사용자 ID로 데이터베이스에서 사용자 정보를 조회합니다.
- 입력받은 비밀번호와 저장된(해싱된) 비밀번호를 비교하여 일치하는지 확인합니다.
- Repository Layer: 데이터베이스와 직접 통신하여 사용자 정보를 조회하거나 저장하는 역할을 합니다.
- 예: userRepository.findByUserId(userId)를 호출하여 사용자 정보를 가져옵니다.
- Authentication Manager / Provider (Spring Security): 실제 사용자 인증을 전담하는 부분입니다.
- Service Layer에서 사용자 인증에 성공하면, Authentication 객체를 생성하여 보안 컨텍스트에 저장합니다.
- 응답:
- 인증 성공 시: 주로 리다이렉트(예: 메인 페이지) 응답을 보냅니다. 이 때, 세션 ID를 포함한 쿠키를 클라이언트에 전송하여 이후 요청의 인증 상태를 유지합니다.
- 인증 실패 시: 에러 메시지와 함께 로그인 페이지로 리다이렉트하거나, HTTP 401 Unauthorized 상태 코드를 반환합니다.
2. 보안 관련 확장 개념
로그인 페이지에서 특히 중요한 보안 개념들은 다음과 같습니다.
- Cross-Site Request Forgery (CSRF): 공격자가 사용자의 세션을 가로채 사용자의 권한으로 원치 않는 요청을 서버에 보내는 공격입니다. 교재 이미지에서 _csrf 파라미터를 사용한 것은 이 공격을 방지하기 위함입니다.
- 원리: 서버는 클라이언트로부터 받은 요청의 CSRF 토큰이 유효한지 검증합니다. 유효한 토큰은 정상적인 웹 페이지에서만 생성될 수 있으므로, 외부 악성 사이트에서 보낸 위조된 요청을 차단할 수 있습니다.
- 세션 관리: 로그인에 성공하면 서버는 사용자의 세션을 생성하고, 클라이언트에게 세션 ID를 발급합니다. 클라이언트는 이후 요청에 이 세션 ID를 포함하여 보내고, 서버는 이를 통해 사용자를 식별하고 인증 상태를 유지합니다.
- 비밀번호 해싱: 데이터베이스에 비밀번호를 평문으로 저장하는 것은 매우 위험합니다. 비밀번호는 반드시 해싱(Hashing)되어 저장되어야 하며, 로그인 시에는 사용자가 입력한 비밀번호를 해싱하여 저장된 해시값과 비교해야 합니다. (예: BCrypt, Scrypt)
- HTTPS: 로그인 정보는 민감한 데이터이므로, 반드시 HTTPS(HTTP Secure)를 통해 암호화되어 전송되어야 합니다. 이는 중간자 공격(Man-in-the-Middle Attack)을 방지합니다.
코드 예시 및 적용 방식
여기서는 웹 애플리케이션의 로그인 처리 로직을 위한 프런트엔드(JavaScript)와 백엔드(Spring Boot) 코드 예시를 통해 핵심 흐름을 살펴봅니다.
코드 적용 예시: 로그인 폼 처리
1. 프런트엔드 (JavaScript를 이용한 유효성 검사 및 비동기 로그인 요청)
이 코드는 HTML 로그인 폼에서 사용자 ID와 비밀번호를 가져와 기본적인 유효성 검사를 수행하고, 문제가 없으면 비동기(AJAX) 방식으로 서버에 로그인 요청을 보냅니다.
<!-- login.html -->
<form id="loginForm" method="post" action="/comm/login/SecurityLoginCheck.do">
<div class="in-row">
<label for="userId" class="label">아이디</label>
<input type="text" name="userId" id="userId" class="form-control" placeholder="ID">
</div>
<div class="in-row">
<label for="userPwd" class="label">비밀번호</label>
<input type="password" name="userPwd" id="userPwd" class="form-control" placeholder="PASSWORD">
</div>
<!-- CSRF 토큰: Spring Security는 자동으로 삽입하거나 Thymeleaf 같은 템플릿 엔진으로 삽입 가능 -->
<input type="hidden" name="_csrf" value="YOUR_CSRF_TOKEN_HERE" id="_csrf_token">
<div class="form-btn">
<button type="submit" class="btn btn-lg btn-wide btn-primary" id="loginButton">로그인</button>
</div>
</form>
<script type="text/javascript">
$(document).ready(function() {
$('#loginForm').on('submit', function(event) {
event.preventDefault(); // 기본 폼 제출 방지
const userId = $('#userId').val();
const userPwd = $('#userPwd').val();
const csrfToken = $('#_csrf_token').val(); // 숨겨진 CSRF 토큰 값
// 1. 클라이언트 측 유효성 검사 (기본적인 내용만)
if (!userId) {
alert('아이디를 입력해주세요.');
$('#userId').focus();
return;
}
if (!userPwd) {
alert('비밀번호를 입력해주세요.');
$('#userPwd').focus();
return;
}
// 2. AJAX를 이용한 로그인 요청 (jQuery 사용)
$.ajax({
url: $(this).attr('action'), // 폼의 action 속성 사용
type: $(this).attr('method'), // 폼의 method 속성 사용 (POST)
data: {
userId: userId,
userPwd: userPwd,
_csrf: csrfToken // CSRF 토큰 포함
},
success: function(response) {
// 성공 시 서버에서 받은 응답 처리 (예: 리다이렉트)
// 실제 환경에서는 서버가 리다이렉트 URL을 응답하거나,
// HTTP 200 OK와 함께 클라이언트에서 리다이렉트하도록 처리할 수 있습니다.
// 이 예시에서는 성공 시 메인 페이지로 이동한다고 가정합니다.
window.location.href = '/edu/main/index.do';
},
error: function(xhr, status, error) {
// 실패 시 에러 메시지 처리
if (xhr.status === 401) {
alert('아이디 또는 비밀번호가 올바르지 않습니다.');
} else if (xhr.status === 403) { // CSRF 토큰 오류 등
alert('보안 오류가 발생했습니다. 다시 시도해주세요.');
} else {
alert('로그인 처리 중 오류가 발생했습니다: ' + xhr.responseText);
}
}
});
});
});
</script>
이 코드는 사용자가 입력한 로그인 정보를 검증하고, 페이지 새로고침 없이 서버와 통신하여 로그인 처리를 시도합니다. 비동기 요청을 통해 사용자 경험을 개선하고, CSRF 토큰을 포함하여 보안을 강화합니다.
2. 백엔드 (Spring Boot를 이용한 로그인 요청 처리)
이 코드는 Spring Boot 애플리케이션에서 로그인 요청을 처리하는 컨트롤러와 서비스를 보여줍니다. Spring Security를 활용하여 인증을 처리하는 간소화된 예시입니다.
// UserController.java
@Controller
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/login")
public String loginPage() {
return "login"; // login.html (또는 login.jsp) 템플릿 반환
}
// Spring Security가 이 엔드포인트를 가로채서 인증을 처리합니다.
// 따라서 실제로는 이 메서드가 직접 호출되지 않을 수 있습니다.
// 여기서는 예시를 위해 직접 인증 로직을 포함하는 경우를 보여줍니다.
@PostMapping("/comm/login/SecurityLoginCheck.do")
public String login(@RequestParam String userId,
@RequestParam String userPwd,
Model model, HttpServletResponse response) {
try {
// 실제 인증은 Spring Security가 담당하지만,
// 여기서는 서비스 계층의 인증 메서드를 호출하는 것으로 가정합니다.
// Spring Security 사용 시에는 CustomUserDetailsService 등을 구현하게 됩니다.
boolean isAuthenticated = userService.authenticate(userId, userPwd);
if (isAuthenticated) {
// 인증 성공 시 세션 생성 및 리다이렉트 (Spring Security가 자동으로 처리)
// 예시: 간단한 세션 속성 추가
// HttpSession session = request.getSession();
// session.setAttribute("loggedInUser", userId);
return "redirect:/edu/main/index.do";
} else {
model.addAttribute("loginError", "아이디 또는 비밀번호가 올바르지 않습니다.");
return "login"; // 로그인 페이지로 돌아가 에러 메시지 표시
}
} catch (Exception e) {
model.addAttribute("loginError", "로그인 처리 중 오류가 발생했습니다.");
return "login";
}
}
}
// UserService.java
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder; // 비밀번호 해싱을 위한 인터페이스
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public boolean authenticate(String userId, String rawPassword) {
User user = userRepository.findByUserId(userId);
if (user != null) {
// 저장된 해싱된 비밀번호와 입력된 비밀번호를 비교
return passwordEncoder.matches(rawPassword, user.getHashedPassword());
}
return false;
}
}
// UserRepository.java (인터페이스)
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUserId(String userId);
}
// User.java (엔티티)
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String userId;
private String hashedPassword; // 해싱된 비밀번호
// ... 다른 필드들
// Getter, Setter, Constructors
public String getUserId() { return userId; }
public String getHashedPassword() { return hashedPassword; }
public void setUserId(String userId) { this.userId = userId; }
public void setHashedPassword(String hashedPassword) { this.hashedPassword = hashedPassword; }
}
이 백엔드 코드는 UserController가 /comm/login/SecurityLoginCheck.do 경로의 POST 요청을 받고, UserService를 통해 사용자 인증 로직을 수행합니다. UserRepository는 데이터베이스에서 사용자 정보를 조회하며, PasswordEncoder를 사용하여 비밀번호의 안전한 비교를 보장합니다. 이는 계층형 아키텍처와 의존성 주입(DI)의 좋은 예시이며, 실제 프로젝트에서는 Spring Security와 같은 프레임워크를 활용하여 인증/인가 과정을 더욱 견고하게 구현합니다.
주의사항 및 자주 발생하는 오류
로그인 기능을 구현하고 운영할 때 발생할 수 있는 주요 문제점과 그에 대한 대응 방안입니다.
- 보안 취약점:
- SQL Injection: 사용자 입력값을 제대로 검증하지 않고 쿼리에 직접 사용하면 발생합니다. Prepared Statement를 사용하거나 ORM(Object-Relational Mapping)을 통해 방지해야 합니다.
- XSS (Cross-Site Scripting): 사용자 입력값이 HTML에 포함될 때 스크립트가 실행될 수 있습니다. 입력값을 HTML 엔티티로 변환하여(Sanitization) 방지합니다.
- 비밀번호 평문 저장: 데이터베이스에 비밀번호를 암호화하지 않고 그대로 저장하면 유출 시 큰 문제가 됩니다. 반드시 강력한 해싱 알고리즘(예: BCrypt)을 사용하여 비밀번호를 해싱하여 저장해야 합니다.
- 세션 관리 문제:
- 세션 하이재킹: 세션 ID가 노출되어 공격자가 사용자의 세션을 가로챌 수 있습니다. 세션 ID를 쿠키에 저장할 때는 HttpOnly, Secure 플래그를 사용하고, 일정 시간 후 세션을 만료시키는 정책을 적용해야 합니다.
- 세션 고정 공격 (Session Fixation): 공격자가 미리 생성된 세션 ID를 사용자에게 강제로 할당한 후, 사용자가 해당 세션으로 로그인하면 공격자가 세션을 가로채는 공격입니다. 로그인 시 세션 ID를 새로 발급(또는 변경)하여 방지할 수 있습니다. Spring Security는 기본적으로 이를 처리합니다.
- 유효성 검사 미흡:
- 클라이언트 측 유효성 검사만으로는 부족합니다. 클라이언트 스크립트는 우회될 수 있으므로, 반드시 서버 측에서도 모든 사용자 입력에 대한 유효성 검사를 수행해야 합니다.
- 오류 메시지 노출:
- 로그인 실패 시 "아이디가 존재하지 않습니다." 또는 "비밀번호가 틀렸습니다."와 같이 구체적인 오류 메시지를 제공하는 것은 사용자 계정 유추를 쉽게 만들어 보안상 위험할 수 있습니다. "아이디 또는 비밀번호가 올바르지 않습니다."와 같이 일반적인 메시지를 사용하는 것이 좋습니다.
'프로젝트 > 웹 성능 테스트' 카테고리의 다른 글
| SSM 기반 무중단 CI/CD (0) | 2025.12.14 |
|---|---|
| 장애 대응을 위한 메트릭 모니터링 단 구축(1. Actuator + Micrometer) (0) | 2025.11.28 |
| 롱폴링 비동기 완료 (0) | 2025.11.27 |
| 비동기 통신 방식 비교와 A/B 테스트 설계 (0) | 2025.11.09 |
| Nginx 도입 2편: 적용 (0) | 2025.11.09 |