3주차 미션 - 동적인 HTML과 쿠키,세션을 이용한 로그인
3주차 미션은 다음과 같았다
1. GET으로 회원가입 -> POST로 변경
2. 동적인 HTML요구(로그인, 비로그인시 메인화면 페이지가 동적으로 변할 수 있어야함
- 로그인시: 사용자의 이름이 보여야하고, 로그인 회원가입 버튼이 삭제
- 비로그인시: 사용자의 이름이 보이지 않고, 로그인 회원가입 버튼이 보여야함
3. 쿠키 세션을 이용한 로그인/회원가입 기능 요구
1. GET으로 회원가입 -> POST로 변경
나는 이전에 중간 피드백시간에서 한 팀원이 리플렉션을 사용하여 컨트롤러를 선택하는 로직을 보았다.
해당 로직은 인상깊었고, 실제로 스프링도 Reflection을 사용하여 로직이 구현되는것으로 알 고 있다.
그렇기에 이번에 Reflection을 공부해볼겸 실제 프로젝트에도 적용해보았다!
public static void findController(HttpRequest httpRequest, DataOutputStream dataOutputStream) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, FailLoggedException, InvocationTargetException, NoSuchMethodException, NotLoggedException {
String packageName = "controller";
ClassPath classpath = ClassPath.from(Thread.currentThread().getContextClassLoader());
ImmutableSet<ClassPath.ClassInfo> havingControllerInfoClasses = classpath.getTopLevelClassesRecursive(packageName);
for (ClassPath.ClassInfo classInfo : havingControllerInfoClasses) {
Class<Controller> clazz = (Class<Controller>) Class.forName(classInfo.getName());
if (clazz.isAnnotationPresent(ControllerInfo.class)) {
handleControllerInfoAnnotation(clazz.newInstance(), httpRequest, dataOutputStream);
}
}
}
public static void handleControllerInfoAnnotation(Controller controller, HttpRequest httpRequest, DataOutputStream dataOutputStream) throws IllegalAccessException, InvocationTargetException, NotLoggedException {
Method[] methods = controller.getClass().getMethods();
Optional<Method> method = Arrays.stream(methods)
.filter(m -> m.isAnnotationPresent(ControllerMethodInfo.class))
.filter(m -> isMatchControllerMethodWithRequest(m.getAnnotation(ControllerMethodInfo.class), httpRequest))
.findAny();
if (method.isEmpty()) {
return;
}
if (method.get().isAnnotationPresent(Auth.class) && !sessionService.cookieValidInSession(httpRequest.getRequestHeader())) {
throw new NotLoggedException(HttpsErrorMessage.NOT_LOGGED);
}
method.get().invoke(controller, dataOutputStream, httpRequest);
}
- FindController함수는 @ControllerInfo라는 어노테이션이 달린 클래스를 팢는다
- handleControllerInfoAnnoatation 은 @ControllerMethodInfo 어노테이션이 달린 메서드를 찾아 해당 메서드의 멤버변수인 path, http method 가 request와 일치하면 해당 메서드(컨트롤러)를 호출한다.
@ControllerInfo(path = "/user/create", u = UrlType.NOTHING, method = HttpMethod.POST)
public HttpResponse UserQueryString(DataOutputStream dataOutputStream, HttpRequest httpRequest) throws IOException {
requestReader = new RequestPostReader();
HashMap<String, String> userMap = requestReader.readData(httpRequest);
User user = (User) userService.createModel(userMap);
userDatabase.addData(user);
HttpResponse httpResponse = new HttpResponse(new Data(dataOutputStream), FileType.HTML, HttpStatus.RE_DIRECT);
logger.debug("저장된 user:{}", userDatabase.findAll());
return httpResponse;
}
확실히 어노테이션과 Reflection을 사용하니 method= HttpMethod.Get을 Post로 바꾸고 Request데이터를 읽는방식만 바꾸면 되었기에 좀더 수월하였다.
2. 동적인 HTML요구
package view;
public class HomeRender {
private static HomeRender homeRender;
private HomeRender() {}
public static HomeRender getInstance() {
if (homeRender == null) {
homeRender = new HomeRender();
}
return homeRender;
}
public byte[] addSignInAndUpTag(byte[] homeData) {
String homeStr = new String(homeData);
homeStr = homeStr.replace("<!-- <li><a href=\"user/login.html\" role=\"button\">로그인</a></li>-->",
" <li><a href=\"user/login.html\" role=\"button\">로그인</a></li>");
homeStr = homeStr.replace("<!-- <li><a href=\"user/form.html\" role=\"button\">회원가입</a></li>-->",
" <li><a href=\"user/form.html\" role=\"button\">회원가입</a></li>");
return homeStr.getBytes();
}
public byte[] addUserName(byte[] homeData,String userName) {
String homeStr = new String(homeData);
homeStr = homeStr.replace("<!-- <li><a href=\"#\">이름</i></a></li>-->",
" <li><a href=\"#\">"+userName+"</i></a></li>");
return homeStr.getBytes();
}
}
- 싱글톤으로 view를 동적으로 변경하게 해주는 클래스로 생성
- addSignInAndUpTag, addUserName모두 이전 index.html을 태그 중 필요한곳에 replace를 하는 기능이다.
즉! 주석으로 처리한 부분을 String의 replace를 사용하여 기능을 구현해 보았다!
3. 쿠키 세션을 이용한 로그인/회원가입 기능 요구
난 쿠키가 누구에게 주도권이 있는지 몰랐다.
하지만 이번에 쿠키를 다루면서 가장 크게 안 사실!
-> 쿠키는 서버가 지정해줄수 있다!(Set-Cookie를 통해) -> 즉 쿠키의 주도권은 서버에게 있다
쿠키
- 요청의 주체는 서버이다! (setCookie)
- 서버가 SetCookie를 저장하면, 다음 http Request시 매번 cookie를 채워 보낸다.
세션
- 서버에서 사용자 하나마다 저장되는 단위
- 세션 ID를 긴 랜덤값 사용 이유 → BruteForce 공격이 가능
- 세션 DB는 주로 redis를 사용한다
- 일회성값일 수 있는 session을 RDB등과 같은 DB에 저장하면 매우느림
- 실무에서는 redis를 cluster로 구성하여 크게 사용한다고 함
- 세션 클러스터링을 통한 SCALE UP
세션과 쿠키를 사용할때 가장 중요하게 생각하는 흐름!
- 클라이언트와 서버 사이는 Cookie를 통해 Session Id만 주고 받는다.
- 서버는 클라이언트가 전송한 Session Id를 활용해 서버에 저장된 값을 꺼내와 사용한다.
(코드 보기싫으면 그냥 넘겨두됨)
public class Cookie {
private static final String COOKIE = "Cookie";
private String key;
private String value;
public Cookie(String key, String value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
public static Optional<Cookie> extractCookie(RequestHeader requestHeader) {
String cookieSet = requestHeader.getHeaderContents().get(COOKIE);
if (cookieSet == null) {
return Optional.empty();
}
String[] splitCookie = cookieSet.split("=");
return Optional.of(new Cookie(splitCookie[0], splitCookie[1]));
}
}
public class Session {
public static final String SESSION_ID = "sid";
private String uuid;
private final User user;
public Session( User user) {
this.uuid = makeSessionId();
this.user = user;
}
public String getUuid() {
return uuid;
}
private String makeSessionId() {
return UUID.randomUUID().toString();
}
public User getUser() {
return user;
}
}
간단하게 정리하면 다음과 같다.
1. 로그인/회원가입시 Session을 하나 생성 (uuid, User)를 가짐 -> 이후 DB에 저장
2. HttpResponse에 Set-Cookie : sid="uuid 값" 을 클라이언트에 보내줌
3. 클라이언트는 항상 cookie값을 서버에게 보내줌
4. 만약 로그인(인증)이 필요한 상황이라면 클라이언트로 받은 uuid 를 가지고 User객체를 찾아 User객체가 있다면 유효한 로그인으로 판단
해당 미션을 하면서 느꼈던것은
1. 확실히 공부를 할때는 내가 아는거 30%, 모르는게 70%정도의 비율은 되어야 공부가 되는거 같다. (Reflection을 적용할때 내가 아는 부분 30퍼(이론)밖에 없었고 실제 적용해보니 더 이해가 잘된것을 확인할 수 있었다.
2. 쿠키 세션도 이론부분만 알고있었고 실제사용해보진 않았다. 로우레벨로부터 접근하니 원리를 확실하게 알 수 있었다.
3. 동적인 HTML을 구현하는 더 좋은 방법이 있을거 같다. 실제로 '타임리프'와 같은 템플릿엔진은 모델을 넘겨주고 동적인 HTML을 구현하는것으로 알 고 있다. 나도 단지 replace를 하는 방법이 아닌 model을 넘겨주는 방식으로 구현해볼 수 있지 않을까??