Node.js

node> 회원기능 만들기 (passport, 로그인기능,세션,가입기능, connect-mongo)

연습노트 2024. 7. 26. 08:57

사이트에 회원기능이 필요하면 회원기능 만들면 되는데

많은 상황에서 가장 기본은 할 수 있는 인증방식이 바로 역사와 전통의 session 방식 회원인증이기 때문에

session 방식으로 회원기능을 구현해봅시다. 

 

직접 처음부터 쌩으로 코드짜서 구현하려면

대충 이런 스텝으로 코드를 짜면 되는데

 

1. 가입기능부터 만듭니다.

유저가 입력한 아이디/비번을 DB에 저장해두는게 가입기능 끝 아닙니까

 

2. 로그인기능 만들면 됩니다. 

로그인폼에서 아이디/비번을 제출하면 DB에 있는거랑 일치하는지 확인하고 일치하면 세션을 하나 만들어줍니다.

 

3. 세션이뭐냐면 그냥 DB에 document 하나 발행해주는 것일 뿐입니다.

document에 "어떤 유저가 로그인했었고 걔 유효기간은 1월 30일까지다~" 이런거 기록해둡니다. 

 

4. 세션 document의 _id같은걸 가져와서 

유저에게 전송해서 유저 브라우저 쿠키에 강제 저장시켜줍니다. 입장권생성임

로그인 기능 끝 

 

5. 이제 유저가 로그인이 필요한 페이지같은거 방문할 때 마다

서버는 유저가 제출한 쿠키를 까보고 

쿠키에 기록되어있는 _id가 실제로 DB에 있는지 확인하고 유효기간도 안지났으면 페이지 보여주면 됩니다. 

 

여기서 모르는건 쿠키 만들어주는거 정도일 뿐이라 이거 찾아보면 알아서 만들 수 있을 것 같은데

하지만 직접 코드짜면 너무 오래걸리기 때문에 라이브러리를 써봅시다. 

 

passport라는 라이브러리를 써볼 것인데

이거 쓰면 논리가없고 지성이 없어도 OAuth, JWT, session 기능을 복붙식으로 쉽게 구현할 수 있습니다.

다른 라이브러리도 많은데 passport가 인터넷에 예시가 가장 많아서 초보에게 좋음 

 

npm install express-session passport passport-local 

터미널 열어서 이거 설치합시다. 

passport는 회원인증 도와주는 메인라이브러리,

passport-local은 아이디/비번 방식 회원인증쓸 때 쓰는 라이브러리

express-session은 세션 만드는거 도와주는 라이브러리입니다.

 

 

const session = require('express-session')
const passport = require('passport')
const LocalStrategy = require('passport-local')

app.use(passport.initialize())
app.use(session({
  secret: '암호화에 쓸 비번',
  resave : false,
  saveUninitialized : false
}))

app.use(passport.session()) 

server.js 상단에 복사붙여넣기 하면 passport 라이브러리 셋팅 끝입니다. 

참고로 app.use()가 3개 있는데 순서틀리면 좀 이상해질 수 있습니다. 

 

- session() 안에 언제 어떻게 세션을 만들지 설정할 수 있는데 

- secret : 안에는 여러분만의 비번 잘 넣어주면 됩니다. 세션문자열같은거 암호화할 때 쓰는데 긴게 좋습니다. 털리면 인생 끝남

- saveUninitialized : 는 유저가 로그인 안해도세션을 저장해둘지 여부 (false 추천)

- resave는 유저가 요청날릴 때 마다 session데이터를 다시 갱신할건지 여부 (false 추천)

이런 설정이 가능합니다. 

나중에 뭔가 바꾸고 싶으면 express session 라이브러리 사용법을 찾아봅시다. 

 

 

 

 

1. 가입기능만들기 

 

회원가입 기능이 뭐죠

유저가 아이디 비번을 서버로 보내면 DB에 그걸 저장해두면 끝 아닙니까 

가입기능 만드는건 쉬우니까 여러분들이 집가서 직접 시간나면 해보고 

지금은 시간이 없으니까 그냥 손으로 아이디 비번 한 쌍을 직접 DB에 만들어줍시다. 

 

 

 

▲ DB들어가서 user라는 이름의 컬렉션 하나 만들고

username : test

password : 1234

이런 document 하나 발행해봅시다. 한 놈 강제가입 끝 

- String 타입이 좋을 것 같군요. 

- 원래 비번은 암호화(해싱)해서 저장하는게 좋습니다 그래야 DB가 털려도 비번은 알 수 없어서 좋은데 지금은 생략합시다.  

 

 

 

 

 

2. 로그인기능 만들기 

 

로그인 기능은 어려운게 아니고

누가 아이디/비번을 서버로 보내면 

DB에 있던거랑 비교해보고 일치하면 세션 document를 만들어주는게 전부입니다. 

 

그래서 유저가 로그인 요청할 수 있는 폼부터 하나 만들어봅시다. 

ejs 페이지 하나 만들고 아이디/비번 전송 폼을 마련해두면 되겠군요. 

 

app.get('/login', (요청, 응답)=>{
  응답.render('login.ejs')
}) 

어떤 놈이 /login페이지 방문하면 login.ejs 파일 보내줍시다.

 

<form class="form-box" action="/login" method="POST">
    <h4>로그인</h4>
    <input name="username">
    <input name="password" type="password">
    <button type="submit">전송</button>
</form> 

login.ejs 레이아웃은 이렇게 만들었습니다.

여기서 전송누르면 /login으로 POST 요청이 가는데

username이랑 password 라는 이름으로 데이터 2개가 전송되겠네요 

이제 /login으로 오는 POST 요청을 수신만 하면 될거같은데 

 

 

 

app.post('/login', async (요청, 응답, next) => {
  제출한아이디/비번이 DB에 있는거랑 일치하는지 확인하고 세션생성
}) 

이렇게 코드짜면 로그인 기능 끝입니다. 

근데 직접 구현은 귀찮으니까 passport 라이브러리를 씁시다. 

 

 

 

passport.use(new LocalStrategy(async (입력한아이디, 입력한비번, cb) => {
  let result = await db.collection('user').findOne({ username : 입력한아이디})
  if (!result) {
    return cb(null, false, { message: '아이디 DB에 없음' })
  }
  if (result.password == 입력한비번) {
    return cb(null, result)
  } else {
    return cb(null, false, { message: '비번불일치' });
  }
}))

app.use 많은 곳 하단 쯤에 복사붙여넣기 합시다. 라이브러리 사용법이라 그냥 복붙해서 쓰면 끝입니다. 

new LocalStrategy 어쩌구는 아이디/비번이 DB와 일치하는지 검증하는 로직 짜는 공간입니다.

이거 짜놓으면 앞으로 유저가 제출한 아이디 비번이 DB랑 맞는지 검증하고 싶을 때 이 코드를 실행시키면 되는데 

실행시키는 방법은 API 안에서 passport.authenticate('local') 이런 코드 작성하면 요 코드가 자동으로 실행됩니다. 

(참고) 이 코드 하단에 API들을 만들어야 그 API들은 로그인관련 기능들이 잘 작동합니다.

 

 

 

위의 new LocalStrategy 어쩌구 코드를 잠깐 설명하자면

(1) 누가 아이디/비번을 제출하면 함수 파라미터로 자동으로 신기하게 들어옵니다.

<input name=”username”>  <input name=”password”>

실은 이런 name을 가진 input태그들을 만들고 거기서 아이디/비번을 입력해야 여기로 잘 도착합니다.

 

(2) 유저가 보낸 아이디/비번과 DB에 있던걸 비교해봅니다.

아이디나 비번이 일치하지 않으면 false를 cb() 안에 넣어주고

일치하면 유저 정보를 cb() 안에 넣어줍니다. 

에러메세지도 넣을 수 있음 

참고로 아이디/비번 말고 다른 것도 검증하고 싶으면 passReqToCallback 옵션 찾아보면 요청.body같은걸 저 코드 안에서 사용가능합니다.

 

 

아무튼 방금 적은게 아이디/비번이 DB와 일치하는지 검증하는 코드인데 이거 언제 실행시키고 싶어요? 

유저가 아이디/비번 제출할 때 실행시켜보면 되겠죠? 

 

 

app.post('/login', async (요청, 응답, next) => {

  passport.authenticate('local', (error, user, info) => {
      if (error) return 응답.status(500).json(error)
      if (!user) return 응답.status(401).json(info.message)
      요청.logIn(user, (err) => {
        if (err) return next(err)
        응답.redirect('/')
      })
  })(요청, 응답, next)

}) 

passport.authenticate('local', 콜백함수)(요청, 응답, next) 이런 코드 작성하면 아까 그 검증하는 코드가 실행됩니다.

 

검증 성공이나 실패시 뭔가 실행하고 싶으면 콜백함수 안에 작성해주라고 하는군요.

 

- 콜백함수의 첫째 파라미터는 뭔가 에러시 뭔가 들어옴

- 둘째 파라미터는 아이디/비번 검증 완료된 유저정보가 들어옴

- 셋째는 아이디/비번 검증 실패시 에러메세지가 들어옴

 

그래서 예외처리는 대충 위처럼 하면 되겠습니다.

이것도 이해보다는 라이브러리 사용법이라 복붙의 영역입니다. 

잘 이해하고 싶으면 파라미터들이 진짜로 그런 정보들이 들어오는지 출력정도만 해보면 됩니다.

 

DB에 document 하나 만들려면

 

await db.collection('post').insertOne(저장할데이터) 

어떤 데이터를 DB에 저장하고 싶으면 이런 코드 작성하면 됩니다.

어떻게 알았냐고요? 검색해봤습니다. 

이러면 post라는 컬렉션에 새로운 document를 하나 만들어서 이 데이터를 안에 기록해줍니다.

데이터는 object자료형식으로 집어넣으면 됩니다. 

 

 

 

await db.collection('post').insertOne({ a : 1 }) 

예를 들어 이렇게 작성하고 실행하면 

 

 

▲ mongodb 사이트가서 확인해보면 

이런 식으로 document 하나가 생성되고 a : 1 이 그대로 저장되어있습니다.

(_id는 자동발행됩니다)

아무튼 테스트 삼아 해본거니까 삭제하고 

 

그럼 .insertOne() 안에 유저가 작성한 글을 넣으면 저장이 잘 될거같은데

이거 .insertOne(요청.body) 그대로 막 이렇게 넣습니까?

정확히 어떤 형식으로 집어넣어야됩니까?

 

 

▲ 지금 DB를 보면 글들이 이런 식으로 저장되어있습니다. 

이 document들과 유사하게 저장하는게 좋을 것 같기 때문에 

{title : 어쩌구, content : 어쩌구}

이런 식으로 저장하는게 좋겠죠? 

 

 

 

app.post('/add', async (요청, 응답) => {
  await db.collection('post').insertOne({ title : 요청.body.title, content : 요청.body.content })
  응답.send('작성완료')
})

그래서 이렇게 저장하라고 했습니다.

실은 요청.body 출력해보면 {title : 어쩌구, content : 어쩌구} 이거랑 똑같이 나오기 때문에 

요청.body를 그대로 .insertOne()에 넣어도 될거같긴 한데

근데 그럴 경우 유저가 이상한 데이터를 보내버리면 그걸 그대로 document에 작성해버리기 때문에 위험할 수도 있을 것 같군요. 

 

그래서 아무튼 저장하고 테스트해보면 

이제 전송버튼누르면 글이 DB에 저장됩니다. 성공 

 

 

 

app.post('/add', async (요청, 응답) => {
  await db.collection('post').insertOne({ title : 요청.body.title, content : 요청.body.content })
  응답.redirect('/list')
})

응답.send()로 메세지 보내는게 싫으면

응답.redirect() 이런거 써도 됩니다. 그러면 다른 페이지로 강제로 이동시켜줍니다. 

 

 

 

 

 

 

예외처리하는 법

 

유저가 제목을 안적고 글을 전송하면 어쩔 것입니까.

한번 테스트로 글 전송 해보면 요청.body.title 부분이 ' ' 이렇게 비어있군요.

이 경우엔 DB에 저장시키면 안될 것 같군요.

그러고 싶으면 "제목이 비어있으면 DB저장하지말기~" 이거 그대로 코드로 번역하면 되는 것일 뿐입니다. 

 

 

app.post('/add', async (요청, 응답) => {
  if (요청.body.title == '') {
    응답.send('제목안적었는데')
  } else {
    await db.collection('post').insertOne({ title : 요청.body.title, content : 요청.body.content })
    응답.redirect('/list') 
  }
  
})

특정조건에 만족하는 경우에 어떤 코드를 실행하고 싶을 때는 if문 쓰면 됩니다.

그럼 내용도 빈칸인지 검사하고 싶으면 어떻게 할까요?

제목이 100자 이상으로 너무 길면 어쩌죠? 

이런 것들도 전부 알아서 if 문으로 처리하면 되겠습니다. 

 

참고로 하나하나 if문 쓰기 귀찮으면 validation 라이브러리를 설치해서 쓰는 사람도 있습니다.

express-validator, vinejs, validator 이런 것들이 있습니다. 

 

 

 

 

DB에 뭔가 저장할 때 몇개의 에러가 발생할 수 있습니다. 

- DB가 다운되어서 통신이 안되거나 

- DB에 뭔가 저장하려는데 _id가 똑같은게 있어서 에러가 나거나 

그런 경우 에러같은게 발생합니다.

에러가 나는 경우에 특정 코드를 실행하고 싶으면 try catch 문법을 쓰면 됩니다.

 

 

try {
   await db.collection('post').insertOne(어쩌구)
} catch (e) {
   console.log(e)
   응답.send('DB에러남')
} 

이건 try 안에 있는 코드가 뭔가 에러나면 catch 안에있는 코드를 대신 실행해주는 유용한 문법입니다. 

catch 안에서 e라는 파라미터 출력해보면 에러 원인 같은 것도 알 수 있습니다. 

그래서 이런 try, catch 문법도 추가해주면 더 안전하고 뛰어난 서버코드를 작성할 수 있으니까

집가서 코드를 업그레이드 해옵시다. 

 

 

 

 

배운거 정리하자면 : 

1. 코드혼자 잘짜고 싶으면 한글로 기능이 어떻게 동작하는지 설명부터하고 그걸 코드로 번역

2. <form>태그쓰면 서버로 POST요청할 수 있고 유저가 입력한데이터도 전송할 수 있음 

3. 서버에서 요청.body쓰면 유저가 폼에 입력한 데이터출력해볼 수 있음 

4. DB에 데이터저장하려면 .insertOne()

5. 그리고 예외처리는 if문이나 try, catch 

 

 

 

세션 만들기

 

로그인 성공시 세션을 만들어주면 되는데 세션이 뭐랬습니까

{ 유저아이디 : 어쩌구, 유효기간 : 저쩌구 }

이런게 적힌 document일 뿐입니다.

 

세션을 만들어주는 코드는 직접 짤 필요없이 

passport.serializeUser() 라는 코드 적어두면

- 유저가 로그인 성공할 때 마다 자동으로 세션이 만들어집니다.

- 그리고 그 세션 document의 _id가 적힌 쿠키를 하나 만들어서 유저에게 보내줄겁니다.

- 센스있게 세션 _id는 암호화해서 전달해줍니다.

다 자동이라 딱히 할게 없음 

 

 

 

passport.serializeUser((user, done) => {
  process.nextTick(() => {
    done(null, { id: user._id, username: user.username })
  })
})

new LocalStrategy 어쩌구 하단에 복사붙여넣기 하면 됩니다. 

요청.login() 이라는 함수가 실행되면 자동으로 동작하는 코드고 위에서 설명한 것 처럼 세션을 알아서 만들어줍니다. 

 

- done() 함수의 둘째 파라미터에 적은 정보는 세션 document에 기록됩니다.

유효기간 이런건 알아서 기록해주기 때문에 유저의 _id 아니면 username 이런걸 적어둡시다. 

user라는 파라미터 출력해보면 DB에 있던 유저정보 꺼내쓸 수 있음 (new LocalStrategy에서 보내줌)

- 근데 정확히 말하면 아직 passport에 DB연결을 안해놨기 때문에 DB말고 컴퓨터 메모리에 세션이 저장됩니다. 

 

 

process.nextTick 이게 뭐임

 

 

 

 

 

세션 유효기간 설정가능

 

아무 설정을 안해놓으면 기본적으로 세션 document 유효기간을 2주로 설정해줍니다.

그니까 한 번 로그인하면 유저가 2주동안 로그인을 유지할 수 있다는 겁니다. 

이게 아니꼬우면 설정을 마음대로 바꿀 수도 있는데 

상단에 app.use(session({ }) 이런 코드를 찾아가봅시다. 

 

 

app.use(session({ 
  secret : '어쩌구',
  resave : false,
  saveUninitialized : false,
  cookie : { maxAge : 60 * 60 * 1000 }
}) 

cookie 항목을 만들어서 ms 단위로 유효기간을 설정가능합니다. 

위처럼 하면 1시간 유지해주는데

1개월 이상으로 길게 유지해주는 사이트들도 많습니다.  

 

 

 

 

 

 

 

실은 deserializeUser도 써놔야 잘됩니다

 

그럼 이제 유저가 뭔가 서버로 요청할 때 마다 

쿠키가 서버로 자동으로 전송됩니다. 

서버는 쿠키를 까서 확인해보고 세션 데이터가 진짜 있는지도 조회해서 

유저가 로그인 잘되어있는지 여부를 판단할 수 있는데 

그건 어떻게 하냐면 그냥 deserializeUser라고 코드짜면 끝입니다.

 

 

passport.deserializeUser((user, done) => {
  process.nextTick(() => {
    return done(null, user)
  })
})

passport.serializeUser 밑에 추가합시다. 

이러면 유저가 요청날릴 때 마다 쿠키에 뭐가 있으면 그걸 까서 세션데이터랑 비교해보고 

그래서 별 이상이 없으면 현재 로그인된 유저정보를 모든 API의 요청.user에 담아줍니다.

그래서 이제 API 만들 때 로그인된 유저 정보를 출력하고 싶으면

아무 API에서 요청.user 쓰면 되는 것임

 

로그인 후에 API들에서 요청.user 잘나오나 한 번 아무 API에서 테스트해봅시다. 

로그인된 유저 정보가 잘 나오면 성공입니다.

 

 

 

근데 실은 deserializeUser 안에 이렇게 채워넣으면 문제가 하나 있을 수 있는데

위처럼만 냅두면 이게 세션 document에 적힌 유저정보만 달랑가져오기 때문에

세션데이터가 좀 오래됐거나 그럴 경우엔 최신 유저이름과 좀 다를 수 있습니다.

 

그래서 좋은 관습은

- 세션에 적힌 유저정보를 가져와서

- 최신 회원 정보를 DB에서 가져오고

- 그걸 요청.user에 집어넣는 식으로 코드짜는게 좋습니다. 

 

 

 

passport.deserializeUser((user, done) => {
  let result = await db.collection('user').findOne({_id : new ObjectId(user.id) })
  delete result.password
  process.nextTick(() => {
    return done(null, result)
  })
})

user 파라미터 출력하면 유저 _id 같은게 나오는데 

그걸로 DB를 조회해본 다음 그걸 요청.user 안에 집어넣으라고 코드짰습니다. 

- 참고로 done() 둘째 파라미터에 집어넣은게 자동으로 요청.user 안에 들어갑니다. 

- delete 문법은 object 자료에서 원하는 key를 제거하는 문법입니다. 패스워드는 요청.user 에서 필요없어보여서 지웠음 

 

 

 

 

 

 

나는 쿠키가 궁금한데요 

 

브라우저 개발자도구 Application 탭에 들어가보시면 쿠키 구경이 가능합니다.

 

 

 

로그인 성공시 connect.sid 어쩌구라는 이름으로 이상한 문자열이 쿠키로 저장되는데

이게 세션 document의 _id 같은 것입니다. 

저번 강의에 입력해둔 비번으로 간단한 암호화를 해줬기 때문에 좀 길고 복잡해 보일 뿐입니다.

 

 

그래서 오늘 배운거 정리하자면 

1. 로그인성공시 세션만들어주고 유저 브라우저 쿠키에 저장해주는건 passport.serializeUser()

2. 유저가 쿠키 제출한걸 확인해보는건 passport.deserializeUser() 

3. 현재 로그인된 유저 정보 출력은 API들 안에서 요청.user 

쓰면 됩니다. 

근데 문제는 지금은 세션을 메모리에 저장하고 있어서 서버 재시작시 로그인이 풀려버립니다.

세션을 DB에 저장하는건 다음 시간에 알아봅시다. 

 

 

 

 

오늘의 숙제 :

마이페이지 같은거 하나 만들어옵시다. 

조건 1. 마이페이지는 로그인 한 사람만 방문할 수 있고

조건 2. 마이페이지 레이아웃은 아무렇게나 만드는데 현재 로그인된 유저의 아이디가 어딘가 표기되어있어야합니다.

 

가입기능 만들기 

 

가입기능 심심하면 만들어보랬던 것 같은데

그냥 유저가 폼에서 아이디/비번 전송하면 DB에 저장해주는게 가입기능 끝 같습니다.

전 어떻게 해놨냐면 

 

app.get('/register', (요청, 응답)=>{
  응답.render('register.ejs')
})

저는 누가 /register 페이지로 방문하면 register.ejs 파일 보내라고 했습니다. 

 

 

<form class="form-box" action="/register" method="POST">
    <h4>가입</h4>
    <input name="username">
    <input name="password" type="password">
    <button type="submit">전송</button>
</form> 

register.ejs 내용은 login.ejs 랑 똑같이 해놨습니다.

 

 

app.post('/register', async (요청, 응답) => {
  await db.collection('user').insertOne({
    username : 요청.body.username,
    password : 요청.body.password
  })
  응답.redirect('/')
})

폼 전송 누르면 DB에 아이디/비번을 저장하라고 코드짰습니다. 

- 같은 username이 이미 있으면?

- username이 빈칸이면? 

- 비번이 너무 짧으면?

이런 예외사항도 여러분들이 if문으로 잘 처리해보도록 합시다. 

 

 

 

 

 

hashing

 

근데 가입시킬 때 지금은 비번을 DB에 그대로 저장하고 있는데 

비번은 암호화해서 저장하는게 좋습니다.

그래야 DB가 털려도 원래 비번은 알 수 없으니까요.

실은 암호화라기보다는 해싱인데 해싱이 뭐냐면 어떤 문자를 다른 랜덤한 문자로 바꾸는걸 해싱이라고 합니다.

SHA3-256, SHA3-512, bcrypt, scrypt, argon2 이런 여러가지 해싱 알고리즘들이 있습니다. 

 

예를 들어 hello 이런 문자를 SHA3-256 알고리즘으로 해싱하면 

3338be694f50c5f338814986cdf0686453a888b84f424d792af4b9202398f392

이런 문자가 됩니다. 

해싱된 문자보고 원래 문자를 유추할 수 없습니다.

그래서 비번같은거 보관할 때 해싱해서 저장하는게 안전합니다.

참고로 나중에 유저가 로그인시 제출한 비번을 DB와 비교하고 싶으면

제출한 비번을 또 해싱해보면 DB와 비교가능합니다. 

 

아무튼 우리는 bcrypt라는 해싱 알고리즘을 써볼건데 이거 쓰기쉽게 도와주는 라이브러리를 설치해봅시다. 

 

 

npm install bcrypt

1. 터미널 열어서 npm install bcrypt 하고 

 

 

const bcrypt = require('bcrypt') 

2. 이런 코드를 상단에 추가하면 셋팅 끝입니다. 

 

 

await bcrypt.hash(해싱할문자, 10) 

3. 그럼 이제 이런 코드를 사용하면 어떤 문자를 해싱해서 그 자리에 퉤 뱉어줍니다. 

뒤에 넣는 숫자는 얼마나 꼬아줄지를 결정해줍니다.

값이 15 이렇게 높아질수록 해싱에 1초 이렇게 걸리는데

그러면 해커도 해시값을 때려맞추려면 매우 긴 시간이 걸리기 때문에

해킹을 포기하게 만드는 지연장치입니다. 

하지만 하나 해싱하는데 1초 그렇게 걸리면 서버도 부담될 수 있기 때문에 적절히 10정도 넣으면 0.1초 미만으로 해싱해줍니다. 

 

 

 

그래서 해시결과를 테스트삼아 출력해보면 대충 이렇게 생겼습니다.

근데 정확히말하면 오른쪽 쯤에 있는게 해시값이고

왼쪽에 있는건 salt라고 부릅니다. 

해싱할 때 얘가 자동으로 salt라는것도 넣어주는데 그게 뭐냐면 

 

 

 

 

 

salt 추가하면 더 안전함

 

비번을 해싱할 때 그냥 비번만 달랑 해싱하는게 아니라

뒤에 랜덤문자를 몰래 이어붙여서 해싱하면 좀 더 안전하지않을까요? 

실제로 그렇습니다. 그 랜덤문자를 salt라고 부릅니다.

그래서 bcrypt 라이브러리 쓰면 자동으로 salt 넣어서 해싱해줍니다. 

 

정확히 말하면 salt를 쓰면 해커들이 lookup table attack, rainbow table attack이 어려워진다는 장점이 있어서 쓰는 것인데

이것들이 뭐냐면 해커들이 해시를 보고 원래 비번을 쉽게 추론할 수 있게 만든 표 같은 것입니다. 

 

 

 

▲ 이런 비번을 해싱하면 5d93ceb~ 이런게 나온다는 정보가 100억개 적혀진 표입니다. 

 

그런데 비번을 저장할 때 salt라는 랜덤문자를 더해서 해시해버리면

해커가 기존에 만들어놓은 표를 아예 못쓰고 새로 만들어야 하기 때문에 (salt를 넣어서 해싱한 표를 새로 만들어야하기 때문에)

그렇게 해킹을 좀 어렵게 만드는 것에 의의가 있다고 보면 됩니다.

 

그래서 salt를 패스워드 옆에 함께 보관해두는데

salt를 다른 별도의 DB나 하드웨어에 보관하는 곳들도 있습니다.

그렇게 보관하는 salt들을 pepper라고 부르는데 거기까지는 귀찮으니까 안할거고요 

아무튼 결론을 해시해서 비번을 저장하면 됩니다. 

 

 

 

 

 

app.post('/register', async (요청, 응답) => {
  let 해시 = await bcrypt.hash(요청.body.password, 10) 
  await db.collection('user').insertOne({
    username : 요청.body.username,
    password : 해시
  })
  응답.redirect('/')
})

아이디/비번 DB에 저장할 때 해싱해서 저장하라고 코드를 짜봤습니다. 

DB에 해시값으로 비번이 잘 저장되나 확인해봅시다.

 

 

 

 

passport.use(new LocalStrategy(async (입력한아이디, 입력한비번, cb) => {
  let result = await db.collection('user').findOne({ username : 입력한아이디})
  if (!result) {
    return cb(null, false, { message: '아이디 DB에 없음' })
  }

  if (await bcrypt.compare(입력한비번, result.password)) {
    return cb(null, result)
  } else {
    return cb(null, false, { message: '비번불일치' });
  }
})) 

근데 해시값으로 비번을 저장하면

new LocalStrategy 어쩌구 같은 코드에서 비번을 비교할 때도 해싱해서 비교해야합니다.

해시값을 비교하고 싶으면 await bcrypt.compare 가져다가 쓰면 되겠습니다. 

그래서 비교 결과가 맞으면 true 같은걸 그 자리에 뱉어줄겁니다. 

 

 

 

 

 

세션을 DB에 저장하려면 connect-mongo

 

유저가 로그인하면 세션 document를 하나 만들어준다고 했는데

실은 DB에 발행되는게 아니라 컴퓨터 메모리에 임시저장되고 있어서 

서버가 재시작되거나 그러면 세션 document들이 증발합니다.

그게 싫고 안정적으로 쓰고 싶으면 세션을 mongodb에 저장합시다. 

connect-mongo라는 라이브러리 설치하면 됩니다.

다른 데이터베이스 쓰고 싶은 사람들은 connect-어쩌구 이름의 다른 라이브러리 찾아보면 되겠습니다. 

 

 

npm install connect-mongo 

터미널 열어서 이런거 설치하고

 

 

const MongoStore = require('connect-mongo')

app.use(session({
  resave : false,
  saveUninitialized : false,
  secret: '세션 암호화 비번~~',
  cookie : {maxAge : 1000 * 60},
  store: MongoStore.create({
    mongoUrl : '님들 DB접속용 URL~~',
    dbName: 'forum',
  })
})) 

require('connect-mongo') 하고 

app.use(session()) 안에 store: 라는 항목 추가하면 됩니다. 

 

그럼 이제 DB에 접속해서 forum이라는 데이터베이스 안에 sessions라는 컬렉션을 만들어서

거기에 세션을 알아서 보관해줄겁니다.

유효기간 지나면 자동으로 삭제도 알아서 해줍니다. 

진짜 되는지 확인하고 싶으면 로그인하고 나서 MongoDB 들어가봅시다. 

 

참고로 로그아웃 기능은 안만들어놔서 로그아웃은 그냥 로그인 다시하면 됩니다. 

깔끔하게 해보고 싶으면 그냥 쿠키 삭제하면 로그아웃 될듯요 

세션저장되는 방식과 관련해서 이거저거 셋팅을 더 만지고 싶으면

connect mongo 라이브러리 사용법 찾아보시면 되겠습니다. 

 

 

 

 

 

 

성능 팁

 

비효율적으로 보이는 포인트가 몇개 있어보이는데 

1. deserializeUser는 항상 유저가 서버로 요청을 날릴 때마다 세션용 쿠키가 있으면 실행됩니다.

그럼 모든 요청을 날릴 때 쓸데없는 DB조회가 발생하는 것 아닙니까  

지금 메인페이지 같은 곳에 방문할 땐 굳이 저걸 실행할 필요가 없어보입니다. 

그래서 deserializeUser를 특정 route에서만 실행시키는법 이런거 찾아보시면 약간 더 효율적으로 동작시킬 수 있습니다.

 

2. 근데 그렇게 해도 요청이 너무 많이 들어와서 DB조회가 너무 많이 발생할거같으면 

Redis 같은 가벼운 메모리기반 데이터베이스를 호스팅받아서 쓰는 사람들도 있습니다.

하드디스크 보다 램이 훨씬 빠르니까요.

connect-redis 그런걸 한번 찾아봅시다.

 

3. 유저가 1억명이거나 아니면 백엔드에서 운영중인 마이크로 서비스가 많다면

세션 말고 JWT 쓰는게 편리할 수도 있습니다. 그건 DB조회할 필요가 없으니까요. 

그것도 passport로 구현할 수 있는 예제가 많기 때문에 찾아보면 쉽게 구현가능합니다. 

물론 DB 조회를 안하면 유저를 강제로 로그아웃 시키거나 그런 기능 만드는게 어려울 수 있습니다.

 

 

 

 

 

오늘의 응용사항 :

 

Q1. 회원가입 시켜줄 때 중복아이디로 가입하는걸 막고싶으면?

혼자 코드 못짜는 어린이를 위한 힌트

 

가입요청시 DB에 바로 저장시키지 말고

DB에서 같은 username이 있는 document가 있는지 조회해보고

있으면 가입 거절해주면 끝 아니겠습니까

 

[collapse]

 

Q2. 유저 비번 입력란을 최소 2개로 만들어서 2개가 일치해야 가입시켜주려면? 

어린이를 위한 힌트

 

<input> 2개 만들고 가입요청시 <input> 2개에 입력한게 일치하면 가입시켜주면 될텐데

자바스크립트 잘하면 html 파일에 자바스크립트 짜서 검증해도 별 상관 없을듯요

프론트엔드에서만 검증하면 위조 가능성이 있겠지만 위조해서 이득볼게 없지 않습니까

 

[collapse]

 

Q3. 로그인 한 사람만 글작성가능하게 만들고 싶으면?

어린이

 

로그인한 경우엔 요청.user 안에 뭔가 들어있습니다.