HTML,CSS,JS,MongoDB를 활용한 풀 스택 프로젝트(취미 공유 플랫폼) API

Hyun·2023년 5월 25일
0

프로젝트

목록 보기
2/2

프로젝트를 진행하면서 생성한 API와 로직에 대해 설명을 하겠다.

1. 암호화 함수

const crypto = require("crypto");
// 솔트 생성
const createSalt = () => {
    return new Promise((resolve, reject) => {
      crypto.randomBytes(64, (err, buf) => {
        if (err) reject(err);
        resolve(buf.toString("base64"));
      });
    });
  };
  // 암호비번 생성
  const createCryptoPassword = async (plainPassword) => {
    console.log(">>>>>>>>>>", plainPassword);
    const salt = await createSalt();
    return new Promise((resolve, reject) => {
      crypto.pbkdf2(plainPassword, salt, 10000, 64, "sha512", (err, key) => {
        if (err) {
          console.log("createCryptoPassword 에서 에러 발생");
          reject(err);
        }
        resolve({ password: key.toString("base64"), salt });
      });
    });
  };
  // 비밀번호 검증
  const getCryptoPassword = (plainPassword, salt) => {
    return new Promise((resolve, reject) => {
      crypto.pbkdf2(plainPassword, salt, 10000, 64, "sha512", (err, key) => {
        if (err) {
          console.log("getCryptoPassword 에서 에러 발생");
          reject(err);
        }
        resolve({ password: key.toString("base64"), salt });
      });
    });
  };
module.exports = {createSalt, createCryptoPassword, getCryptoPassword} 
  • 사용자가 전달한 암호는 createCryptoPassword에서 createSalt로 생성한 Salt값을 활용하여 같이 sha-512 해쉬 알고리즘으로 해쉬화가 된다.

  • getCryptoPassword는 사용자가 입력한 암호와 DB에 저장되어 있는 Salt값을 이용하여 위와 동일하게 해쉬화 한다.

2. GET

1) 렌더링(5개)

exports.RenderMain = (req,res) =>{
    res.render('main')
}
// 로그인 페이지
exports.RenderLogin = (req, res) => {
    res.render("login");
};

// 회원가입 페이지
exports.RenderSignup = (req, res) => {
    res.render("signup");
};

// 방 생성 페이지
exports.RenderMakeRoom = (req,res)=>{
    res.render('makeRoom')
}

// 채팅 페이지
exports.RenderChat = (req, res) => {
    res.render("chatRoom");
};
router.get('/makeRoom',logined,controller_render.RenderMakeRoom)
router.get('/chat',controller_render.RenderChat)

여기서 방 생성 페이지채팅 페이지passport를 사용하여 로그인을 해야만 접근이 가능하게 해주었다.

  1. /getData : DB 데이터를 받아옴
// 메인페이지(초기 데이터 삽입)
exports.GetData = async (req,res) =>{
    const result = await Room.find({})
    res.send({ data: result })
}

MVC로 분리하기 전에는 mongodb를 사용하였는데 mongoose를 사용하니 findAll 메소드를 사용할 수 없다는 것을 알게 되었고 find({})를 하면 전체 데이터를 불러올 수 있다는 사실을 알게 되었다.

  1. /DetailedPage/:id : 방 정보 데이터 불러오기 및 페이지 렌더링
// 방 정보가 담긴 예약 페이지
exports.DetailPage = async (req, res) => {
    let getId = req.params.id;
    console.log("getId:", getId);
    await Room.findOne({ _id: ObjectId(getId)}).then((result)=>{
        console.log("방정보", res);
        res.render("DetailedPage", { data: result});
    })
};

여기서 idDB에 저장된 방에 대한 id인데 RESTful적으로 id보다는 room-number가 더 좋은 것 같다.
Front 측에서 전달한 params(id)DB에 저장된 Rooom 스키마ID를 비교하여 같은 ID를 가진 데이터data라는 이름의 객체로 전달하면서 DetailedPage렌더링 해준다.

  1. /logout : 로그아웃
// 로그아웃
exports.Logout = (req,res,next)=>{
    req.logout((err)=>{
        if(err){
            return next(err)
        }
        else{
            req.session.destroy(()=>{
                res.clearCookie('connect.sid')
                res.redirect('/main')
            })
        }
    })
}

passport에서 로그인에 성공하면 세션이 생성되고 브라우저에는 connet.sid라는 이름의 쿠키가 생성이 된다. 그렇기 때문에 로그아웃 버튼을 누르게 되면 /logout하여 세션을 지우고 또한 쿠키도 제거하여 메인 페이지로 이동하게 하였다.

  1. 로그인 전
  2. 로그인 후
  3. 쿠키 생성

3. POST

  1. /signup : 회원가입
// 회원가입
exports.Signup = async (req, res) => {
    const r = req.body;
    console.log(r)
    let cryptoPassword = await createCryptoPassword(r.pw);
    const result = await User_Info.find({id : r.id})
    console.log('res!: ',result)
    if(result.length===0){
        User_Info.create(
            {
              name: r.name,
              birthday: r.birthday,
              email: r.id,
              pw: cryptoPassword.password,
              salt: cryptoPassword.salt,
              gender: r.gender,
            }).then((re)=>{
                console.log('유저정보 저장 완료')
            }).catch((err)=>{
                console.log(err)
            })
        res.redirect("/login");
    }
    else{
        console.log("중복자 발견");
        res.send("<script>location.href='/signup'; alert('ID가 중복되었어요!');</script>");
    }
};

Front에서 전달받은 값들은 req.body에 담기게 된다.
거기서 pw(비밀번호)를 1번에 있는 createCryptoPassword 함수를 이용하여 비밀번호 평문해쉬화한다. 그 다음 사용자가 전달한 id(email)중복 여부를 체크하고 만약 중복자가 없다면 result빈 배열을 반환한다.
그렇기 때문에 .length를 사용하면 길이가 0이 되어 전달받은 값해쉬화된 비밀번호DB에 저장하고 login 페이지로 이동한다.
만약 중복자가 있으면 id가 중복되었다는 메시지Front에게 보여준다.

  1. /make/room : 예약 생성
// 방 생성
exports.MakeRoom = async (req,res)=>{
    const r = req.body
    console.log(r)
    let img_src;
    switch (r.category) {
        case "축구":
            img_src = "../static/img/soccer.jpg";
            break;
        case "야구":
            img_src = "../static/img/baseball.jpeg";
            break;
        case "농구":
            img_src = "../static/img/basketball.jpeg";
            break;
        case "배구":
            img_src = "../static/img/volleyball.jpeg";
        default:
            break;
    }

    if(r.title=='' || r.date=='' || r.location=='' || r.personnel=='' || r.price=='' || r.category=='') {
        res.send("<script>location.href='/makeRoom'; alert('제대로 입력하세요!');</script>");
    }
    else{
        await Room.create({
            src: img_src,
            title: r.title,
            date: r.date,
            location: r.location,
            personnel: r.personnel,
            price: r.price,
            category: r.category,
        }).then((result)=>{
            console.log('방정보 저장')
            res.redirect('/main')
        }).catch((error)=>{
            console.log(error)
        })
    }    
}

Front에서 전달받은 값들은 req.body.category를 보고 각 스포츠에 맞는 이미지 주소img_src 변수에 넣는다.
그 다음 하나라도 빈값이 있으면 다시 입력하라는 메시지makeRoom(예약 생성)페이지로 이동시킨다. (Front에서 required를 사용하여 방지할 수 있을 것 같다.)
만약 제대로 다 입력을 하였다면 req.body의 정보들DB의 Room 스키마에 저장하게 된다.

  1. /login : 로그인
exports.Login = (req,res,next)=>{
    passport.authenticate('local',(err,user,info)=>{
        if(err){
            console.error(err)
            return next(err)
        }
        if(!user){
            return res.send("<script>location.href='/login'; alert('비밀번호가 일치하지 않습니다!');</script>");
        }
        return req.login(user,(loginError)=>{
            if(loginError){
                console.log(loginError)
                return next(loginError)
            }
            return res.redirect('/main')
        })
    })(req,res,next)
}

Strategy 종류 (로그인 인증 방식)
1) Local Strategy(passport-local) : 로컬 DB에서 로그인 인증 방식
2) Social Authentication (passport-kakao, passport-twitter 등) : 소셜 네트워크 로그인 인증 방식

여기서는 Local Strategy를 사용하였다.

로그인 요청이 들어오면 passport.authenticate() 호출한다.
이때 authenticate는 passport/localStrategy.js 호출한다.

module.exports = ()=>{
 passport.use(new LocalStrategy({
   usernameField : 'id',
   passwordField : 'pw',
 }, async(user_id,user_pw,done)=>{
   try{
     let result = await User_Info.findOne({email:user_id})
     if(result){
       const getcry = await getCryptoPassword(user_pw,result.salt)
       if(getcry.password == result.pw){
         done(null,result)
       }
       else{
         done(null,false,{message:'wrong password'})
       }
     }
     else{
       done(null,false,{message:'not exist'})
     }
   }
   catch(err){
     console.error(err)
     done(err)
   }
 }
 ))
}

여기서 요청된 id와 pwuser_id, user_pw가 되고 DB에 저장된 email과 user_id를 비교하여 데이터가 있다면 result에 담기고 없다면 done(null,false,{message:'not exist'}) 해준다.
result에 담긴 salt전달받은 user_pwgetCryptoPassword를 이용하여 얻은 해쉬화된 값DB에 저장된 해쉬화된 비밀번호를 비교하여 같다면 done(null,result), 틀리다면 done(null,false,{message:'wrong password'}) 해준다.

로그인을 실행하고, done()을 호출하면, 다시 passport.authenticate() 돌아가서 다음 미들웨어를 실행한다.
=> (err,user,info) 부분

done()정보를 토대로, 로그인 성공 시 사용자 정보 객체와 함께 req.login()를 자동으로 호출한다.

module.exports = ()=>{
    // id를 이용해 세션을 저장(로그인 성공시 발동)
    passport.serializeUser((user, done)=>{
        done(null, user.id);
    });
    passport.deserializeUser((id,done)=>{
        User_Info.findOne({_id : ObjectId(id)})
        .then((user) => {
            done(null,user)
        })
        .catch(err=>done(err))
    })
    local()
};

1) req.login 메서드passport.serializeUser() 호출한다.
2) req.session사용자 아이디 키값 저장
3) passport.deserializeUser()로 바로 넘어가서 DB 조회후 req.user 객체를 등록하고 done() 반환하여 req.login 미들웨어로 다시 되돌아간다.
4) 미들웨어 처리후, res.redirect('/main')을 응답하면, 세션쿠키를 브라우저에 보내게 된다.
5) 로그인 완료 처리 (이제 세션쿠키를 통해서 통신하며 로그인됨 상태를 알 수 있다.)

로그인 이후

  1. 모든 요청에 passport.session() 미들웨어가 passport.deserializeUser() 메서드를 매번 호출한다.
// 세션 및 쿠키 세팅
app.use(
  session({
    resave: false,
    saveUninitialized:false,
    secret:process.env.COOKIE_SECRET,
    cookie: {
      httpOnly : false,
    }
  })
)
app.use(passport.session())
  1. deserializeUser에서 req.session에 저장된 아이디로 DB에서 사용자 조회
module.exports = ()=>{
    // id를 이용해 세션을 저장(로그인 성공시 발동)
    passport.serializeUser((user, done)=>{
        done(null, user.id);
    });
    passport.deserializeUser((id,done)=>{
        User_Info.findOne({_id : ObjectId(id)})
        .then((user) => {
            done(null,user)
        })
        .catch(err=>done(err))
    })
    local()
};
  1. 조회된 사용자 전체 정보req.user 객체에 저장한다.
  2. 그 후 라우터에서 req.user를 공용적으로 사용 가능하게 된다.

로그인 이후에만 접근

exports.logined = (req, res, next)=>{
  console.log(req.user)
  if (req.user) {
    next();
  }else{
    res.send("<script>location.href='/main'; alert('로그인하세요')</script>");
  }

공용적으로 사용 가능 해진 req.user를 이용하여 req.user가 존재하면 next()하여 다음 과정이 진행되지만 그렇지 않을 경우 로그인하라는 alert가 나온다.

const {logined} = require('../routes/Auth')
router.get('/makeRoom',logined,controller_render.RenderMakeRoom)
router.get('/DetailedPage/:id',logined,controller_room.DetailPage)
router.post('/make/room',logined,controller_room.MakeRoom)

logined 함수를 불러와 라우터의 미들웨어로 넣을 경우 로그인 한 사람만 해당 라우터에 접근이 가능하다.

0개의 댓글