따꿍의 프로젝트

[2025.07.26] auth PR 확인하기 본문

웹프로젝트/졸작

[2025.07.26] auth PR 확인하기

공장 주인 따꿍 2025. 7. 26. 17:58

app.ts

import express, { ErrorRequestHandler } from 'express';
import cookieParser from 'cookie-parser';
import morgan from 'morgan';
import session from 'express-session';
import passport from 'passport';
import sequelize from './sequelize';
import authRouter from './routes/auth';
import passportConfig from './passport';
import './models';

const app = express();
passportConfig();

export const syncDB = async () => {
    try {
        await sequelize.sync({ alter: true });
        console.log('DB 동기화 완료');
    } catch (err) {
        console.error('DB 동기화 실패:', err);
    }
};

app.use(morgan('dev'));
app.use((req, res, next) => {
    if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
        express.json()(req, res, next);
    } else {
        next();
    }
});
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
    session({
        resave: false,
        saveUninitialized: false,
        secret: process.env.COOKIE_SECRET!,
        cookie: {
            httpOnly: true,
            secure: false,
        },
    })
);

//passport 연결
app.use(passport.initialize());
app.use(passport.session()); //로그인 상태 세션 기반으로 유지

app.use('/auth', authRouter);

app.get('/', (req, res) => {
    res.send('Hello TypeScript Backend!');
});

app.use((req, res, next) => {
    const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
    error.status = 404;
    next(error);
});
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
    console.error(err);
    res.status(err.status || 500);
};
app.use(errorHandler);

export default app;

 

1. if (['POST', 'PUT', 'PATCH'].includes(req.method)) 이해하기

app.use(morgan('dev'));
app.use((req, res, next) => {
    if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
        express.json()(req, res, next);
    } else {
        next();
    }
});

This middleware conditionally parses JSON only for certain HTTP methods.

 

  • For POST, PUT, and PATCH requests — which usually contain JSON bodies — it runs the express.json() middleware.
  • For other methods (like GET, DELETE), it skips JSON parsing to avoid unnecessary processing.

This avoids running express.json() on requests that almost never have a body (like GET), which can be a minor optimization, especially when handling large volumes of traffic or unusual edge cases (e.g., when parsing JSON on GET causes errors due to unexpected content type).

 

app.use(express.json()); // always apply (POST, PUT, PATCH, GET, DELETE 모두 파싱함)


This is what most projects do, unless you need fine-grained control like in your example.

 

 

2. session과 passport 미들웨어 완벽 정리

app.use(session())

- if authentication succeeds, a session(personal 'container' for that user) will be established
    and maintained through a cookie set in the user's browser.

검정 부분이 session이 하는 역할이고,

파란 부분이 중간중간 passport가 하는 역할이다. 

 

❓ 궁금점: 
이러면 passport가 돌아갈때마다 express-session에 cookie라는 이름의 세션이 존재하는지 확인한다는 것인데,

passport관련 코드는 무조건 session이 다 세팅된 이후 시점에 적어줘야하는 것 아닌가?
어떻게 passportConfig()가 app.use(session())보다 이전에 나타나는 것일까?
-> It works because passportConfig() only defines the strategies, and not the middleware that depends on session().

passportConfig에는 local()와 kakao()가 들어가있는데
All this is doing is configuring Passport — defining how it should serialize users and what strategies to use.
⚠️ This part does not rely on sessions yet. It just registers logic.


passport>index.ts

- serializeUser은 로그인 직후, 해당 유저만의 상자(session)을 만들어주고,
    세션에 무엇을 저장해줄지 정해줍니다.  -> done이 해당 역할을 해줌

    ex) done(null, user.user_id)이면 세션에 user_id만 저장함
    만일 user_id보다 많은것을 session에 저장해주고 싶다면

- deserializeUser은 세션안에 저장한 user_id를 기준으로 전체 User 정보를 DB에서 데리고 오고
    이 유저 전체 정보를 req.user에 저장해주는 역할을 한다. 

import passport from 'passport';
import local from './localStrategy';
import kakao from './kakaoStrategy';
import User from '../models/user';

export default () => {
    passport.serializeUser((user: any, done) => {
        done(null, user.user_id); // user.user_id 가 PK
    });

    passport.deserializeUser(async (id: number, done) => {
        try {
            const user = await User.findByPk(id);
            if (!user) return done(null, false);
            return done(null, user);
        } catch (err) {
            console.error(err);
            return done(err);
        }
    });

    local();
    kakao();
};

 

- done 안에 있는 null은 에러가 없다는 소리이다. 

done(error, result);

 

 

passport>localStrategy.ts

import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import bcrypt from 'bcrypt';
import User from '../models/user';

export default () => {
    passport.use(
        new LocalStrategy(
            {
                usernameField: 'email',
                passwordField: 'password',
                passReqToCallback: false,
            },
            async (email, password, done) => {
                try {
                    const exUser = await User.findOne({ where: { email } });

                    // 유저가 존재하고, 비밀번호도 존재할 때만 bcrypt 비교
                    if (exUser && exUser.password) {
                        const result = await bcrypt.compare(
                            password,
                            exUser.password
                        );
                        if (result) {
                            return done(null, exUser);
                        } else {
                            return done(null, false, {
                                message: '비밀번호가 일치하지 않습니다.',
                            });
                        }
                    }

                    // 유저가 없거나 비밀번호가 없을 때
                    return done(null, false, {
                        message:
                            '가입되지 않은 회원이거나 비밀번호가 없습니다.',
                    });
                } catch (error) {
                    console.error(error);
                    return done(error);
                }
            }
        )
    );
};

 

1. async function은 email, password, done을 어떻게 받아올까?

async function은 LocalStrategy에서 verifyCallback 역할을 한다. 

new LocalStrategy(options, verifyCallback)

=> passport.authenticate에서 알아서 email, password을 꺼내낸다. 

   Passport handles extracting the fields from req.body and passing them in.

 

2. options는 뭐하는 녀석일까?

{ usernameField: 'email', passwordField: 'password' }

These tell Passport:

  • usernameField: 'email'
    • Instead of looking for a field called username (default), use the field called email from req.body.
  • passwordField: 'password'
    • This one is actually default, but it’s explicitly saying: look for a field called password in req.body.

So with this, Passport will extract credentials like this:

const email = req.body.email; const password = req.body.password;

and pass them into your verify function:

async (email, password, done) => { ... }

 

 

Without These Options

If you didn’t include them, Passport would look for:

req.body.username 
req.body.password

which is the default for passport-local.

 

passport>kakaoStrategy.ts

import passport from 'passport';
import { Strategy as KakaoStrategy } from 'passport-kakao';
import User from '../models/user';

export default () => {
    passport.use(
        new KakaoStrategy(
            {
                clientID: process.env.KAKAO_ID!,
                callbackURL: '/auth/kakao/callback',
            },
            async (accessToken, refreshToken, profile, done) => {
                console.log('Kakao profile:', profile);
                try {
                    const exUser = await User.findOne({
                        where: { sns_id: profile.id, provider: 'kakao' },
                    });
                    if (exUser) {
                        return done(null, exUser);
                    } else {
                        const newUser = await User.create({
                            email: profile._json?.kakao_account?.email || null,
                            nickname: profile.displayName,
                            sns_id: profile.id,
                            provider: 'kakao',
                        });
                        return done(null, newUser);
                    }
                } catch (error) {
                    console.error(error);
                    return done(error);
                }
            }
        )
    );
};

middlewares>index.ts

import { Request, Response, NextFunction } from 'express';

export const isLoggedIn = (req: Request, res: Response, next: NextFunction) => {
    if (req.isAuthenticated()) {
        next();
    } else {
        res.status(403).send('로그인이 필요합니다.');
    }
};

export const isNotLoggedIn = (
    req: Request,
    res: Response,
    next: NextFunction
) => {
    if (!req.isAuthenticated()) {
        next();
    } else {
        res.redirect('/');
    }
};

routes>auth.ts

import express from 'express';
import passport from 'passport';
import { join, login, logout } from '../controllers/auth';
import { isLoggedIn, isNotLoggedIn } from '../middlewares/index';

const router = express.Router();

router.post('/join', isNotLoggedIn, join);
router.post('/login', isNotLoggedIn, login);
router.get('/logout', isLoggedIn, logout);

// Kakao OAuth
router.get('/kakao', passport.authenticate('kakao'));
router.get(
    '/kakao/callback',
    passport.authenticate('kakao', {
        failureRedirect: '/?loginError=카카오로그인 실패',
    }),
    (req, res) => {
        res.redirect('/');
    }
);

export default router;

controllers>auth.ts

import { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcrypt';
import passport from 'passport';
import User from '../models/user';

export const join = async (req: Request, res: Response, next: NextFunction) => {
    const { email, nick, password } = req.body;

    try {
        const exUser = await User.findOne({ where: { email } });
        if (exUser) {
            return res.status(400).json({
                success: false,
                message: '이미 가입된 이메일입니다.',
            }); //유저 있으면 에러
        }

        if (!password || password.length < 6) {
            return res.status(400).json({
                success: false,
                message: '비밀번호는 6자 이상이어야 합니다.',
            }); //비밀번호 최소 6자
        }
        const hash = await bcrypt.hash(password, 12); //비밀번호 암호화

        await User.create({
            email,
            nickname: nick,
            password: hash,
            provider: 'local',
        }); //회원정보 저장

        return res.status(201).json({
            success: true,
            message: '회원가입이 완료되었습니다.',
        });
    } catch (error) {
        console.error(error);
        return next(error);
    }
};

export const login = (req: Request, res: Response, next: NextFunction) => {
    passport.authenticate(
        'local',
        (
            authError: Error | null,
            user: Express.User | false, //로그인 성공 시 req.user = user로 Express에 등록됨
            info: { message: string }
        ) => {
            if (authError) {
                console.error(authError);
                return next(authError);
            }

            if (!user) {
                return res
                    .status(401)
                    .json({ success: false, message: info.message });
            }

            return req.login(user, (loginError) => {
                if (loginError) {
                    console.error(loginError);
                    return next(loginError);
                }

                const { user_id, email, nickname } = user as User;

                return res.status(200).json({
                    success: true,
                    message: '로그인 성공',
                    user: {
                        id: user_id,
                        email,
                        nickname,
                    },
                });
            });
        }
    )(req, res, next);
};

export const logout = (req: Request, res: Response) => {
    req.logout(() => {
        return res.status(200).json({
            success: true,
            message: '로그아웃 되었습니다.',
        });
    });
};