JS/boostCamp

24.10.5~7. 개발일지 // 회원가입구현, redirect, cookie, session, 인증

Mini_96 2024. 10. 7. 23:00

* 회원가입구현

  • 프론트 컨트롤러 라우팅 추가
this.handlerMappingMap.set("/user/save", new UserSaveController());
import {Member} from "../../../domain/member/Member";
import {MemberRepository} from "../../../domain/member/MemberRepository";
import {ControllerV4} from "../ControllerV4";

export class UserSaveController implements ControllerV4{

    private memberRepository: MemberRepository = MemberRepository.getInstance();

    process(paramMap: Map<string, string>, model: Map<string, object>): string {
        const userId: string = paramMap.get('userId');
        const name: string = paramMap.get('name');
        const email : string = paramMap.get('email');
        const password: string = paramMap.get('password');

        const member = new Member(0, userId, password , name, email);
        this.memberRepository.save(member);

        model.set("member", member);
        return "save-result";
    }
}

 

* post 의 문제

  • 현재 웹사이트는 심각한 문제가있다.
  • 바로 새로고침을하면 같은회원이 계속 저장되는것이다.
  • 새로고침은 직전의 요청을 다시보내는것이다.

새로고침시 post 요청을 다시보냄

  • 이는 개발자의 의도, 사용자의 의도를 벗어난다.
  • 단순한 회원가입이면 그나마 문제가 적겠지만, 상품주문같은 기능 or 계좌이체서비스 에서 이런일이 생긴다면 사이트가 망할것이다.

 

* 해결 : post redirect get 패턴

  • 우선 controller에서 개발자가 redirect를 원할경우,
  • return redirect:viewName 이런 형식으로 개발해야한다.

  • 프론트 컨트롤러에서는 이를 파싱해 개발자가 redirect를 원한경우
  • response 코드를 302로 해주고
  • response header에 location = viewName을 추가한다. //이래야 브라우저에서 해당 location의 url로 이동된다.
/**
 * Controller에서 redirect:index 요청시
 * res.302
 * res.location = index.html
 */
let viewName = mv.getViewName();
let view: MyView = this.viewResolver(viewName); //물리이름이 들어간 MyView 객체 만들기

/**
 * redirect 처리위한 로직
 */
if(viewName.startsWith("redirect:")){
    const [_, temp ] = viewName.split(":");
    viewName = temp;
    view = this.viewResolver(viewName);
    res.status(302).header("Location","http://localhost:3000/" + viewName);
}

회원가입전
회원가입후 자동으로 index로 이동!

  • 이제 새로고침하면 홈페이지에 get 요청을 다시 보낼뿐이다.
  • 직전 회원정보를 가지고 post를 날리지 않는다.

 

* css 경로 버그 fix

  • 원인 : 앞에 .. 을 안쓰면 현재 url 경로  + css/main.css 를 찾는다. 

절대경로?
not found

  • 해결 : .. 을 붙이면 이 html 파일이있는경로 + css/main.css 를 찾는다.

 

* 쿠키 구현

  • response를 이런식으로 만들면 되겠군.

Set-Cookie 설정시 모든 요청에 대해 Cookie 처리가 가능하도록 Path 설정 값을 Path=/로 설정한다.

public cookie(name:string, uuid : string) : this {
    this.header("Set-Cookie",name+"="+uuid+"; "+"path=/");
    return this;
}

이후 요청에서는 이렇게 보내진다.

  • 파싱을 위해 cookie-parser를 사용해보자.

 

* 프론트컨트롤러 v6 adapter 구현

  • set-cookie를 위해서는 컨트롤러에서 res를 직접 접근해야한다.
  • 이를 위해 v2 adapter를 구현해야한다. -> 너무 구버전이라 복잡..(물리이름반환필요)
  • v3부터는 res, req 종속성이 제거된 컨트롤러이다.
  • v6 컨트롤러 : res, req를 인자로 받고 논리적인 뷰이름만 반환하는 컨트롤러를 구현하자.
public createSession(member: Member, res: Response): string {
    const sessionId = uuidv4();
    this.sessionStore.set(sessionId, member);
    res.cookie(this.SESSION_COOKIE_NAME, sessionId);

    return sessionId;
}
  • instanceof 문제는 일단 version이름을 둬서 해결함.
  • 확인시 메소드명 확인하면됨.
import {Request} from "../../was/request";
import {Response} from "../../was/response";
import {MyView} from "../MyView";

export interface ControllerV6 {
    process(req : Request , res : Response, paramMap : Map<string, string> , model : Map<string, object>) : string;

    version6(): void;
}
  • v6 어댑터
import {MyHandlerAdapter} from "../MyHandlerAdapter";
import {Request} from "../../../was/request";
import {Response} from "../../../was/response";
import {ModelView} from "../../ModelView";
import {objectToMap} from "../../../utils/utils";
import {ControllerV6} from "../../v6/ControllerV6";

export class ControllerV6HandleAdapter implements MyHandlerAdapter {
    /**
     * interface에는 instanceof 사용 불가능
     * -> 세부비교 필요
     * @param handler
     */
    supports(handler : any): boolean {
        return (
            handler !== null &&
            "process" in handler &&
            "version6" in handler &&
            typeof (handler as ControllerV6).process === "function" &&
            (handler as ControllerV6).process.length === 4
        );
    }

    handle(req: Request, res: Response , handler : any): ModelView {
        const controller  = handler as ControllerV6;

        //컨트롤러가 req를 몰라도 되도록 Map에 req 정보를 담아서 넘김
        const paramMap : Map<string, string> = objectToMap(req.body);
        const model : Map<string, object> = new Map<string, object>();
        const viewName = controller.process(req, res, paramMap, model);

        const mv = new ModelView(viewName);
        mv.setModel(model); //set도 해줘야함!

        return mv;
    }
}
  • loginController V6
  • 여기서 res를 넘겨서 res에 cookie를 set 한다.
import {ControllerV6} from "../ControllerV6";
import {MemberRepository} from "../../../domain/member/MemberRepository";
import {Request} from "../../../was/request";
import {Response} from "../../../was/response";
import {SessionManager} from "../../../utils/SessionManager";

export class LoginControllerV6 implements ControllerV6{

    private memberRepository: MemberRepository = MemberRepository.getInstance();
    private sessionMgr : SessionManager = SessionManager.getInstance();

    process(req: Request, res: Response, paramMap: Map<string, string>, model: Map<string, object>): string {
        const email: string = paramMap.get('email');
        const password: string = paramMap.get('password');

        const findMember = this.memberRepository.findByEmail(email);
        if(!findMember){
            console.log('회원없음 회원가입필요');
            return "new-form";
        }
        if(findMember && password !== findMember.getPassword()){
            console.log('비밀번호 불일치');
            return "new-form";
        }

        this.sessionMgr.createSession(findMember, res);
        return "redirect:index"; //성공시 홈으로 보냄
    }

    version6() {
    }
}

쿠키 생성까지 확인

 

* todo : html 에서 login 누르면 요청보내기

 

* 로그인이 실패하면 /user/login_failed.html로 이동한다.

  • login 실패시, user/login_failed로 redirect 식으로 구현하고자 한다.
  • post redirect get pattern
  • html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login failed!</title>
</head>
<body>
    로그인 실패!
</body>
</html>
  • 로그인컨트롤러에서 실패시 redirect 시킨다.

  • 라우팅 추가

  • login fail controller 추가
import {ControllerV4} from "../ControllerV4";

export class LoginFailControllerV4 implements ControllerV4{

    process(paramMap: Map<string, string>, model: Map<string, object>): string {
        return "user/login_failed";
    }
}
  • 주의점은 html 파일을 user폴더 내에 만들어야 정상작동한다.
  • 로그인 실패시 get으로 다시 요청을 보내는 모습.

결과

 

* static serving fix

  • 문제 : 경로에 path 원본을 더하면 아래와 같이 오류가난다.

  • 해결 : 정규표현식 => 마지막 2개 경로만 남김. 두개 미만인경우는 전체경로 반환

css 를 잘불러온다.

 

* 쿠키로 인증 구현

  • 먼저 쿠키가 set 된후, 이후요청에 보내지는지 확인하자.
  • 먼저 로그인 폼을 구현하자.

절대경로 지정 / 회원이 없는경우
로그인후 쿠키가 set 된 모습

 

* 사용자가 로그인 상태일 경우 /index.html에서 사용자 이름을 표시 구현

 

* http://localhost:8080/user/list  페이지 접근시 로그인하지 않은 상태일 경우 로그인 페이지(login.html)로 이동구현.

  • 먼저, 모든 컨트롤러에서, cookie를 이용해서 sessionMap에 있는 member를 가져오도록 하고, member 가 없으면 return 하는 방식으로 구현할수있다. -> 하지만 모든 컨트롤러에 이 로직을 추가해야한다.
  • 따라서, 미들웨어를 이용해서 보다 보편화하고 화이트 리스트 패턴을 사용해 개발자의 실수를 줄이고자 한다.
  • 회원 리스트 컨트롤러
import {ControllerV4} from "../ControllerV4";

export class UserListControllerV4 implements ControllerV4{

    process(paramMap: Map<string, string>, model: Map<string, object>): string {
        return "user/list";
    }
}
  • test용 임시 html (user/list.html)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>list</title>
</head>
<body>
    회원리스트 (임시)
</body>
</html>
  • auth middleware 구현
    • 화이트 리스트 => 지정된 페이지 외의 페이지는 로그인필요
    • 화이트 리스트 배열에 '/' 추가시 모든 페이지가 허용되는 버그가 있음 -> 화이트 리스트 배열에 추가하지않고, url이 '/'인 경우, pass 하도록 예외처리. (임시해결)
    • 이외의 페이지인경우, 쿠키를 이용해서 member가 세션맵에 있는지 검증후, 다음미들웨어로 통과를 허가한다.
import {Request} from "../was/request";
import {Response} from "../was/response";
import {SessionManager} from "../utils/SessionManager";

const whiteList: string[] = ['index','/user/login','/user/form','/user/save','/user/login/form','/user/login/failed',  '/css', '/js', '/images', '/favicon.ico', '/user/views/css/main.css'];
const sessionManager: SessionManager = SessionManager.getInstance();

function isLoginCheckPath(url: string): boolean {
    if(url=='/') return false;
    return !whiteList.some(path => url.startsWith(path));
}

export function authMiddleware(req: Request, res: Response, next): void {
    const requestURI: string = req.path;
    try {
        console.log(`인증 체크 필터 시작 ${requestURI}`);
        if (isLoginCheckPath(requestURI)) {
            console.log(`인증 체크 로직 실행 ${requestURI}`);
            const findCookieValue: string | null = sessionManager.findCookie(req, sessionManager.SESSION_COOKIE_NAME);
            if (findCookieValue === null) {
                console.log(`미인증 사용자 요청 ${requestURI}`);
                // 로그인으로 redirect
                res.status(302).redirect('user/login/form');
                req.isEnd=true;
                return; // 미인증 사용자는 다음 미들웨어로 진행하지 않고 끝!
            }
        }
        next(); // 인증된 사용자는 다음 미들웨어로 진행
    } catch (e) {
        throw e; // error는 끝까지 보내줘야 함.
    } finally {
        console.log(`인증 체크 필터 종료 ${requestURI}`);
    }
}
  • response 에 redirect 구현
    • 문제 1 : redirect가 302코드일때만 브라우저 에서 작동함, 원래는 401 Unauth 에러를 내려준후 redirect 해야되는게 맞는게 아닌지? // 401로 status를 set한경우 redirect 작동안함.
    • 문제 2 : is End = true 로 둬야지 추가적인 동적 라우팅을 막을수있음. (임시 해결)
public redirect(path: string) {
    this.header("Location","http://localhost:3000/" + path);
    this.send();
}

화이트 리스트인 회원가입, main.css 등은 로그인안해도 응답가능.
이외의 user/list 접근시 302 redirect 됨.

 

 

* todo : 사용자가 로그인 상태일 경우 /index.html에서 사용자 이름을 표시해 준다.

사용자가 로그인 상태가 아닐 경우 /index.html에서 [로그인] 버튼을 표시해 준다.

  • index를 ejs로 만들고, 찾은 맴버객체를 넘기면 될듯?