Skip to content
Merged
19 changes: 14 additions & 5 deletions src/main/java/app/db/Database.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,27 @@
import app.model.User;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class Database {
private static Map<String, User> users = new HashMap<>();
private static Map<Long, User> users = new ConcurrentHashMap<>();
private static AtomicLong sequentialId = new AtomicLong(0);

public static void addUser(User user) {
users.put(user.getUserId(), user);
long id = sequentialId.getAndIncrement();
user.setUserId(id);
users.put(id, user);
}

public static User findUserById(String userId) {
return users.get(userId);
public static Optional<User> findUserById(Long userId) {
return Optional.ofNullable(users.get(userId));
}

public static Optional<User> findUserByEmail(String email){
return users.values().stream().filter(u -> u.getEmail().equals(email)).findAny();
Comment on lines +25 to +26

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

성능 경고: stream().filter().findAny()는 매번 전체 컬렉션을 순회합니다. 사용자 수가 증가하면 로그인 성능이 저하됩니다.\n\n개선안: 사용자를 이메일로 검색하려면 별도의 Map을 유지하거나 데이터베이스 인덱싱을 사용하세요.\njava\nprivate static Map<String, User> usersByEmail = new ConcurrentHashMap<>();\n\npublic static Optional<User> findUserByEmail(String email) {\n return Optional.ofNullable(usersByEmail.get(email));\n}\n"

}

public static Collection<User> findAll() {
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/app/handler/LoginWithPost.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package app.handler;

import app.db.Database;
import app.model.User;
import config.VariableConfig;
import exception.ErrorCode;
import exception.ServiceException;
import http.HttpMethod;
import http.response.CookieBuilder;
import web.dispatch.argument.QueryParameters;
import web.handler.SingleArgHandler;
import web.response.HandlerResponse;
import web.response.RedirectResponse;
import web.session.SessionEntity;
import web.session.SessionStorage;

public class LoginWithPost extends SingleArgHandler<QueryParameters> {
private final SessionStorage sessionManager;

public LoginWithPost(SessionStorage sessionManager) {
super(HttpMethod.POST, "/user/login");
this.sessionManager = sessionManager;
}

@Override
public HandlerResponse handle(QueryParameters params) {
String email = params.getQueryValue("email")
.orElseThrow(() -> new ServiceException(ErrorCode.LOGIN_FAILED, "email required"));

String password = params.getQueryValue("password")
.orElseThrow(() -> new ServiceException(ErrorCode.LOGIN_FAILED, "password required"));

User user = Database.findUserByEmail(email)
.orElseThrow(() -> new ServiceException(ErrorCode.LOGIN_FAILED));

if (!user.getPassword().equals(password)) {
throw new ServiceException(ErrorCode.LOGIN_FAILED);
Comment on lines +35 to +37

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보안 위험: 평문 비밀번호를 데이터베이스에 저장하고 직접 비교하면 안 됩니다. 해킹으로 데이터베이스가 유출되면 모든 사용자의 비밀번호가 노출됩니다.\n\n필수 개선: 비밀번호를 해싱(예: bcrypt, PBKDF2)으로 저장하고, 로그인 시에는 입력 비밀번호를 동일한 해시 알고리즘으로 검증하세요."

}

SessionEntity session = sessionManager.create(
user.getUserId(),
user.getUserRole());

RedirectResponse res = RedirectResponse.to("/");
res.setCookie(
CookieBuilder.of("SID", session.getId())
.path("/")
.httpOnly()
.sameSite(CookieBuilder.SameSite.LAX)
.maxAge(VariableConfig.ABSOLUTE_MS)
);
return res;
}
}
10 changes: 5 additions & 5 deletions src/main/java/app/handler/RegisterWithGet.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import web.dispatch.argument.QueryParameters;
import web.filter.authentication.UserRole;
import web.handler.SingleArgHandler;
import web.response.HandlerResponse;
import web.response.StaticViewResponse;
Expand All @@ -22,12 +23,11 @@ public RegisterWithGet() {

@Override
public HandlerResponse handle(QueryParameters params) {
String userId = params.getQueryValue("userId").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "userId required"));
String password = params.getQueryValue("password").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "password required"));
String name = params.getQueryValue("name").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "name required"));
String email = params.getQueryValue("email").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "email required"));
Database.addUser(new User(userId, password, name, email));
log.info("Registered - userId:{}, password:{}, name:{}, email:{}", userId, password, name, email);
String password = params.getQueryValue("password").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "password required"));
String nickname = params.getQueryValue("nickname").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "nickname required"));
Database.addUser(new User(password, nickname, email, UserRole.MEMBER.toString()));
log.info("Registered - password:{}, nickname:{}, email:{}", password, nickname, email);
return StaticViewResponse.of("/login");
}
}
10 changes: 5 additions & 5 deletions src/main/java/app/handler/RegisterWithPost.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import web.dispatch.argument.QueryParameters;
import web.filter.authentication.UserRole;
import web.handler.SingleArgHandler;
import web.response.HandlerResponse;
import web.response.StaticViewResponse;
Expand All @@ -21,12 +22,11 @@ public RegisterWithPost() {

@Override
public HandlerResponse handle(QueryParameters params) {
String userId = params.getQueryValue("userId").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "userId required"));
String password = params.getQueryValue("password").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "password required"));
String name = params.getQueryValue("name").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "name required"));
String email = params.getQueryValue("email").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "email required"));
Database.addUser(new User(userId, password, name, email));
log.info("Registered - userId:{}, password:{}, name:{}, email:{}", userId, password, name, email);
String nickname = params.getQueryValue("nickname").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "nickname required"));
String password = params.getQueryValue("password").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "password required"));
Database.addUser(new User(password, nickname, email, UserRole.MEMBER.toString()));
log.info("Registered - password:{}, nickname:{}, email:{}", password, nickname, email);
return StaticViewResponse.of("/login");
}
}
27 changes: 18 additions & 9 deletions src/main/java/app/model/User.java
Original file line number Diff line number Diff line change
@@ -1,36 +1,45 @@
package app.model;

public class User {
private String userId;
private Long userId;
private String password;
private String name;
private String nickname;
private String email;
private String userRole;

public User(String userId, String password, String name, String email) {
this.userId = userId;
public User(String password, String nickname, String email, String userRole) {
this.password = password;
this.name = name;
this.nickname = nickname;
this.email = email;
this.userRole = userRole;
Comment on lines +10 to +14

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

설계 문제: User 엔티티의 userId가 null로 초기화되고 나중에 setUserId()로 설정됩니다. 생성 후 즉시 사용하면 런타임 오류가 발생할 수 있습니다.\n\n개선안: 생성자에서 userId를 받거나, 필수 필드를 필드 선언 단계에서 초기화하세요."

}

public String getUserId() {
public Long getUserId() {
return userId;
}

public void setUserId(Long userId){
this.userId = userId;
}

public String getPassword() {
return password;
}

public String getName() {
return name;
public String getNickname() {
return nickname;
}

public String getEmail() {
return email;
}

public String getUserRole() {
return userRole;
}

@Override
public String toString() {
return "User [userId=" + userId + ", password=" + password + ", name=" + name + ", email=" + email + "]";
return "User [userId=" + userId + ", password=" + password + ", name=" + nickname + ", email=" + email + "]";
}
}
66 changes: 50 additions & 16 deletions src/main/java/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package config;

import app.handler.LoginWithPost;
import app.handler.RegisterWithGet;
import app.handler.RegisterWithPost;
import exception.ExceptionHandlerMapping;
Expand All @@ -18,13 +19,13 @@
import web.dispatch.argument.ArgumentResolver;
import web.dispatch.argument.resolver.HttpRequestResolver;
import web.dispatch.argument.resolver.QueryParamsResolver;
import web.filter.AccessLogFilter;
import web.filter.FilterChainContainer;
import web.filter.RestrictedFilter;
import web.filter.*;
import web.handler.StaticContentHandler;
import web.handler.WebHandler;
import web.renderer.HttpResponseRenderer;
import web.renderer.RedirectRenderer;
import web.renderer.StaticViewRenderer;
import web.session.SessionStorage;

import java.util.List;

Expand Down Expand Up @@ -71,7 +72,7 @@ public Dispatcher dispatcher() {
() -> new Dispatcher(
webHandlerList(),
handlerAdapterList(),
webHandlerResponseHandlerList()
httpResponseRendererList()
)
);
}
Expand All @@ -82,11 +83,19 @@ public List<WebHandler> webHandlerList() {
() -> List.of(
staticContentHandler(),
registerWithGet(),
registerWithPost()
registerWithPost(),
loginWithPost()
)
);
}

public StaticContentHandler staticContentHandler() {
return getOrCreate(
"staticContentHandler",
StaticContentHandler::new
);
}

public RegisterWithGet registerWithGet() {
return getOrCreate(
"registerWithGet",
Expand All @@ -101,29 +110,34 @@ public RegisterWithPost registerWithPost() {
);
}

public List<HttpResponseRenderer> webHandlerResponseHandlerList() {
public LoginWithPost loginWithPost() {
return getOrCreate("loginWithPost",
() -> new LoginWithPost(sessionStorage()));
}

// ===== Renderer =====
public List<HttpResponseRenderer> httpResponseRendererList() {
return getOrCreate(
"webHandlerResponseHandlerList",
"httpResponseRendererList",
() -> List.of(
staticViewResponseHandler()
staticViewRenderer(),
redirectRenderer()
)
);
}

public StaticContentHandler staticContentHandler() {
public StaticViewRenderer staticViewRenderer() {
return getOrCreate(
"staticContentHandler",
StaticContentHandler::new
"staticViewRenderer",
StaticViewRenderer::new
);
}

public StaticViewRenderer staticViewResponseHandler() {
return getOrCreate(
"staticViewResponseHandler",
StaticViewRenderer::new
);
public RedirectRenderer redirectRenderer() {
return getOrCreate("redirectRenderer", RedirectRenderer::new);
}


// ===== Adapter =====
public List<HandlerAdapter> handlerAdapterList() {
return getOrCreate(
Expand Down Expand Up @@ -228,4 +242,24 @@ public AccessLogFilter accessLogFilter(){
public RestrictedFilter restrictedFilter(){
return getOrCreate("restrictedFilter", RestrictedFilter::new);
}

public AuthenticationFilter authenticationFilter() {
return getOrCreate("authenticationFilter",
() -> new AuthenticationFilter(sessionStorage()));
}

public MemberAuthorizationFilter memberAuthorizationFilter(){
return getOrCreate("memberAuthorizationFilter",
MemberAuthorizationFilter::new);
}

public UnanimousAuthorizationFilter unanimousAuthorizationFilter(){
return getOrCreate("unanimousAuthorizationFilter",
UnanimousAuthorizationFilter::new);
}

public SessionStorage sessionStorage() {
return getOrCreate("sessionStorage",
SessionStorage::new);
}
}
17 changes: 12 additions & 5 deletions src/main/java/config/FilterConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ private void setFilterChains(){
.addFilterList(FilterType.ALL, getFilterListByAuthorityType(FilterType.ALL))
.addFilterList(FilterType.PUBLIC, getFilterListByAuthorityType(FilterType.PUBLIC))
.addFilterList(FilterType.AUTHENTICATED, getFilterListByAuthorityType(FilterType.AUTHENTICATED))
.addFilterList(FilterType.RESTRICT, getFilterListByAuthorityType(FilterType.RESTRICT));
.addFilterList(FilterType.RESTRICT, getFilterListByAuthorityType(FilterType.RESTRICT))
.addFilterList(FilterType.LOG_IN, getFilterListByAuthorityType(FilterType.LOG_IN));
}

private List<ServletFilter> commonFrontFilter(){
Expand All @@ -47,10 +48,16 @@ private List<ServletFilter> getFilterListByAuthorityType(FilterType type) {
private List<ServletFilter> authorizedFilterList(FilterType type) {
return switch (type) {
case ALL -> List.of();
case PUBLIC -> List.of();
case AUTHENTICATED -> List.of();
case RESTRICT -> List.of(appConfig.restrictedFilter());
case LOG_IN -> List.of();
case PUBLIC -> List.of(
appConfig.authenticationFilter());
case AUTHENTICATED -> List.of(
appConfig.authenticationFilter(),
appConfig.memberAuthorizationFilter());
case RESTRICT -> List.of(
appConfig.restrictedFilter());
case LOG_IN -> List.of(
appConfig.authenticationFilter(),
appConfig.unanimousAuthorizationFilter());
};
}
Comment on lines +51 to 62

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구조 설계 피드백 (PR 요청사항): 로그인 로직을 핸들러 계층에 배치한 것은 좋은 선택입니다. 이유:\n\n✅ 핸들러 계층 배치의 장점:\n- 로그인은 비즈니스 로직 (사용자 검증, 세션 생성)이므로 handlers에 속함\n- 필터는 요청/응답의 관심사 분리(authentication/authorization 처리)에만 집중\n- 필터-핸들러 분리가 명확하고 유지보수 용이\n\n✅ 구현 모델이 명확함: \n- AuthenticationFilter: 세션 정보 로드 및 사용자 정보 설정\n- MemberAuthorizationFilter/UnanimousAuthorizationFilter: 권한 확인\n- LoginWithPost: 실제 로그인 비즈니스 로직 처리\n\n다만 위의 보안 및 안정성 이슈(비밀번호 해싱, null 체크, 성능 등)를 개선하면 더 견고한 구조가 될 것입니다."

}
1 change: 1 addition & 0 deletions src/main/java/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public void config(){
public void setPaths(){
appConfig.filterChainContainer()
.addPath(FilterType.AUTHENTICATED, "/mypage/**")
.addPath(FilterType.LOG_IN, "/user/login")
.addPath(FilterType.ALL, "/user/**")
.addPath(FilterType.PUBLIC, "/**");
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/config/VariableConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ public class VariableConfig {
public static final List<String> STATIC_RESOURCE_ROOTS = List.of(
"./src/main/resources",
"./src/main/resources/static");

public static final long IDLE_MS = 30*60*100;
public static final long ABSOLUTE_MS = 180*60*100;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단위 오류: 30*60*100은 18,000ms(18초)이며, 180*60*100은 1,080,000ms(18분)입니다. 일반적으로 세션 타임아웃은 분 단위로 설정하는데, 계산식에서 1000을 곱해야 합니다.\n\n의도:\n- IDLE_MS: 30분 = 30*60*1000\n- ABSOLUTE_MS: 180분(3시간) = 180*60*1000"

}
2 changes: 2 additions & 0 deletions src/main/java/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public enum ErrorCode {
/* Service Exception */
MISSING_REGISTER_TOKEN(
HttpStatus.BAD_REQUEST, "400_MISSING_REGISTER_TOKEN", "회원가입에 필요한 토큰이 누락되었습니다."),
LOGIN_FAILED(
HttpStatus.BAD_REQUEST, "400_LOGIN_FAILED", "아이디 혹은 비밀번호가 잘못되었습니다."),

/* Internal Error */
INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500_INTERNAL", "서버 내부 오류가 발생했습니다."),
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/http/HttpStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public enum HttpStatus {
ACCEPTED(202),
NO_CONTENT(204),

FOUND(302),

BAD_REQUEST(400),
UNAUTHORIZED(401),
FORBIDDEN(403),
Expand Down
Loading