본문 바로가기
공부/express

[Express] passport 모듈을 통해 쿠키 세션 방식으로 로그인 구현

by 웅대 2024. 5. 18.
728x90
반응형

이번 포스팅에서는 쿠키, 세션 방식으로 로그인을 구현해보려고 한다.

 

세팅

 

패키지를 설치하자.

 

npm install express express-session bcrypt dotenv passport passport-local mongoose

 

  1. express : express 서버
  2. express-session : 세션 관리, 매 요청마다 session에 저장된 값을 req.session을 통해 꺼내올 수 있다.
  3. bcrypt : 단방향 해시함수. 비밀번호를 해시하여 데이터베이스에 안전하게 보관할 수 있다.
  4. dotenv : .env 파일을 사용하여 중요한 값을 관리할 수 있다.
  5. passport : 인증 과정 구현을 도와준다.
  6. passport-local : passport와 연결하여 로컬 인증을 쉽게 구현할 수 있다.
  7. mongoose : mongoDB ORM

기존에는 cookie를 읽고 쓸 수 있는 cookie-parser 미들웨어와 함께 사용을 했지만 express-session 버전 1.5.0 이상부터 cookie-parser가 내장되어서 cookie-parser를 따로 설치할 필요가 없다.

 

특히 cookie-parser를 따로 사용하면 session과 cookie의 비밀 키가 달라지는 문제가 발생하기 때문에 사용하지 않을 것을 권고하고 있다.

 

기본 세팅

먼저 보안이 중요한 정보들을 .env 파일에 작성해두자.

 

<.env>

# MongoDB
MONGODB_URL=localhost:27017
MONGODB_NAME=auth_practice

# Server
SECRET_KEY=secretkey
SERVER_PORT=3001

 

몽고 디비 관련 설정을 해보자.

 

설명이 필요하다면 이전 포스팅 참고

https://growth-coder.tistory.com/276

 

[Express] mongoose 사용법 (mongoDB ORM)

https://growth-coder.tistory.com/275 [Express] sequelize 사용법 (ORM)https://growth-coder.tistory.com/274 [express] express 웹 서버 기초pacakage.json 생성npm init 명령어로 package.json을 만들어준다. package.json에는 프로젝트가

growth-coder.tistory.com

 

dotenv를 사용하기 위해선 require('dotenv')로 dotenv를 가져오고 dotenv.config()를 작성하면 process.env를 통해 .dotenv에 저장된 값을 가져올 수 있다.

 

<schemas/index.js>

const mongoose = require('mongoose')
const dotenv = require('dotenv')
dotenv.config()

const connect = ()=>{
    mongoose.connect(`mongodb://${process.env.MONGODB_URL}/${process.env.MONGODB_NAME}`)
    .then(()=>{
        console.log("mongodb 연결 성공")
    })
    .catch((error)=>{
        console.log(error)
    })
    mongoose.connection.on('error', (error)=>{
        console.log(error)
    })
    mongoose.connection.on('disconnected', ()=>{
        console.log("연결 실패 재연결 시도합니다.")
        connect()
    })
}
module.exports = connect

 

<schemas/user.js>

const mongoose = require('mongoose')
const {Schema} = mongoose
const userSchema = new Schema({
    name: {
        type: String,
        required: true
    },
    password: {
        type: String,
        required: true
    },
    age: {
        type: Number,
        required: true
    }
})
module.exports = mongoose.model('User', userSchema)

 

유저를 저장하고 id로 유저를 가져오는 라우터도 작성해보자.

 

라우터 분리 방법은 다음 포스팅 참고

https://growth-coder.tistory.com/277

 

[Express] API 작성법 (라우터, 컨트롤러 분리)

https://growth-coder.tistory.com/274 [express] express 웹 서버 기초pacakage.json 생성npm init 명령어로 package.json을 만들어준다. package.json에는 프로젝트가 사용 중인 패키지의 정보가 담겨있다.package.json 문서를

growth-coder.tistory.com

 

 

지금은 인증, 인가를 적용하지 않았고 이후에 차근차근 적용해보자.

 

<routes/user.js>

const express = require('express')
const User = require('../schemas/user')
const router = express.Router()
router.get('/:id', async (req, res)=>{
    const id = req.params.id
    const findUser = await User.find({
        _id: id
    })
    res.json(findUser)
})
router.post('/', async (req, res)=>{
    const {name, password, age} = req.body
    const newUser = new User({
        name,
        password: await bcrypt.hash(password, 10),
        age
    })
    await newUser.save()
    res.json(newUser)
})
module.exports = router

 

자세히 보면 유저를 등록할 때 비밀번호를 해쉬하여 저장하는 모습을 확인할 수 있다.

 

이후 bcrypt의 compare를 사용하여 평문 비밀번호와 해쉬된 비밀번호를 비교할 수 있다.

 

index.js에서 라우터를 등록하고 몽고 디비와 연결해보자.

const express = require('express')
const connect = require('./schemas/index')
const userRouter = require('./routes/user')
const app = express()
app.use(express.json())
app.use('/users', userRouter)
app.listen(process.env.SERVER_PORT, ()=>{
    console.log(`${process.env.SERVER_PORT} 서버 연결`)
})
connect()

 

이제 서버를 실행한 다음 api가 잘 작동하는지 확인해보자.

 

세션 설정

express-session 기본 설정을 해보자.

const session = require('express-session')
app.use(session({
    cookie: {
        path: "/", // 쿠키 저장 경로.
        httpOnly: true, // 클라이언트가 자바스크립트를 통해 쿠키 접근 불가.
        secure: false, // https에서만 사용 여부. 로컬에서 http로 테스트 할 예정이므로 false.
        maxAge: null // 만료 시간. null이면 무한
    },
    secure: false, // https에서만 사용 여부. 로컬에서 http로 테스트 할 예정이므로 false.
    secret: process.env.SECRET_KEY, // 시크릿 키. 
    saveUninitialized: false, // 세션이 변경되지 않았을 때 세션은 초기화 되지 않지만 true로 두면 초기화 됨. 
    resave: false // 변경 사항이 없어도 항상 세션을 저장할지
}))

 

이외에도 다양한 설정이 있는데 궁금하다면 공식문서 참고

https://www.npmjs.com/package/passport

 

passport

Simple, unobtrusive authentication for Node.js.. Latest version: 0.7.0, last published: 6 months ago. Start using passport in your project by running `npm i passport`. There are 5294 other projects in the npm registry using passport.

www.npmjs.com

 

세션 설정을 마치고나면 req.session을 통해 세션에 접근할 수 있다.

 

참고로 만약 라우터에서 세션에 접근하고 싶다면 세션 설정이 라우터 설정보다 위에 있어야 한다.

 

passport 설정

먼저 전략 코드를 작성해보자.

 

실질적으로 로그인을 수행하는 코드이고 다양한 strategy가 존재하기 때문에 다양한 로그인 방법을 구현하여 사용할 수 있다.

<passport/localStrategy.js>

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

const User = require('../schemas/user')
module.exports = ()=>{
    passport.use('local', new LocalStrategy({
        usernameField: 'name',
        passwordField: 'password'
    }, async(name, password, done)=>{
        try{
            const findUser = await User.findOne({name})
            if (findUser){
                const res = await bcrypt.compare(password, findUser.password)
                if (res){
                    done(null, findUser)
                } else{
                    done(null, false, {message: "비밀번호가 일치하지 않습니다."})
                }
            } else{
                done(null, false, {message: "해당 유저가 존재하지 않습니다."})
            }
        } catch(error){
            console.log(error)
        }
    }))
}

 

name과 password를 통해 사용자 정보가 일치하는지 확인하는 모습이다.

 

done에는 세 개의 파라미터가 들어갈 수 있는데 차례대로 (에러, 유저 정보, 정보)이다.

 

성공하면 에러에는 null, 유저 정보에 찾은 유저 정보를 넣으면 되고 실패하면 유저 정보에 false를 넣어주면 된다.

 

done 함수는 이후 passport.authenticate에서 콜백으로 받아서 사용한다.

 

이제 직렬화, 역직렬화 설정을 해보자.

 

<passport/index.js>

const passport = require('passport')
const local = require('./localStrategy')
const User = require('../schemas/user')

module.exports = ()=>{
    passport.serializeUser((user, done)=>{
        done(null, user._id)
    })
    passport.deserializeUser(async (_id ,done)=>{
        const findUser = await User.findOne({_id})
        done(null, findUser)
    })
    local()
}

 

serializeUser는 세션에 어떤 값을 저장할지 선택한다.

 

세션에 모든 유저 정보를 넣어도 되지만 세션 저장소에 무리가 갈 수 있기 때문에 보통 유저를 식별할 수 있는 아이디 값을 넣어두고 deserialize를 통해 필요한 유저 정보들을 가져온다.

 

로그인 라우터를 만들어보자.

 

<routes/user.js>

...
const passport = require('passport')
router.post('/login', async(req, res, next)=>{
    passport.authenticate('local', (error, user, info)=>{  
        if (error){
            console.error(error)
            return next(error)
        }
        if (!user){
            return res.json(info)
        }
        return req.login(user, (error)=>{
            if(error){
                return next(error)
            }
            return res.json(user)
        })

    })(req, res, next)
})
...

 

passport.authenticate의 첫 번째 인자로 사용할 전략을 적어준다.

 

위에서 localStrategy를 구현할 때 done을 사용했는데 이 done을 받아와서 다음 함수를 실행한다.

 

오류가 있으면 오류를 반환하고 그렇지 않으면 req.login을 통해 로그인 세션을 생성할 수 있다.

 

성공하면 처음에 작성했던 serializeUser가 유저 정보에서 아이디 값만 가져와서 세션에 저장한다.

 

마지막으로 index.js에서 미들웨어를 등록해주자.

 

<index.js>

...
const passportConfig = require('./passport')
passportConfig()
app.use(passport.initialize())
app.use(passport.session())

 

유저 라우터에서 유저 정보를 출력하는 라우터를 작성해보고 테스트 해보자.

 

...
router.get('/my-info', (req, res)=>{
    console.log(req.user)
    res.send("good")
})
...

위 경로로 요청을 보냈을 때 유저 정보가 출력된다면 성공적으로 구현한 것이다.

 

참고로 passport 설정은 rea.user를 사용하는 라우터 설정보다 먼저 진행되어야 req.user에 유저 정보를 성공적으로 저장한다.

 

추가로 로그인 된 사용자만 특정 요청을 할 수 있게 하려면 req.isAuthenticated()가 true인 사용자만 요청을 보낼 수 있도록 구현하면 된다.

728x90
반응형

댓글