서버 세팅
index.ts
import express from 'express';
import cors from 'cors';
import routes from './routes';
const app = express();
/**
* http에 암호화된 데이터를 분석하여 req.body에 담아주기 위해 사용
* */
// json 포맷을 해독하기 위해 사용하는 미들웨어
app.use(express.json());
// x-www-form-urlencoded 포맷을 해독하기 위해 사용하는 미들웨어
app.use(express.urlencoded({extended : false}));
// cors 설정
app.use(
cors({
credentials: true,
origin : 'http://localhost:3000',
})
);
// 서버 시작 함수
function main() {
app.listen(4000, () => {
console.log('Server listening at http://localhost:4000');
});
routes(app);
}
main();
fake DB
users.ts
interface IUser {
id : number;
email : string;
password: string;
}
// user 데이터 생성
export const users = [
{
id : 1,
email : "test@test.com",
password : "123456"
}
]
export const searchUser = (email : string) => {
return users.find((user) => user.email === email) as IUser
}
- userData를 users에 담아줌
- DB를 사용하게 되면 DB 연결 및 DB 관련 메서드 공부에 시간이 안 날거 같아 fake DB로 대체
- searchUser는 email을 통해 users에 있는 email과 동일 한 객체를 찾아 배열로 반환
회원가입 구현
routes.ts
import { Express } from "express";
import handleUserCreate from "./controller/handleUserController";
export default function routes(app : Express) {
// 회원 가입 api
app.post("/api/register", handleUserCreate);
}
- 해당 api에 요청이 오면 콜백 함수 handleUserCreate(controller)가 실행
handleUserController.ts
import { Request, Response } from 'express';
import { searchUser, users } from "../db/user";
// api로 부터 요청 받을 경우, res로 받은 데이터를 db에 추가 후 성공 응답 남겨주는 함수
export default function handleUserCreate(req : Request, res : Response) {
const {email, password} = req.body;
// user의 request를 통해 받은 email을 통해 실제 유저가 있는지 검색하여 나온 값
const user = searchUser(email);
// 만약 email이 db에 조회된다면 401 에러를 리턴
if(user) {
return res.status(401).send("이미 등록된 사용자 입니다.");
}
// 만약 email이 db에 조회가 된다면 users db에 데이터 추가
users.push({
id : users.length + 1,
email,
password
})
// 응답으로 200 코드와 함께 회원가입 처리를 완료 시킴
console.log(users);
return res.status(200).send("회원가입 처리가 완료되었습니다.");
}
실행 결과
성공

- 해당 url로 요청을 보낼 경우 status code 200과 함께 회원가입 처리를 완료 시킴

- server 쪽 console을 확인해보면 데이터가 잘 들어간 것을 확인
실패

- 이미 등록된 사용자로 다시 요청을 보내게 됨
- 등록된 사용자에 한해 status code 401 에러와 메시지를 반환
- 데이터도 실제로 들어오지 않는 것을 확인
로그인 구현
JWT 토큰 발행 및 검증 (jwt.ts)
import jwt from "jsonwebtoken";
// jwt sign과 verify를 위한 secret key
const PRIVATE_KEY = `
-----BEGIN RSA PRIVATE KEY-----
- private key -
-----END RSA PRIVATE KEY-----`;
const PUBLIC_KEY = `
-----BEGIN PUBLIC KEY-----
- public key -
-----END PUBLIC KEY-----
`
export function signJWT(payload : object, expiresIn : string | number) {
// payload와 secret key, expire time을 인자로 넣어 jwt 토큰 생성
return jwt.sign(payload, PRIVATE_KEY, {algorithm : "RS256", expiresIn})
}
export function verifyJWT(token : string) {
try {
// 인자로 받은 token이 유효한지 확인하는 변수 (유효하다면 decoded가 존재)
const decoded = jwt.verify(token, PUBLIC_KEY);
// 유효하다면 payload에 decoded를 넣고 expired에 false로 리턴(만료되지 x)
return { payload : decoded, expired : false };
} catch (error : any) {
// 만약 유효하지 않다면 payload는 Null, expired엔 errorMessage를 담아 리턴
return { payload : null, expired: error.message.includes("jwt expired")};
}
}
- jwt 토큰을 발행하기 위한 secret key를 변수로 등록
- signJWT, verifyJWT를 위한 모듈
- JWT 토큰을 발행하기 위한 함수 signJWT
- token이 유효한지 확인하기 위한 함수 verifyJWT
routes.ts
import { Express } from "express";
import handleUserLogin from "./controller/handleUserLoginController";
export default function routes(app : Express) {
// 로그인 api
app.post("/api/login", handleUserLogin);
}
userSession.ts
interface IUserSession {
sessionId : string;
email : string;
valid : boolean
}
export const userSession : Record<string, IUserSession> = {};
// 유저의 세션 생성
export function createSession(email : string) {
// sessionId 설정
const sessionId = String(Object.keys(userSession).length + 1);
// 세션 변수
const session = {sessionId, email, valid : true};
// 만든 세션을 userSession에 추가
userSession[sessionId] = session;
return session;
}
handleUserLoginController.ts
import { NextFunction, Request, Response } from "express";
import { searchUser } from "../db/user";
import { createSession } from "../db/userSession";
import { signJWT } from "../utils/jwt";
// function for handle user login
export default function handleUserLogin(req : Request, res : Response, next : NextFunction) {
// user가 입력한 email, password를 변수로 저장
const {email, password} = req.body;
// email을 통해 user 정보 접근
const user = searchUser(email);
// 만약 db에 password와 id 정보가 없다면 401 리턴
if(!user) {
return res.status(401).send("등록된 아이디가 존재하지 않습니다.");
} else if (user.password !== password) {
return res.status(401).send("비밀번호가 유효하지 않습니다.");
}
// 입력한 email을 통해 session 생성
const session = createSession(email);
// access token과 refresh token 생성
// access token과 refresh token의 만료 주기는 각각 5분, 1년으로 설정
const accessToken = signJWT({
email : user.email, sessionId : session.sessionId
}, "5s")
const refreshToken = signJWT({
sessionId : session.sessionId
}, "1y");
// 쿠키에 accessToken과 refreshToken을 담음
res.cookie("accessToken", accessToken, {
maxAge : 300000, // 5분
httpOnly : true,
});
res.cookie("refreshToken", refreshToken, {
maxAge : 3.154e10, // 1년
httpOnly : true,
});
// 유저에게 session 반환
return res.status(200).send(session);
}
실행 결과

- 로그인 요청을 보내게 되면 해당 응답과 함께 로그인이 가능함

- access token과 refresh token이 클라이언트 측 cookie에 담긴 것을 확인
- httponly를 true로 함으로써, XSS 공격에 예방

- access token을 인코딩 해보면 유저의 정보가 잘 담긴 것을 확인 가능

- 만약 아이디가 존재하지 않는다면 401 에러과 메시지를 반환

- 만약 비밀번호가 다르면 역시 401에러와 함께 메시지를 반환