React, Typescript Express로 JWT 구현 - (2) server & db setting, 회원가입, 로그인 구현

jiny·2022년 8월 29일
0
post-thumbnail

서버 세팅

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);
}
  • 로그인 api를 위한 로직

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);
}
  • 다음의 로직을 통해 login 컨트롤러 구현

실행 결과

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

  • access token과 refresh token이 클라이언트 측 cookie에 담긴 것을 확인
  • httponly를 true로 함으로써, XSS 공격에 예방
  • access token을 인코딩 해보면 유저의 정보가 잘 담긴 것을 확인 가능

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

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

0개의 댓글