-
Notifications
You must be signed in to change notification settings - Fork 24
3주차 미션 / 서버 1조 이준용 #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
85090b6
e2d878b
1832434
0fc81c0
75de669
023ce0f
dc5f1aa
787e13b
4c5fed3
3cc2095
ac4f4ee
48595e2
88ec8f9
9ea04cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package controller; | ||
|
|
||
| import http.HttpRequest; | ||
| import http.HttpResponse; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| public interface Controller { | ||
| void execute(HttpRequest request, HttpResponse response) throws IOException; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package controller; | ||
|
|
||
| import http.HttpRequest; | ||
| import http.HttpResponse; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
|
|
||
| public class ForwardController implements Controller { | ||
| private static final Logger log = Logger.getLogger(ForwardController.class.getName()); | ||
|
|
||
| @Override | ||
| public void execute(HttpRequest request, HttpResponse response) throws IOException { | ||
| String path = request.getPath(); | ||
| if (path == null || path.isEmpty() || "/".equals(path)) { | ||
| path = "index.html"; | ||
| } else if (path.startsWith("/")) { | ||
| path = path.substring(1); | ||
| } | ||
| log.log(Level.INFO, "Forward static resource path={0}", path); | ||
| response.forward(path); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package controller; | ||
|
|
||
| import http.HttpRequest; | ||
| import http.HttpResponse; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| public class HomeController implements Controller { | ||
| @Override | ||
| public void execute(HttpRequest request, HttpResponse response) throws IOException { | ||
| response.forward("index.html"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package controller; | ||
|
|
||
| import http.CookieName; | ||
| import http.HttpRequest; | ||
| import http.HttpResponse; | ||
| import webserver.UrlPath; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| public class ListController implements Controller { | ||
| @Override | ||
| public void execute(HttpRequest request, HttpResponse response) throws IOException { | ||
| String logined = request.getCookie(CookieName.LOGINED.key()); | ||
| if (!"true".equalsIgnoreCase(logined)) { | ||
| response.forward(UrlPath.USER_LOGIN_PAGE.value()); | ||
| return; | ||
| } | ||
|
|
||
| response.forward(UrlPath.USER_LIST_HTML.value()); | ||
|
|
||
| // String path = request.getPath(); | ||
| // if (path == null) path = "/user/list"; | ||
| // int q = path.indexOf('?'); | ||
| // if (q >= 0) path = path.substring(0, q); | ||
| // String normalized = path.startsWith("/") ? path.substring(1) : path; | ||
| // | ||
| // // 3) 표준 경로만 서비스 (user/list, user/list.html, user/list/ 허용) | ||
| // if (normalized.equals("user/list") || normalized.equals("user/list.html") || normalized.equals("user/list/")) { | ||
| // response.forward("user/list.html"); // 실제 파일로 고정 | ||
| // return; | ||
| // } | ||
| // | ||
| // // 4) 과거/실수 경로 보정: /user/userList* → /user/list 로 리다이렉트 | ||
| // if (normalized.equals("user/userList") || normalized.equals("user/userList.html") || normalized.equals("user/userList/")) { | ||
| // response.response302Header("/user/list.html"); | ||
| // return; | ||
| // } | ||
|
|
||
| // if (normalized.startsWith("user/userList")) { | ||
| // response.forward("user/list.html"); | ||
| // return; | ||
| // } | ||
|
|
||
| // 4) 그 외는 필요에 따라 404 또는 다른 처리 | ||
| response.send404("/user/userList"); | ||
|
|
||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package controller; | ||
|
|
||
| import db.MemoryUserRepository; | ||
| import db.Repository; | ||
| import http.*; | ||
| import model.User; | ||
| import model.UserQueryKey; | ||
| import webserver.UrlPath; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
|
|
||
| public class LoginController implements Controller { | ||
| private static final Logger log = Logger.getLogger(LoginController.class.getName()); | ||
| private final Repository repository = MemoryUserRepository.getInstance(); | ||
|
|
||
| @Override | ||
| public void execute(HttpRequest request, HttpResponse response) throws IOException { | ||
| if (request.getMethod() == HttpMethod.GET) { | ||
| response.forward("user/login.html"); | ||
| return; | ||
| } | ||
|
|
||
| if (request.getMethod() != HttpMethod.POST) { | ||
| response.response302Header(UrlPath.USER_LOGIN_FAILED.value()); | ||
| return; | ||
| } | ||
|
|
||
| String userId = request.getParameter(UserQueryKey.USER_ID.key()); | ||
| User user = repository.findUserById(userId); | ||
| log.log(Level.INFO, "Login attempt userId={0}", userId); | ||
| if (user != null && user.getPassword().equals(request.getParameter(UserQueryKey.PASSWORD.key()))) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain평문 비밀번호 비교는 보안 위험입니다. 현재 구현은 평문으로 비밀번호를 비교합니다. 실제 프로덕션 환경에서는 BCrypt, Argon2 등의 해시 알고리즘을 사용하여 비밀번호를 안전하게 비교해야 합니다. 학습/데모 목적이라면 현재 구현이 적절할 수 있지만, 실제 배포 환경에서는 비밀번호 해싱을 반드시 구현해야 합니다. 🏁 Script executed: #!/bin/bash
# Search for BCrypt usage in Java files
rg -n --type=java 'BCrypt' -g '*.java'
# Search for PasswordEncoder usage in Java files
rg -n --type=java 'PasswordEncoder' -g '*.java'
# Display imports and password-related declarations in LoginController.java
sed -n '1,80p' src/main/java/controller/LoginController.javaLength of output: 1638 비밀번호 평문 비교 제거 및 해시 알고리즘 적용 🤖 Prompt for AI Agents |
||
| response.addHeader(HttpHeader.SET_COOKIE, CookieName.LOGINED.key() + "=true; Path=/; HttpOnly; SameSite=Lax"); | ||
|
|
||
| log.log(Level.INFO, "Login success userId={0}", userId); | ||
| response.response302Header(UrlPath.INDEX.value()); | ||
| } else { | ||
| log.log(Level.INFO, "Login failed userId={0}", userId); | ||
| response.response302Header(UrlPath.USER_LOGIN_FAILED.value()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,67 @@ | ||||||||||||||
| package controller; | ||||||||||||||
|
|
||||||||||||||
| import db.MemoryUserRepository; | ||||||||||||||
| import db.Repository; | ||||||||||||||
| import http.HttpMethod; | ||||||||||||||
| import http.HttpRequest; | ||||||||||||||
| import http.HttpResponse; | ||||||||||||||
| import model.User; | ||||||||||||||
| import model.UserQueryKey; | ||||||||||||||
| import webserver.UrlPath; | ||||||||||||||
|
|
||||||||||||||
| import java.io.IOException; | ||||||||||||||
| import java.util.logging.Level; | ||||||||||||||
| import java.util.logging.Logger; | ||||||||||||||
|
|
||||||||||||||
| public class SignUpController implements Controller { | ||||||||||||||
| private static final Logger log = Logger.getLogger(SignUpController.class.getName()); | ||||||||||||||
| private final Repository repository = MemoryUserRepository.getInstance(); | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MemoryUserRepository는 스레드 안전하지 않습니다(싱글턴/저장 Map 모두). 요청은 멀티스레드로 처리되므로, 현재 HashMap과 비동기 안전한 getInstance 구현은 경쟁 상태, 데이터 손상 위험이 있습니다. ConcurrentHashMap과 안전한 싱글턴으로 교체하세요. 아래와 같이 리팩터링(별도 파일 수정): // in MemoryUserRepository.java
import java.util.concurrent.ConcurrentHashMap;
public class MemoryUserRepository implements Repository {
private final Map<String, User> users = new ConcurrentHashMap<>();
private static final MemoryUserRepository INSTANCE = new MemoryUserRepository();
private MemoryUserRepository() {}
public static MemoryUserRepository getInstance() {
return INSTANCE;
}
public void addUser(User user) { users.put(user.getUserId(), user); }
// 기타 메서드 동일
}🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| @Override | ||||||||||||||
| public void execute(HttpRequest request, HttpResponse response) throws IOException { | ||||||||||||||
| HttpMethod method = request.getMethod(); | ||||||||||||||
|
|
||||||||||||||
| // Requirement 2 : GET 방식으로 회원가입하기 | ||||||||||||||
| // if (method != HttpMethod.GET && method != HttpMethod.POST) { | ||||||||||||||
| // response.send404(request.getPath()); | ||||||||||||||
| // return; | ||||||||||||||
| // } | ||||||||||||||
|
Comment on lines
+24
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 주석 처리된 코드를 제거하세요. 더 이상 사용하지 않는 코드는 버전 관리 시스템에 기록되어 있으므로 삭제하는 것이 좋습니다. 코드베이스를 깔끔하게 유지할 수 있습니다. 다음과 같이 주석 처리된 코드를 제거하세요: - // Requirement 2 : GET 방식으로 회원가입하기
-// if (method != HttpMethod.GET && method != HttpMethod.POST) {
-// response.send404(request.getPath());
-// return;
-// }
-
// Requirement 3 : POST 방식으로 회원가입하기📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| // Requirement 3 : POST 방식으로 회원가입하기 | ||||||||||||||
| if (method == HttpMethod.GET) { | ||||||||||||||
| // response.forward("user/form.html"); | ||||||||||||||
| response.response302Header("/index.html"); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if (method != HttpMethod.POST) { | ||||||||||||||
| response.send404(request.getPath()); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| String userId = normalized(request, UserQueryKey.USER_ID); | ||||||||||||||
| String password = normalized(request, UserQueryKey.PASSWORD); | ||||||||||||||
| String name = normalized(request, UserQueryKey.NAME); | ||||||||||||||
| String email = normalized(request, UserQueryKey.EMAIL); | ||||||||||||||
|
|
||||||||||||||
| if (userId.isEmpty() || password.isEmpty() || name.isEmpty() || email.isEmpty()) { | ||||||||||||||
| log.log(Level.WARNING, "Missing sign-up parameters method={0} userId={1}", new Object[]{method, userId}); | ||||||||||||||
| response.response302Header(UrlPath.USER_SIGNUP_FORM.value()); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| User user = new User( | ||||||||||||||
| userId, | ||||||||||||||
| password, | ||||||||||||||
| name, | ||||||||||||||
| email); | ||||||||||||||
| repository.addUser(user); | ||||||||||||||
| log.log(Level.INFO, "User signup method={0} userId={1}", new Object[]{method, user.getUserId()}); | ||||||||||||||
| response.response302Header(UrlPath.INDEX.value()); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+20
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain가입은 POST 전용으로 제한하고, GET은 폼 포워딩으로 분리하세요. 중복 가입도 막아야 합니다. 현재 GET에서도 모든 파라미터가 채워지면 가입이 진행됩니다. 이는 보안/정합성 측면에서 위험합니다. 가입은 POST에서만 처리하고, GET은 폼을 포워딩하세요. 또한 동일 userId 덮어쓰기를 방지하기 위해 중복 체크가 필요합니다. 아래와 같이 분기 구조를 단순화해 주세요(중복 체크는 Repository 인터페이스에 findUserById가 있다면 추가). - public void execute(HttpRequest request, HttpResponse response) throws IOException {
- HttpMethod method = request.getMethod();
- if (method != HttpMethod.GET && method != HttpMethod.POST) {
- response.send404(request.getPath());
- return;
- }
-
- String userId = normalized(request, UserQueryKey.USER_ID);
- String password = normalized(request, UserQueryKey.PASSWORD);
- String name = normalized(request, UserQueryKey.NAME);
- String email = normalized(request, UserQueryKey.EMAIL);
-
- if (userId.isEmpty() || password.isEmpty() || name.isEmpty() || email.isEmpty()) {
- log.log(Level.WARNING, "Missing sign-up parameters method={0} userId={1}", new Object[]{method, userId});
- response.response302Header(UrlPath.USER_SIGNUP_FORM.value());
- return;
- }
-
- User user = new User(
- userId,
- password,
- name,
- email);
- repository.addUser(user);
- log.log(Level.INFO, "User signup method={0} userId={1}", new Object[]{method, user.getUserId()});
- response.response302Header(UrlPath.INDEX.value());
- }
+ public void execute(HttpRequest request, HttpResponse response) throws IOException {
+ HttpMethod method = request.getMethod();
+
+ // GET: 폼만 제공
+ if (method == HttpMethod.GET) {
+ response.forward(UrlPath.USER_SIGNUP_FORM.value());
+ return;
+ }
+ // POST 이외: 거부
+ if (method != HttpMethod.POST) {
+ response.send404(request.getPath());
+ return;
+ }
+
+ String userId = normalized(request, UserQueryKey.USER_ID);
+ String password = normalized(request, UserQueryKey.PASSWORD);
+ String name = normalized(request, UserQueryKey.NAME);
+ String email = normalized(request, UserQueryKey.EMAIL);
+
+ if (userId.isEmpty() || password.isEmpty() || name.isEmpty() || email.isEmpty()) {
+ log.log(Level.WARNING, "Missing sign-up parameters method={0} userId={1}", new Object[]{method, userId});
+ response.response302Header(UrlPath.USER_SIGNUP_FORM.value());
+ return;
+ }
+
+ // TODO: Repository에 findUserById가 있다면 중복 체크 추가
+ // if (repository.findUserById(userId) != null) {
+ // response.response302Header(UrlPath.USER_SIGNUP_FORM.value());
+ // return;
+ // }
+
+ User user = new User(userId, password, name, email);
+ repository.addUser(user);
+ log.log(Level.INFO, "User signup method={0} userId={1}", new Object[]{method, user.getUserId()});
+ response.response302Header(UrlPath.INDEX.value());
+ }Repository에 중복 조회 메서드 존재 여부 확인용 스크립트: 🏁 Script executed: #!/bin/bash
# Repository 인터페이스/구현에 사용자 조회 메서드가 있는지 확인
rg -nP -C2 '(interface|class)\s+Repository\b' | sed -n '1,120p'
rg -nP -C3 'class\s+MemoryUserRepository\b'
rg -nP -C3 'findUserById\s*\('Length of output: 2375 GET 요청은 폼 렌더링으로만 처리하고, POST에서만 가입 로직을 수행하세요 🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| private String normalized(HttpRequest request, UserQueryKey key) { | ||||||||||||||
| String value = request.getParameter(key.key()); | ||||||||||||||
| return value == null ? "" : value.trim(); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package http; | ||
|
|
||
| public enum CookieName { | ||
| LOGINED("logined"); | ||
| private final String key; | ||
| CookieName(String key){ this.key = key; } | ||
| public String key(){ return key; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package http; | ||
| import java.util.Set; | ||
|
|
||
| public enum FormatType { | ||
| HTML("text/html; charset=utf-8", Set.of(".html", ".htm")), | ||
| CSS("text/css", Set.of(".css")), | ||
| JS("application/javascript; charset=utf-8", Set.of(".js")), | ||
| JSON("application/json; charset=utf-8", Set.of(".json")), | ||
| SVG("image/svg+xml", Set.of(".svg")), | ||
| PNG("image/png", Set.of(".png")), | ||
| JPEG("image/jpeg", Set.of(".jpg", ".jpeg")), | ||
| GIF("image/gif", Set.of(".gif")), | ||
| ICO("image/x-icon", Set.of(".ico")), | ||
| OCTET("application/octet-stream",Set.of()); | ||
|
|
||
| private final String contentType; | ||
| private final Set<String> exts; | ||
|
|
||
| FormatType(String contentType, Set<String> exts) { | ||
| this.contentType = contentType; | ||
| this.exts = exts; | ||
| } | ||
| public String contentType() { return contentType; } | ||
|
|
||
| public static String fromFilename(String name) { | ||
| String n = name.toLowerCase(); | ||
| for (FormatType mt : values()) { | ||
| for (String ext : mt.exts) { | ||
| if (n.endsWith(ext)) return mt.contentType; | ||
| } | ||
| } | ||
| return OCTET.contentType; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package http; | ||
|
|
||
| public enum HttpHeader { | ||
| CONTENT_TYPE("Content-Type"), | ||
| CONTENT_LENGTH("Content-Length"), | ||
| LOCATION("Location"), | ||
| SET_COOKIE("Set-Cookie"), | ||
| COOKIE("Cookie"); | ||
|
|
||
| private final String value; | ||
|
|
||
| HttpHeader(String value) { | ||
| this.value = value; | ||
| } | ||
|
|
||
| public String value() { | ||
| return value; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package http; | ||
|
|
||
| public enum HttpMethod { | ||
| GET, | ||
| POST; | ||
|
|
||
| public boolean isEqual(HttpMethod method) { | ||
| return this == method; | ||
| } | ||
|
|
||
| public boolean isEqual(String rawMethod) { | ||
| return rawMethod != null && name().equalsIgnoreCase(rawMethod); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
입력 검증 및 null 안전성 검증이 필요합니다.
request.getParameter()가 null을 반환할 수 있으므로, Line 34와 Line 36에서 NPE가 발생할 수 있습니다. userId와 password에 대한 null 체크를 추가하세요.다음 diff를 적용하여 null 체크를 추가하세요:
📝 Committable suggestion
🤖 Prompt for AI Agents