back-end/express.js

[NextJS + Express] Passport-LocalStarategy를 통한 로그인

hojung 2022. 7. 15.
728x90
반응형

1. 무엇을 하려는가?

나는 로그인 기능을 만들고자 했다. 로그인에서 중요한 점은 인증이다. 

현재에는 session 기반 인증 JWT, OAUTH 등 많은 인증 방법이 있지만 나는 node로 백엔드를 개발 중이어서 passport의 localStartegy를 이용하여 session과 cookie기반 인증을 하려고 했다. 인증이란 사용자가 어떠한 작업을 할 때 이 사용자가 이 작업을 할 자격이 있는지에 대한 확인이라고 생각했다. 

만약 포스트를 업로드하는 기능을 이용하려면 우리는 이 사용자가 우리의 사이트의 회원인지 검사해야하고 현재 로그인 중인지 검사해야한다. 이 때 인증을 하는 방법은 로그인 시 서버가 브라우저에게 쿠키를 발급해주고 요청 때마다 이 발급받은 쿠키를 요청과 함께 보내 서버가 자신이 발급한 쿠키가 맞는 지 확인하는 과정을 거치는 것이다. 

https://www.passportjs.org/

 

Passport.js

Simple, unobtrusive authentication for Node.js

www.passportjs.org

공식 사이트는 다음과 같다. 우선 

yarn을 통해 패키지를 설치해준다. 

yarn add passport passport-local cookie-parser

다음 패키지들은 session과 cookie를 통한 인증을 진행할 때 필요한 패키지 들이다. 

 

2. 구현

구현을 할 때 passport에는 여러가지 전략이 존재한다. 구글에 검색해보면 아마 passport-kakao passport-facebook passport-google등 많은 로그인 전략들이 존재할 것이다. 하지만 난 사용자가 아이디와 비밀번호를 입력하면 우리의 db에서 사용자가 있는 지 없는 지 검사한 후 비밀번호 일치 여부를 통해 로그인을 하는 local 전략에 해당하는 로그인 기능을 구현하려고 한다. 

우선 passport 설정에 대한 2가지 파일을 만들어주었다. 

express 앱 내부에 passport라는 폴더를 만들어주고 그 안에 index.js와 local.js라는 두 파일을 만들어주었다. 

패스포트 설정에 필요한 두 파일

import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import db from '../models/index.js';
const env = process.env.NODE_ENV || 'development';

const User = db.User;
const PassportStrategy = () => {
	passport.use(
		new LocalStrategy(
			{
				usernameField: 'email',
				passwordField: 'password',
			},
			async (email, password, done) => {
				try {
					const user = await User.findOne({
						where: { email },
					});
					console.log(user);
					if (!user) {
						done(null, false, { reason: '존재하지 않는 사용자 입니다.' });
					}
					if (password === user.dataValues.password) {
						return done(null, user);
					} else {
						return done(null, false, { reason: '비밀번호가 틀렸습니다.' });
					}
				} catch (err) {
					console.error(err);
					return done(err);
				}
			},
		),
	);
};

export default PassportStrategy;

local.js의 코드를 살펴보겠다. 로그인 과정에는 많은 예외 경우들이 존재하는데 이 local 전략에서는 크게 3가지 경우 정도로 나누어서 조건에 따른 코드를 구현해주어야한다. 우선 LocalStrategy안에 field이름들은 프런트에서 아이디와 비밀번호의 인풋 태그 name과 같아야한다. 그 후 email과 password를 받아서 sequelize를 이용해 mysql 내부에 사용자가 있는 지 확인한 후 만약 없다면 존재하지 않는 사용자라는 메시지와 함께 done함수를 실행한다. 

이 때 done함수는 세 가지 인자를 받는데 두 번째에는 결과, 세번째는 에러 메시지를 함께 보낼 수 있다.  

또한 password가지 같다면 done함수의 두 번째 인자로 찾은 사용자 정보를 전송한다. 

만약 아니라면 password가 다르다는 것이므로 비밀번호가 틀렸다는 에러메시지와 함께 전송한다. 

 

이렇게 되면 session내부에 사용자 정보가 저장될 수 있다. 이 session을 사용하려면 index.js파일에서 설정을 해주어야하는데 이 때 사용하는 것이 다음의 코드이다. 

import passport from 'passport';
import local from './local.js';
import db from '../models/index.js';

const User = db.User;
const passportConfig = () => {
	passport.serializeUser((user, done) => {
		done(null, user.id);
	});

	passport.deserializeUser(async (id, done) => {
		try {
			const user = await User.findOne({ where: { id } });
			console.log('deserialize!', user);
			done(null, user);
		} catch (error) {
			console.error(error);
			done(error);
		}
	});

	local();
};

export default passportConfig;

위의 코드를 보면 serializeUser와 deserializeUser라는 부분으로 나뉘게 되는데 

serializeUser가 담당하는 부분은 user의 아이디를 보관하는 부분이다. session에서는 현재 로그인 한 유저들의 정보를 저장하고 있는데 만약 유저의 모든 정보를 다 저장하고 있다면 session 메모리에 무리가 가게 될 것이다. 따라서 유저들의 primary key인 id만을 가지고 있게 되는데 이 부분을 담당해주는 것이 serializeUser부분이다. 

 

desiralizeUser에서는 이렇게 id만으로 가지고 있던 user의 정보를 다시 데이터베이스를 통해서 복구해주는 과정이 필요한데 따라서 sequelize의 findOne함수가 들어가는 것이다. 만약 session에 사용자의 정보가 필요한 요청이 있다면 가지고 있는 아이디를 통해서 데이터베이스에서 유저 정보를 찾아 다시 반환해준다. 

 

그 후 해주어야하는 설정들이 존재한다. 

app전체의 설정들은 app.js에서 해주는 것이 일반적이므로 app.js에서 다음과 같은 설정을 해준다. 

dotenv.config();
passportConfig();
const Options = {
	origin: `${front}`,
	credentials: true, // 쿠키 전달
};
app.use(
	cors({
		origin: true,
		credentials: true,
	}),
);
app.use(cors(Options));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
	session({
		saveUninitialized: false,
		resave: false,
		secret: process.env.COOKIE_SECRET,
		cookie: {
			httpOnly: true,
			secure: false,
		},
	}),
);
app.use(passport.initialize());
app.use(passport.session());

한 컴퓨터에서 local로 개발을 하고 있다면 가장 많이 발생하는 문제는 CORS이다. CORS는 앞선 포스팅에서 설명을 했으니 넘어가겠다. 

express에서는 cors라는 미들웨어를 통해 쉽게 프록시를 구축하여 우회할 수 있는데 

우리가 중요시 봐야할 점은 credentials: true라는 옵션이다. 이 옵션이 있어야 쿠키를 전송할 수 있다. 

물론 통신이란 양방향이므로 프론트에서도 다음과 같이 withCredentials옵션을 주어야한다. axios post요청에선 세번째 옵션으로 넣어줄 수 있다. 이 설정을 해주면 요청에 쿠키를 넣어서 보낸다. 

front의 axios요청

이렇게 해주고 app.js에서

다음과 같은 session설정을 해주어야한다. cookie부분을 봤을 때 secure부분이 true라면 https방식에서만 쿠키를 발급할 수 있으므로 현재는 false로 설정해주었다. 또한 http통신이 아닌 요청에 대해서는 cookie를 발급하지 못하게 하는 httpOnly라는 옵션을 줬는데 이는 자바스크립트로 session에 접근해서 cookie를 알아낼 수 있는 가능성을 방지하기 위해서이다. 

 

그 후 app.use(passport.initialize())와 app.use(passport.session())을 수행해주면 정상적으로cookie가 발급되고 저장되는 것을 확인할 수 있다. 

 

3. 결과

로그인 폼을 통해 로그인 요청을 보내면 다음과 같이 응답으로 Set-cookie라는 응답 헤더가 포함된 것을 확인할 수 있다. Set-Cookie헤더는 쿠키를 요청자의 브라우저에 저장할 수 있도록 해주는데 chrome 80 업데이트 이후로 Set-Cookie옵션이 잘 먹지 않는 경우가 많다고 한다. 이는 SameSite옵션이 변경되어서 그러는데 아마 https를 ngrok을 통해 간단히 적용해주면 해결이 되는 것 같다. 

이렇게 되면 애플리케이션 탭에 쿠키가 성공적으로 저장되어 있는 것을 확인할 수 있다. 

 

4. 그럼 인증은???

passport에서는 이 인증을 middleware로 처리가 가능하다. middleware란 router를 처리하기 전 들르는 곳으로 생각할 수 있는데 req.isAuthenticated()라는 명령어를 통해 이 사용자가 현재 세션에 존재하는 지 쿠키를 가지고 있는 지를 확인할 수 있다. 

export const isLoggedIn = (req, res, next) => {
	console.log('authenticated', req.isAuthenticated());
	if (req.isAuthenticated()) {
		next();
	} else {
		res.status(401).send('로그인이 필요합니다.');
	}
};

export const isNotLoggedIn = (req, res, next) => {
	console.log('authenticated', req.isAuthenticated());
	if (!req.isAuthenticated()) {
		next();
	} else {
		res.status(401).send('로그인하지 않은 사용자만 가능합니다.');
	}
};

다음과 같은 미들웨어를 만들어준 후 라우터로 이동해 확인하면 다음과 같다. 

다음은 예시로 로그아웃 요청인데 isLoggedIn미들웨어를 통해 로그인을 한 사용자만 이 요청을 할 수 있도록 설정해주었다. 

728x90
반응형

댓글