* 회원가입구현
- 프론트 컨트롤러 라우팅 추가
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 의 문제
- 현재 웹사이트는 심각한 문제가있다.
- 바로 새로고침을하면 같은회원이 계속 저장되는것이다.
- 새로고침은 직전의 요청을 다시보내는것이다.
- 이는 개발자의 의도, 사용자의 의도를 벗어난다.
- 단순한 회원가입이면 그나마 문제가 적겠지만, 상품주문같은 기능 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);
}
- 이제 새로고침하면 홈페이지에 get 요청을 다시 보낼뿐이다.
- 직전 회원정보를 가지고 post를 날리지 않는다.
* css 경로 버그 fix
- 원인 : 앞에 .. 을 안쓰면 현재 url 경로 + css/main.css 를 찾는다.
- 해결 : .. 을 붙이면 이 html 파일이있는경로 + css/main.css 를 찾는다.
* 쿠키 구현
- response를 이런식으로 만들면 되겠군.
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개 경로만 남김. 두개 미만인경우는 전체경로 반환
* 쿠키로 인증 구현
- 먼저 쿠키가 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();
}
* todo : 사용자가 로그인 상태일 경우 /index.html에서 사용자 이름을 표시해 준다.
사용자가 로그인 상태가 아닐 경우 /index.html에서 [로그인] 버튼을 표시해 준다.
- index를 ejs로 만들고, 찾은 맴버객체를 넘기면 될듯?
'JS > boostCamp' 카테고리의 다른 글
24. 10. 9. 개발일지 // redirectAttributes, 로그인이완료된후 원래페이지 이동, db, n+1 문제 (1) | 2024.10.10 |
---|---|
24.10.8. 개발일지 // 쿠키파서, db연결 , typeorm (0) | 2024.10.08 |
24. 10. 4. 개발일지 // 정적서빙버그 fix (2) | 2024.10.05 |
24.10.3. 개발일지 // MemberSaveController 구현, 프론트컨트롤러 v4구현, 유연한 컨트롤러 구현, 어댑터패턴, instanceof interface (0) | 2024.10.03 |
24. 10. 2. 개발일지 // 프론트컨트롤러 v3, 동적렌더링, mapToObj (2) | 2024.10.02 |