최근 개발 중인 프로젝트에서 로그인 기능을 구현하게 되었다. 이 과정에서 데이터 보안의 중요성을 깊이 인식하게 되었고, 여러 보안 관련 기술을 조사하고 비교하여 적용해보기로 했다. 특히, 비밀번호 해싱, SQL 인젝션 방지, 입력 검증, 세션 관리 등의 중요성을 강조하며, 이러한 기술들을 통합적으로 적용해보았다.
데이터베이스에 비밀번호를 평문으로 저장하는 것은 매우 위험한 행위다. 이를 방지하기 위해, 현재 가장 강력한 해싱 알고리즘 중 하나인 Argon2 해싱 알고리즘을 선택했다. 이 알고리즘을 사용하여 비밀번호를 안전하게 해싱하고, 데이터베이스에 저장하는 로직을 구현했다.
<dependency>
<groupId>de.mkammerer</groupId>
<artifactId>argon2-jvm</artifactId>
<version>2.11</version>
</dependency>
<dependency>
<groupId>de.mkammerer</groupId>
<artifactId>argon2-jvm-nolibs</artifactId>
<version>2.11</version>
</dependency>
<%@ page import="de.mkammerer.argon2.Argon2, de.mkammerer.argon2.Argon2Factory" %>
<%
Argon2 argon2 = Argon2Factory.create();
String hashedPassword = argon2.hash(10, 65536, 1, password);
%>
이 코드는 사용자의 비밀번호를 받아 안전하게 해싱하여 반환한다. 해싱 과정에서 발생할 수 있는 보안 취약점을 최소화하기 위해, 비밀번호 배열을 사용한 후 즉시 메모리에서 삭제해 보안성을 강화했다.
SQL 인젝션은 해커가 애플리케이션의 보안 취약점을 이용하여 임의의 SQL 코드를 실행하는 공격 기법이다. 이를 방지하기 위해 PreparedStatement를 사용했다. 이 방법은 SQL 쿼리를 미리 컴파일하고, 입력 값을 파라미터로 전달하여 SQL 인젝션 공격을 효과적으로 차단한다.
PreparedStatement stmt = conn.prepareStatement("INSERT INTO USERS (USERNAME, PASSWORD, EMAIL) VALUES (?, ?, ?)");
stmt.setString(1, username);
stmt.setString(2, hashedPassword);
stmt.setString(3, email);
사용자 입력을 검증하지 않는 것은 다양한 보안 문제를 일으킬 수 있다. 따라서, 입력 값에 대한 검증 로직을 추가하여 사용자가 예상치 못한 값을 입력하는 것을 방지했다. 예를 들어, 사용자 이름은 알파벳과 숫자만을 포함하도록 제한했으며, 이메일 형식도 정규 표현식을 사용하여 검증했다.
if (!username.matches("^[a-zA-Z0-9_]{3,15}$") || !email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
error = "Invalid username or email format.";
}
사용자가 시스템에 성공적으로 로그인했다면, 로그인 세션을 생성하여 사용자의 로그인 상태를 유지했다. 이는 HTTP 세션을 사용하여 구현했다. 세션을 통해 사용자의 로그인 상태를 안전하게 관리하고, 사용자가 시스템을 이용하는 동안 인증 상태를 유지할 수 있게 했다.
request.getSession().setAttribute("USER", user);
모든 방법을 적용한 코드다.
<%@ page import="java.sql.*, de.mkammerer.argon2.Argon2, de.mkammerer.argon2.Argon2Factory" %>
<%@ page import="com.example.db.ConnectionProvider" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Signup</title>
</head>
<body>
<%
String error = "";
request.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
String email = request.getParameter("email");
if ("POST".equalsIgnoreCase(request.getMethod()) && username != null && password != null && email != null) {
if (!username.matches("^[a-zA-Z0-9_]{3,15}$") || !email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
error = "Invalid username or email format.";
} else {
Argon2 argon2 = Argon2Factory.create();
String hashedPassword = argon2.hash(10, 65536, 1, password);
try (Connection conn = ConnectionProvider.getConnection()) {
String sql = "INSERT INTO USERS (USERNAME, PASSWORD, EMAIL) VALUES (?, ?, ?)";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, username);
stmt.setString(2, hashedPassword);
stmt.setString(3, email);
int rowsInserted = stmt.executeUpdate();
System.out.println("Rows inserted: " + rowsInserted);
response.sendRedirect("welcome.jsp"); // Redirect to a welcome page or similar
}
} catch (SQLException e) {
e.printStackTrace();
error = "Database error: " + e.getMessage();
} finally {
argon2.wipeArray(password.toCharArray());
}
}
}
%>
<form action="signup.jsp" method="post">
username: <input type="text" id="username" name="username" placeholder="Username" required><br>
password: <input type="password" id="password" name="password" placeholder="Password" required><br>
email: <input type="email" id="email" name="email" placeholder="Email" required><br>
<button type="submit" value="signup">Sign Up</button>
</form>
<% if (!error.isEmpty()) { %>
<p>Error: <%= error %></p>
<% } %>
</body>
</html>
이번 프로젝트에서 보안을 강화한 로그인 기능을 구현하면서, 보안 기술의 중요성을 다시 한번 깊이 인식하게 되었다. 각 보안 기술을 조사하고 비교하며 적용해보는 과정에서 많은 것을 배울 수 있었다. 특히, 사용자의 데이터를 안전하게 보호하는 것이 얼마나 중요한지에 대한 인식을 더욱 확고히 할 수 있었다. 앞으로도 보안은 개발 과정에서 항상 최우선으로 고려할 사항이 될 것이다.