포스트

노드 학습 18일차

안녕하세요 🐸

이번엔 passport 미들웨어를 사용해봤습니다.
했갈리는 부분이 많아서 나중에 다시 봐도 이해할 수 있게 정리하는 것이 어렵네요
특히나 자바를 현업에서 사용 중인 저는 함수 내에 return 없이 미들웨어 내의 함수를 사용하는 것으로 호출부로 값을 전달해주는 것 같은 부분이 이해가 안되어서 많이 혼동이 왔습니다.

passport

passport란?

  • 사용자의 로그인 처리를 돕는 미들웨어
  • 세션, 쿠키 처리 등의 복잡한 작업들을 처리할 수 있음
  • 서비스 자체의 로그인 방법 외에도 카카오, 구글 로그인 등과 같은 로그인 방법도 처리할 수 있으며 이러한 방법을 구분하는 것을 전략(Strategy) 이라 부름

    passport 로그인 처리 순서

    기본 적인 사용 방법을 알기에 앞서 passport 에서 로그인 처리의 흐름에 대해 이해하고 넘어갈 필요가 있습니다.
    이 부분을 이해하지 못하고 무작정 코드만 따라 쓰다보니까 이해가 안되는 부분이 많았습니다.

  1. 로그인 요청
  2. 사용자의 로그인 상태를 확인
    • req.isAuthenticate() 함수를 통해 확인
  3. 로그인 전략에 따른 미들웨어 호출
    • 사전에 초기화한 로직에 따라 로그인 성공 여부 판단
  4. 결과에 따라 로그인 성공/실패 판단
    1. 성공시 req.serialize() 를 통해 세션에 사용자 정보를 저장
  5. (성공 시) passport 미들웨어는 세션 쿠키를 클라이언트에게 전달
    1. passport.session() 에서 처리

passport 사용 방법

passport를 사용하기 위해서는 직접 passport 미들웨어를 구현해야 합니다.
정확한 표현은 아니지만 passport 미들웨어에서는 인터페이스를 제공하고 우리는 인터페이스를 구현한다 생각하면 이해에 도움이 됩니다.
구현한 passport 모듈과 passport 미들웨어는 app.js에서 각각 연결해줘야 합니다.
passport 미들웨어 받아놓고서 두 개를 연결하는 부분이 이해가 안가서 처음에는 해메이기 쉽습니다.
앞서 배웠던 미들웨어들은 1 미들웨어 1 연결이었는데 passport는 모듈을 구현하고 이를 또 app.js에서 따로 연결해줘야 하기 때문에 특히나요.

아래는 passport 모듈을 구현하기 위한 파일 구조의 예시 입니다.

1
2
3
4
passport/
├── index.js
├── localStrategy.js
└── kakaoStrategy.js

각각의 파일 설명입니다.

  • index.js
    • passport를 초기화 하고 내부에서는 아래의 기능을 구현화 합니다
      • passport.serializeUser()
      • passport.deserializeUser()
      • 전략에 맞는 passport 모듈의 Strategy
  • localStrategy.js
    • passport-local 을 초기화 하고 LocalStrategy 객체를 생성하고 passport 가 해당 객체를 use() 합니다.
    • 로그인 로직에 따른 성공/실패 여부를 판단 하고 이를 다음 미들웨어로 전달합니다.
  • kakaoStrategy.js
    • passport-kakao 를 초기화하고 KakaoStrategy 객체를 생성하고 passport 가 해당 객체를 use() 합니다.
    • 로그인 로직에 따른 성공/실패 여부를 판단 하고 이를 다음 미들웨어로 전달합니다.

다음은 app.js 에서 passport를 사용하도록 설정하는 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const express = require('express');
const app = express();
const session = require('express-session');
const passport = require('passport');
const passportConfig = require('./passport'); // passport 디렉토리 내의 index.js

// passport 모듈 초기화
passportConfig(); // passport/index.js에서 module.exports 한 부분을 호출

/* 
passport 미들웨어의 initialize(), session()은
세션을 사용하기 때문에 express-session을 먼저 use() 한 다음에 연결
*/
app.use(session({
	// 생략...
}));

// req.user, req.login, req.logout req.isAuthticate 함수 생성
app.use(passport.initialize());

// connect.sid 이름으로 세션 쿠키를 클라이언트로 전달
app.use(passport.session());

passport 구현

아래 코드는 passport 를 구현한 index.js 의 예시 입니다.

  • passport.serializeUser
  • passport.deserializeUser 를 구현화하고 각 전략별 passport 를 구현한 것을 호출하여 초기화합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');

module.exports = () => {
    passport.serializeUser((user, done)=>{ // req.login 할 때 호출
        // console.log('user : ',user);
        done(null,user);
    })

    passport.deserializeUser((user, done)=>{ // 로그인한 사용자의 모든 호출에서 사용
        // console.log('id :',user.user.id);
        User.findOne({where:{id:user.user.id}})
        .then((user)=>{
            done(null,user)
        })
        .catch((err)=>{
            done(err);
        })
    })

    local();

    kakao();
}

passport-local

서비스에서 사용할 로그인 로직을 구현합니다.
passport-local의 Strategy 클래스 객체를 구현하고 이를 passport 에서 use() 하도록 처리합니다.
예제 코드)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const passport = require('passport');
const User = require('../models/user');
// const LocalStrategy = require('passport-local').Strategy;
const {Strategy : LocalStrategy} = require('passport-local'); // 위 라인의 방법과 결과는 동일하며 구조분해할당 사용하는 방법
const bcrypt = require('bcrypt');

module.exports = () => {
    passport.use(new LocalStrategy({
        usernameField : 'email', // req.body.email 을 유저 아이디로 받음
        passwordField : 'password', // req.body.password 를 유저 비밀번호로 받음
        passReqToCallback : false, // true 설정시 VerifyFunction 의 첫번째 인자로 요청 객체(request) 가 추가됨 ex) async(req,email,password,done)
        session : true // 로그인 정보를 세션에 저장해서 로그인 상태를 유지할지 여부. 기본값은 true. *interface에서는 undefined 라고 하지만 
    },async(email, password, done)=>{ // (아이디, 비밀번호, done) 를 고정적으로 받음. *interface 확인
        try {
            const exUser = await User.findOne({where:{email}});
            if(exUser){
                const result = await bcrypt.compare(password,exUser.password);
                if(result){
                    done(null,{user:exUser}); 
                } else {
                    done(null, false, {message:'비밀번호 불일치'})
                }
            } else {
                connect(null, false, {message:'미가입 회원'})
            }
        } catch (error) {
            console.error(error);
            done(error);
        }
    }));
}

passport-kakao

passport-kakao의 Strategy 클래스 객체를 구현하고 passport 에서 use 합니다.

예제 코드)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const passport = require('passport');
const { Strategy : KakaoStrategy } = require('passport-kakao');
const User = require('../models/user');

module.exports = () =>{
    passport.use(new KakaoStrategy({
        clientID: process.env.KAKAO_ID,
        callbackURL: process.env.KAKAO_CALLBACK,
        // kakao developers > 내 애플리케이션 > 제품 설정 > 카카오 로그인 > 보안 에서 생성&확인 가능
        // clientSecret: '', 
        
        // 공식 문서를 찾아봤지만 아직 내용 확인 불가
        // GPT : OAuth 요청 URL에 권한 목록의 구분자를 지정하는 옵션
        // scopeSeparator: '', 
        
        // 공식 문서를 찾아봤지만 아직 내용 확인 불가
        // GPT : 커스텀 헤더를 설정. 인터페이스에서는 string이라고 되어있지만 Map 객체를 받음
        // customHeaders: ''
    },async (accessToken,refreshToken,profile,done)=>{
        // console.log('profile :',profile);
        try {
            const exUser = await User.findOne({where:{snsId : profile.id, provider:'kakao'}});
            if(exUser){
                done(null,{user:exUser, accessToken});
            } else {
                const newUser = await User.create({
                    email : profile._json.kakao_account?.email,
                    nick : profile.displayName, 
                    provider : 'kakao',
                    snsId : profile.id
                });
                done(null,{user:newUser, accessToken});
            }
        } catch (error) {
            console.error(error);
            done(error);
        }
    }));
}

Q. passport 초기화 하는 부분을 express-session 보다 앞서 초기화 했음에도 불구하고 정상 동작 하는 것은 왜일까? A. 이벤트리스너와 유사하다고 생각하면 이해가 쉬웠다. 로그인에 대한 전략을 설정하고 초기화만 했을 뿐 실제 동작은 요청이 올 때 실행되기 때문에 코드 작성 당시에는 express-session을 사용하지 않기 때문

Q. passport 전략에 사용되는 미들웨어는 전부 Strategy 클래스를 구현하는데 이는 어디서 오는 것일까? A. passport 전략은 전부 passport에서 선언된 Stategy 클래스를 상속 받으며 이를 구현하도록 되어 있기 때문

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.