클라우드 13일차

soso·2024년 6월 26일

클라우드 부트캠프

목록 보기
15/77

salt

pw + salt를 붙여 암호화, salt는 물리적으로 다른 db에 저장해야 함
예를 들어 사용자 정보를 mongoDB에 저장한다면 salt는 mySQL에 저장

salt는 사용자마다 다르게 랜덤하게 발행해야 함(정보 탈취 방지)

const sha = require('sha256');
app.post('/signup', (req, res) => {
  console.log(req.body);
  console.log(sha(req.body.userpw));

  const crypto = require('crypto');
  const generateSalt = (length = 16) => {				//salt 16자로 랜덤 발행
    return crypto.randomBytes(length).toString("hex");
  };

  const salt = generateSalt();

  console.log(`Generated salt: ${salt}`);

  console.log('salt 없는 pw hash: ', sha(req.body.userpw));
  req.body.userpw = sha(req.body.userpw + salt);
  console.log('salt 있는 pw hash: ', req.body.userpw);

  mydb.collection('account')
    .insertOne(req.body)
    .then(result => {
      console.log('회원가입 성공');

      //MySQL에 salt를 저장
      const sql = `INSERT INTO UserSalt (userid, salt)
                  VALUES (?,?)`;    //?에 들어가는 data는 배열 인자에 들어감
      mysqlconn.query(sql, [req.body.userid, salt], (err, result2) => { // 인자 3개(sql문, 삽입할 데이터(여러개가 들어갈 수 있으므로 배열로 선언), callback 함수)
        if (err) {
          console.log(err);
        } else {
          console.log('salt 저장 성공');
        }
      });
    })
    .catch(err => {
      console.log(err);
    });

  res.redirect('/');
});

mysql에 salt 값 저장

이후 로그인 페이지에서 로그인 요청 시

  1. req.body.userid의 값이 db에 있는지 확인(mongoDB)

  2. 있다면 salt 값이 저장된 다른 db(MySQL)에서 salt 값을 불러옴

  3. db 안의 userid와 매치되는 userpw값과 sha(req.body.userpw + salt) 값이 일치하면 user 정보를 session에 저장 후 index.ejs로 렌더링, 아니면 login.ejs로 렌더링

app.post("/login", (req, res) => {
  mydb
    .collection("account")
    .findOne({ userid: req.body.userid })
    .then((result) => {
      let salt;
      const sql = `SELECT salt FROM UserSalt
                  WHERE userid=?`;
      mysqlconn.query(sql, [req.body.userid], (err, rows, fields) => {  // 콜백 함수 인자 : err, 결과 데이터들
        console.log(rows);
        salt = rows[0].salt;
        const hashPw = sha(req.body.userpw + salt);
        console.log(salt);
        console.log('result.userpw: ', result.userpw);
        console.log('hashPw: ', hashPw);
        if (result != null && result.userpw == hashPw) {
          req.body.userpw = hashPw;
          req.session.user = req.body;
          console.log("새로운 로그인");
          // res.send(`${req.session.user.userid}님 환영합니다`);
          res.render("index.ejs", { user: req.session.user });
        } else {
          //res.send("login fail");
          res.render("login.ejs");
        }
      });
    })
    .catch((err) => {
      console.log(err);
      res.status(500).send();
    });
});

passport

passport는 이름 그대로 서비스를 사용할 수 있게끔 해주는 여권 같은 역할을 하는 Node.js용 인증 미들웨어

클라이언트가 서버에 요청할 자격이 있는지 인증할 때에 passport 미들웨어 사용

npm install passport

Strategies(전략)

  • passport는 Strategy를 사용하여 사용자 요청에 대한 인증을 처리

  • 대표적인 전략

    • LocalStrategy : 사용자의 아이디와 패스워드를 직접 받아서 인증하는 방식

    • OAuth : 외부 서비스를 통해 엑세스 토큰을 발급받아 인증하는 방식, 예를 들어 카카오톡, 페이스북, 구글 등을 통한 로그인

    • OpenID Connect : OAuth 2.0을 기반으로 하여 ID 토큰을 발급받아 인증하는 방식, 주로 구글 로그인에서 사용됨

Sessions (serializeUser / deserializeUser)

  • 사용자가 로그인한 상태를 유지하고, 각 요청에서 사용자를 식별할 수 있도록 도와줌

  • serializeUser

    • 사용자가 로그인할 때 호출되며, 사용자 정보를 세션에 저장하는 역할

    • 여기서 저장되는 정보는 보통 사용자 ID와 같은 간단한 정보

  • deserializeUser

    • 이후 요청이 있을 때 호출되며, 세션에 저장된 정보를 이용해 사용자를 인증

    • 이 과정에서 데이터베이스 조회 등을 통해 사용자 정보를 완전히 복원할 수 있음

passport_local

기본 local은 session 방법을 사용하고 있기 때문에 express-session도 설치해야 함

npm install passport-local express-session

미들웨어 등록

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

app.use(passport.initialize());   //passport를 사용한다고 express에 알림
app.use(passport.session());      //session을 이용하여 passport를 동작

인증 라우터 등록

app.post('/login',					//'/login'으로 요청이 들어왔을 때
  passport.authenticate('local', {	//local 전략을 사용해 passport 실행
    //successRedirect: '/',			//인증 성공 시 '/'로 이동
    //failureRedirect: '/fail'		//인증 실패 시 '/fail'로 이동
  }),
  (req, res) => {					//밑의 자격 검증 성공 시(done(null, result)) 실행되는 콜백함수
    console.log(req.session);
    console.log(req.session.passport);
    res.render('index.ejs', { data: req.session.passport });
  });

자격검증(Strategies)

passport.use(new LocalStrategy(	//요청에 대한 자격 검증을 use()를 통해 제공
  {
    usernameField: 'userid',  	//form에서 전달해줄 id와
    passwordField: 'userpw',	//password의 이름을 입력
    session: true,
    passReqToCallback: false  //콜백 함수에서 request를 pass할거냐의 여부
  },
  function (inputid, inputpw, done) {	//inputid, inputpw를 인자로 받아 검증을 하는 콜백함수 실행
    mydb.collection('account')
      .findOne({ userid: inputid })
      .then(result => {
        if (result.userpw == inputpw) {   //result.userpw: DB, inputpw: input값
          console.log('새로운 로그인');
          done(null, result);			//검증이 유효하면 done(null, result)로 result에 대한 정보 리턴
        } else {
          done(null, false, { message: '비밀번호 틀렸어요' });	//검증에 실패하면 done(null, false)를 리턴하고 
        }													//실패한 이유를 메세지로 전달
      })
      .catch();
  }
));
  • local strategy 내부의 로직을 실행하고 나면 done이 마지막으로 실행

  • done의 두 번째 인자가 false가 아니라면 passport.serializeUser가 실행됨

Session

passport.serializeUser(function (user, done) {
  console.log('serializeUser');
  console.log(user);
  done(null, user.userid);		//세션에 사용자 ID를 저장
});

passport.deserializeUser(function (puserid, done) {
  console.log('deserializeUser');
  console.log(puserid);

  mydb.collection('account')
    .findOne({ userid: puserid })
    .then(result => {
      console.log(result);
      done(null, result);		//데이터베이스에서 사용자 정보를 조회하여 세션에 저장
    })
    .catch();
});

serializeUser : 로그인에 성공했을 때 유저 정보를 session에 저장하는 기능

로그인 성공 시

  • strategy에서 리턴한 result가 serializeUser에 user로 전달

  • session passport에 user값으로 user.userid(사용자의 식별자)가 저장

  • 검증에 필요한 페이지에 방문할 때마다 deserializeUser가 실행

  • deserializeUser에서는 session에 있는 사용자의 식별자를 받아서 데이터베이스에서 조회, 해당 사용자를 찾아 done(null, result) result를 done 함수에 2번째 인자로 전달하면 request.user 객체에 전달됨

로그인 판별

  • deserializeUser를 통해 request.user의 정보를 받을 수 있음

  • 로그인이 되었다면 request.user가 있을 것이고 로그인 되지 않았다면 request.user가 없을 것

https://velog.io/@suyeonpi/Node.js-Passport%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-passport-local

passport_facebook

https 구축

Win64OpenSSL v3.0.14 설치

설치 후 환경 변수 설정

cmd> openssl req -nodes -new -x509 -keyout server.key -out server.cert

(server.key와 server.cert 파일이 생성됨. 이것을 server.js와 같은 디렉토리에 저장)

facebook 개발자 센터에서 MyBoard라는 앱 생성

session 미들웨어 등록

const session = require("express-session");
app.use(
  session({
    secret: "암호화키",
    resave: false,				//매번 세션을 새로 발급받을지의 여부
    saveUninitialized: false,	//세션에 아무것도 들어있지 않을때에도 세션을 발급받을지의 여부
  })
);

passport 등록

const passport = require('passport');
app.use(passport.initialize());
app.use(passport.session());        //내 세션과 연동

facebook 인증

const FacebookStrategy = require('passport-facebook');

app.get('/facebook', passport.authenticate('facebook'));    //'/facebook' url로 get 요청할때 passport 인증받음

app.get('/facebook/callback', passport.authenticate('facebook', {
    successRedirect: '/',            //성공했을 때 '/'(index)페이지로
    failureRedirect: '/fail'         //실패했을 때 '/fail' 페이지로, 필수 요소는 아님
}),
    (req, res) => { }        //화살표 함수로 들어가지 않기 때문에 비워둠
);

facebook passport 사용

passport.use(new FacebookStrategy({     //여권 사용, 모듈을 생성자로 호출
    clientID: "앱 ID",
    clientSecret: "앱 시크릿 코드",
    callbackURL: "/facebook/callback",          //임의로 설정, 단 맨 앞에 '/'가 있어야 함
}, function (accessToken, refreshToken, profile, done) {         //function 안의 작업이 성공하면 위의 succesRedirect로 이동/done은 콜백을 호출하는 인자/accessToken, refreshToken은 JWT
    console.log('2', profile);
    const authkey = 'facebook' + profile.id;      //profile 안의 id, displayName은 facebook에서 profile을 통해 준 정보
    const authName = profile.displayName;

    mydb.collection('account')
        .findOne({ userkey: authkey })              //사용자 정보를 못찾으면(없으면) 저장(중복 저장 피하기 위해)
        .then(result => {
            console.log('3', result);
            if (result != null) {       //연결하여 인증을 받았던 경험이 있으면
                console.log('3-1 페이스북 사용자를 우리 DB에서 찾았음');
                done(null, result);     //err일 시 null, 아니면 result를 반환해 다음 흐름으로 넘어가겠다
            } else {
                console.log('3-1 페이스북 사용자를 우리 DB에서 못찾았음');
                mydb.collection('account')
                    .insertOne({
                        userkey: authkey,
                        userid: authName
                    })
                    .then(insertResult => {
                        if (insertResult != null) {
                            console.log("3-2 페이스북 사용자를 우리 DB에 저장 완료");
                            mydb.collection('account')
                                .findOne({ userkey: authkey })
                                .then(result2 => {
                                    if (result2 != null) {
                                        console.log("3-3 페이스북 사용자를 우리 DB에 저장 후 다시 찾았음");
                                        done(null, result2);
                                    }
                                })
                                .catch(err => {
                                    console.log(err);
                                })
                        }
                    })
                    .catch(err => {
                        console.log(err);
                    });
            }
        })
        .catch(err => {
            console.log(err);
        });
}));

Session

passport.serializeUser((user, done) => {
  try{				//함수 안에 들어온 것을 확인
    console.log('4 serializeUser', user);
    done(null, user);
  } catch(err) {
    console.log(err);
  }
});

app.get('/', (req, res) => { 	//첫 페이지 요청 시
  console.log("'/'요청");
  try {				//serialize 된 상태로 왔을수도 있기 때문에 session에 passport가 있는지 확인
    console.log("1", req.session.passport);
    if(typeof req.session.passport != undefined	//passport도 undefined가 아니고 
    	&& req.session.passport.user) {	   		//user정보도 passport에 있을 때   
      res.render("index.ejs", {data: req.session.passport}); //passport를 가지고 index.ejs로 렌더링  
    } else{						//passport가 없는 경우(브라우저를 막 띄운 상태)
	    res.render('index.ejs', {data:null});	//data가 null인 상태로 index.ejs 렌더링
    }
  } catch (err) {
    console.log('1-1 '. err);
    res.render('index.ejs', {data:null});	//에러가 나도 data가 null인 상태로 index.ejs 렌더링
  }
});


passport.deserializeUser((user, done) => {
    console.log('5, deserializedUser');
    mydb
        .collection("account")
        .findOne({ userkey: user.userkey })
        .then((result) => {
            console.log(result);
            done(null, result);
        }); //user는 이미 passport에 있는 객체라서 이렇게 매번 DB에 가서 확인할 필요가 전혀없다!
});

개인 과제 : 회원가입 시 중복된 ID 판별

signup.ejs

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Home</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>

<body>
  <%- include('menu.html') %>

    <div class="container mt-4">
      <form action="/signup" method="post">
        <div class="form-group">
          <label>아이디</label>
          <input type="text" name="userid" class="form-control userid">
        </div>
        <p></p>
        <div><a href="#" class="btn btn-primary idCheck">중복확인</a></button>
          <p></p>
        </div>


        <div class="form-group">
          <label>비밀번호</label>
          <input type="password" class="form-control" name="userpw">
        </div>
        <p></p>

        <div class="form-group">
          <label>소속</label>
          <input type="text" class="form-control" name="usergroup">
        </div>
        <p></p>

        <div class="form-group">
          <label>이메일</label>
          <input type="text" class="form-control" name="useremail">
        </div>
        <p></p>

        <button type="submit" class="btn btn-primary" style="float:right" disabled>가입</button>
      </form>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
      crossorigin="anonymous"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

    <script>
      $('.idCheck').click(function (event) {
        event.preventDefault();
        let userid = $('.userid').val();
        console.log(userid);

        $.ajax({
          async: true,
          type: 'post',
          url: '/idcheck',
          data: JSON.stringify({ userid: userid }),
          contentType: 'application/json',
          dataType: 'json',
          success: function (data) {
            if (data.idcheck == 0) {
              alert('사용할 수 있는 아이디입니다');
              $('button[type="submit"]').prop('disabled', false);
            } else {
              alert('중복된 아이디입니다');
              $('button[type="submit"]').prop('disabled', true);
            }
          },
          error: function (err) {
            alert('error: ', err);
          }
        });
      });
    </script>
</body>

</html>

server.js

app.use(express.json());

app.post('/idcheck', async (req, res) => {
  const exist = await mydb.collection('account')
    .findOne({ userid: req.body.userid });

  console.log('req에서 받은 userid', req.body.userid);
  console.log('db에서 찾은 userid', exist);

  if (exist) {
    res.json({ idcheck: 1 });
  } else {
    res.json({ idcheck: 0 });
  }
});

db에 존재하는 id를 중복 확인 시

db에 존재하지 않는 id를 중복 확인 시

오류 해결
클라이언트와 서버 사이를 오가는 data를 JSON 형식으로 보냈지만 express.json() 미들웨어를 등록하는 것을 잊어 서버에서 JSON 데이터가 파싱되지 않아 req.body.userid 값은 undefined, db에 있는 account.userid 값은 null이 되는 문제가 있었다

1개의 댓글

comment-user-thumbnail
2024년 6월 27일

멋져요!

답글 달기