-
Notifications
You must be signed in to change notification settings - Fork 23
3주차 미션 / 서버 2조 박성근 #1
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
fba6fbd
d90c6bb
48235ce
148c4b3
71da5b0
8fc5f74
6ba9cfa
913652b
8e2a33e
626e901
6e06134
0a5ca5b
1db727e
d3b3d15
6a62fc7
c0b73a0
070db5f
1a43fbe
afc5b7b
3cbbf7a
d91bd20
81328f3
af032a3
766093a
201e646
4d4c1da
9cad2f7
b77b479
5b96d7b
25ed0b9
53f75b6
4e926f5
24aaf94
cd5e736
822abc5
2b6e292
6a36007
6db6245
75f58e3
48e86d8
0788b13
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,41 @@ | ||
| package controller; | ||
|
|
||
| import http.HttpRequest; | ||
| import http.HttpResponse; | ||
| import http.enums.RequestPath; | ||
|
|
||
| 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(); | ||
|
|
||
| // 1. 루트 경로 ("/") 처리 - 기본 페이지로 변경 | ||
| if (path.equals(RequestPath.ROOT.getValue())) { | ||
| path = RequestPath.INDEX.getValue(); | ||
| } | ||
|
|
||
| // 2. 보안 검증 - ../ 과 같은 디렉토리 traversal 공격 방지 | ||
| if (path.contains("..")) { | ||
| log.log(Level.WARNING, "Directory traversal attack detected: " + path); | ||
| response.notFound(); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| // 3. 파일 forward | ||
| response.forward(path); | ||
| log.log(Level.INFO, "Static file served successfully: " + path); | ||
|
|
||
| } catch (IOException fileException) { | ||
| // 4. 파일이 없거나 읽기 실패시 404 에러 응답 | ||
| log.log(Level.WARNING, "File not found or read error: " + path); | ||
| response.notFound(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package controller; | ||
|
|
||
| import http.HttpRequest; | ||
| import http.HttpResponse; | ||
| import http.enums.RequestPath; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
|
|
||
| public class UserListController implements Controller { | ||
| private static final Logger log = Logger.getLogger(UserListController.class.getName()); | ||
|
|
||
| @Override | ||
| public void execute(HttpRequest request, HttpResponse response) throws IOException { | ||
| // Cookie에서 로그인 상태 확인 | ||
| String logined = request.getCookie("logined"); | ||
|
|
||
| if (logined != null) { | ||
| // 로그인된 사용자: user/list.html 파일 forward | ||
| log.log(Level.INFO, "Logged in user accessing user list"); | ||
| response.forward(RequestPath.USER_LIST_HTML.getValue()); | ||
| } else { | ||
| // 비로그인 상태: 로그인 페이지로 리다이렉트 | ||
| log.log(Level.INFO, "Non-logged user redirected to login page"); | ||
| response.redirect(RequestPath.USER_LOGIN_HTML.getValue()); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package controller; | ||
|
|
||
| import db.MemoryUserRepository; | ||
| import http.HttpRequest; | ||
| import http.HttpResponse; | ||
| import http.enums.RequestPath; | ||
| import model.User; | ||
| import model.UserField; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Map; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
|
|
||
| public class UserLoginController implements Controller { | ||
| private static final Logger log = Logger.getLogger(UserLoginController.class.getName()); | ||
|
|
||
| @Override | ||
| public void execute(HttpRequest request, HttpResponse response) throws IOException { | ||
| Map<String, String> params = request.getParameters(); | ||
|
|
||
| String userId = params.get(UserField.USER_ID.getValue()); | ||
| String password = params.get(UserField.PASSWORD.getValue()); | ||
|
|
||
| if (userId != null && !userId.isEmpty()) { | ||
| log.log(Level.INFO, "Login attempt: " + userId); | ||
| } | ||
|
|
||
| // 파라미터 유효성 검사 | ||
| if (userId == null || userId.isEmpty() || password == null || password.isEmpty()) { | ||
| log.log(Level.WARNING, "Login failed: missing parameters"); | ||
| response.redirect(RequestPath.USER_LOGIN_FAILED.getValue()); | ||
| return; | ||
| } | ||
|
|
||
| // MemoryUserRepository에서 사용자 조회 | ||
| MemoryUserRepository repository = MemoryUserRepository.getInstance(); | ||
| User user = repository.findUserById(userId); | ||
|
|
||
| // 인증 검증 | ||
| if (user != null && user.getPassword().equals(password)) { | ||
| // 로그인 성공: Cookie 설정 + 메인페이지로 리다이렉트 | ||
| log.log(Level.INFO, "Login successful: " + userId); | ||
| response.redirectWithCookie(RequestPath.INDEX.getValue(), "logined=true"); | ||
| } else { | ||
| // 로그인 실패: 에러페이지로 리다이렉트 | ||
| log.log(Level.WARNING, "Login failed: " + userId); | ||
| response.redirect(RequestPath.USER_LOGIN_FAILED.getValue()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package controller; | ||
|
|
||
| import db.MemoryUserRepository; | ||
| import http.HttpRequest; | ||
| import http.HttpResponse; | ||
| import http.enums.HttpMethod; | ||
| import http.enums.RequestPath; | ||
| import model.User; | ||
| import model.UserField; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Map; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
|
|
||
| public class UserSignupController implements Controller { | ||
| private static final Logger log = Logger.getLogger(UserSignupController.class.getName()); | ||
|
|
||
| @Override | ||
| public void execute(HttpRequest request, HttpResponse response) throws IOException { | ||
| // 파라미터 추출 | ||
| Map<String, String> params = request.getParameters(); | ||
| log.log(Level.INFO, "POST Signup params: " + params); | ||
|
|
||
| // 필수 필드 검증 | ||
| String userId = params.get (UserField.USER_ID.getValue()); | ||
| String password = params.get (UserField. PASSWORD.getValue()); | ||
| String name = params.get (UserField.NAME.getValue()); | ||
| String email = params.get (UserField.EMAIL.getValue()); | ||
|
|
||
| if (userId == null || userId.trim().isEmpty() || | ||
| password == null || password. length() < 4 || | ||
| name == null || name.trim().isEmpty() || | ||
| email == null || !email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")) { | ||
| // TODO: Bad Request | ||
| return; | ||
| } | ||
|
Comment on lines
+31
to
+37
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. Bad Request에 대해 TODO로 남아있는데 어떻게 처리되는걸까요??
Author
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. 이 부분은 이전 파트장님의 코멘트를 통해 확인할 수 있습니다..
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. 이전 코멘트의 성근님과 파트장님의 의견에도 동의합니다만! 서버가 누군가(?)의 요청을 믿어야 하는 상황이 만들어지는거 같아요! 정확한 문서화를 바탕으로 한 개발을 진행하거나 특별한 사유(유저 플로우 등)에 의해서 유효성 검사 위치를 정하는게 아니라면?
ParkSeongGeun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // User 객체 생성 | ||
| User newUser = new User(userId, password, name, email); | ||
|
|
||
| // 메모리 저장소에 저장 | ||
| MemoryUserRepository repository = MemoryUserRepository.getInstance(); | ||
| repository.addUser(newUser); | ||
| log.log(Level.INFO, "New User Registered: " + newUser.getUserId()); | ||
|
|
||
| // 302 리다이렉트로 메인 페이지로 이동 | ||
| response.redirect(RequestPath.INDEX.getValue()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| package http; | ||
|
|
||
| import http.enums.HttpHeader; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.IOException; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| public class HttpHeaders { | ||
| private final Map<String, String> headers; | ||
|
|
||
| public HttpHeaders(Map<String, String> headers) { | ||
| this.headers = new HashMap<>(headers); | ||
| } | ||
|
|
||
| public static HttpHeaders from(BufferedReader br) throws IOException { | ||
| Map<String, String> headerMap = new HashMap<>(); | ||
|
|
||
| String line; | ||
| while ((line = br.readLine()) != null && !line.isEmpty()) { | ||
| int colonIndex = line.indexOf(":"); | ||
| if (colonIndex != -1) { | ||
| String key = line.substring(0, colonIndex).trim().toLowerCase(); | ||
| String value = line.substring(colonIndex + 1).trim(); | ||
| headerMap.put(key, value); | ||
| } | ||
| } | ||
|
|
||
| return new HttpHeaders(headerMap); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| public String getHeader(HttpHeader header) { | ||
| return headers.get(header.getValue().toLowerCase()); | ||
| } | ||
|
|
||
| public String getHeader(String headerName) { | ||
| return headers.get(headerName.toLowerCase()); | ||
| } | ||
|
|
||
| public int getContentLength() { | ||
| String contentLength = getHeader(HttpHeader.CONTENT_LENGTH); | ||
| if (contentLength == null) { | ||
| return 0; | ||
| } | ||
| try { | ||
| return Integer.parseInt(contentLength); | ||
| } catch (NumberFormatException e) { | ||
| return 0; | ||
| } | ||
| } | ||
|
|
||
| public String getCookie() { | ||
| return getHeader(HttpHeader.COOKIE); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| package http; | ||
|
|
||
| import http.enums.HttpMethod; | ||
| import http.util.HttpRequestUtils; | ||
| import http.util.IOUtils; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.IOException; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| public class HttpRequest { | ||
| private final HttpStartLine startLine; | ||
| private final HttpHeaders headers; | ||
| private final String body; | ||
|
|
||
| private HttpRequest(HttpStartLine startLine, HttpHeaders headers, String body) { | ||
| this.startLine = startLine; | ||
| this.headers = headers; | ||
| this.body = body; | ||
| } | ||
|
|
||
| public static HttpRequest from(BufferedReader br) throws IOException { | ||
| String requestLine = br.readLine(); | ||
| if (requestLine == null || requestLine.isEmpty()) { | ||
| throw new IllegalArgumentException("Request line cannot be null or empty"); | ||
|
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. 이 예외는 어디서 핸들링 되나요??
Author
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. 이부분 핸들링은 requestHandler에서 이뤄집니당 |
||
| } | ||
|
|
||
| HttpStartLine startLine = HttpStartLine.from(requestLine); | ||
| HttpHeaders headers = HttpHeaders.from(br); | ||
|
|
||
| String body = null; | ||
| if (startLine.getMethod() == HttpMethod.POST && headers.getContentLength() > 0) { | ||
| body = IOUtils.readData(br, headers.getContentLength()); | ||
| } | ||
|
|
||
| return new HttpRequest(startLine, headers, body); | ||
| } | ||
|
|
||
| public HttpMethod getMethod() { | ||
| return startLine.getMethod(); | ||
| } | ||
|
|
||
| public String getUrl() { | ||
| return startLine.getPath(); | ||
| } | ||
|
|
||
| public String getPath() { | ||
| return startLine.getPathWithoutQuery(); | ||
| } | ||
|
|
||
| public String getQueryString() { | ||
| return startLine.getQueryString(); | ||
| } | ||
|
|
||
| public String getVersion() { | ||
| return startLine.getVersion(); | ||
| } | ||
|
|
||
| public String getHeader(String headerName) { | ||
| return headers.getHeader(headerName); | ||
| } | ||
|
|
||
| public String getCookie() { | ||
| return headers.getCookie(); | ||
| } | ||
|
|
||
| public String getCookie(String key) { | ||
| String cookieHeader = headers.getCookie(); | ||
| if (cookieHeader == null) { | ||
| return null; | ||
| } | ||
|
|
||
| for (String pair : cookieHeader.split(";\\s*")) { | ||
| String[] kv = pair.split("=", 2); | ||
| if (kv.length == 2 && kv[0].equals(key)) { | ||
| return kv[1]; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| public int getContentLength() { | ||
| return headers.getContentLength(); | ||
| } | ||
|
|
||
| public String getBody() { | ||
| return body; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| public HttpStartLine getStartLine() { | ||
| return startLine; | ||
| } | ||
|
|
||
| public HttpHeaders getHeaders() { | ||
| return headers; | ||
| } | ||
|
|
||
| public Map<String, String> getParameters() { | ||
| if (startLine.getMethod() == HttpMethod.GET && getQueryString() != null) { | ||
| return HttpRequestUtils.parseQueryParameter(getQueryString()); | ||
| } else if (startLine.getMethod() == HttpMethod.POST && body != null) { | ||
| return HttpRequestUtils.parseQueryParameter(body); | ||
| } | ||
| return new HashMap<>(); | ||
| } | ||
| } | ||
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.
이런 예외처리 부분이 너무 포괄적이라는 생각도 드네요
나중에 프론트에 에러 던져줄 거 생각해보면 "bad request"보다는 "userId가 비었습니다" 같은 문구가 원인 파악하기에 좋으니까 하나씩 빼서 다른 log 찍도록 처리하면 좋을것 같습니다!
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.
우선은 코드래빗 리뷰대로만 수정했습니다... <- 추석 이슈 입니다..[이전에 프론트만 했덥 입장으로..]
백엔드에서 처리할
파라미터 유효성 검사에 대해서 많이 생각을 해봤는데...
이런 부분은 프론트에서 처리하는 게 맞지 않을까요?-?
서버에서 유효성 검사를 진행하는 게 맞을까요?-? @jyun-KIM
이럴 때 파트장님의 생각은 어떠하신지가 궁금하네요.
제 생각에는 예외처리 부분에서는
userId - password가 매칭이 안되는 것? 과 같이 유효성 검사를 다 뚫고 오는 것들만 처리해주는 게 맞는 거 같아서요...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.
@ParkSeongGeun
네 맞습니다. 성근님이 더 잘 아시겠지만 실제 프로젝트에서는 ID나 비밀번호 값이 비어있는 예외는 프론트엔드에서 처리해주는 것이 일반적이죠. 그리고 백엔드에서는 말씀하신 대로 ID를 받아 사용자가 존재하는지 확인하는 것부터 시작하고요.
다만 지금은 학습하는 과정에 있으니, 발생할 수 있는 예외 상황을 백엔드에서 하나하나 명시적으로 처리하는 연습을 해보는 것이 중요하다고 생각했습니다. 이런 경험이 나중에 안정적인 서비스를 만드는 데 좋은 밑거름이 될 것이라 생각해서 자세하게 말씀드렸습니다!
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.
@jyun-KIM
넵! 반영하도록 하겠습니다!
피드백 감사드려요~!~!