Lsiron
HELLO FOLIO(express, mongoose)- 4(passport를 이용한 로그인 전략 구현) 본문
HELLO FOLIO(express, mongoose)- 4(passport를 이용한 로그인 전략 구현)
Lsiron 2024. 6. 29. 21:22현재 폴더 구조
web_project/
├── db/
│ ├── model # 각종 모델 폴더
│ ├── schemas # 각종 스키마 폴더
│ └── index.js # db 연결파일
├── middleware/ # 각종 middleware 폴더
├── passport/ # 로그인 관련 passport 폴더
│ ├── strategies # 전략 구성
│ └── index.js # 전략 exports 파일
├── routes/ # 라우트 관련 폴더
│ └── index.js # 라우트 exports 파일
├── services/ # 유저 관련 service 폴더
├── views/ # EJS 템플릿 폴더
├── public/ # static 파일 폴더
│ ├── css/ # CSS 파일들
│ ├── js/ # JS 파일들
│ └── images/ # 이미지 파일들
├── .env # 환경 변수 파일
├── .gitignore # Git 무시 파일
├── package.json # 프로젝트 메타데이터 및 종속성 목록
├── app.js # 애플리케이션 진입점
├── index.js # 서버 실행 파일
└── README.md # 프로젝트 설명 파일
현재 설치한 모듈
"dependencies": {
"bcrypt": "^5.1.1"
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
"mongodb": "^6.7.0",
"mongoose": "^8.4.3",
}
이제 제법 API가 많아졌다. app.js 파일에서 불러온 라이브러리와 함께 모두 관리하기에는 가독성이 상당히 떨어져보인다.
그러면 이제 라우팅을 해보자.
이 곳을 참조해 보도록 하겠다.
먼저 routes 폴더로 가서 userRouter.js 파일을 하나 만들어주자.
(똑같은 방법으로 myPageRouter도 만들고 변경해주자.)
이 후, 아래와 같이 코드를 입력해보자.
const express = require('express');
const router = express.Router();
그러면 이 파일에서 라우터를 사용할 수 있게 되었다. 그러면 본격적으로 라우터를 등록해보자.
기존에 app.js 파일에서 입력해 놓았던 API들을 가져오는데 단, 모든 API를 app.get 처럼 app으로 시작되는 것이 아닌 router.get 처럼 router로 작성해준다. 또한 이제 이 라우터에서 UserModel을 사용하기 때문에 여기로 불러와주자.
모두 변환하면 아래와 같이 된다.
변환하는 김에 home으로 설정했던 경로를 이제 login으로 바꿔주고 home.ejs 파일 또한 login.ejs로 바꿔주자.
(똑같은 방법으로 myPageRouter도 만들고 변경해주자.)
const express = require('express');
const router = express.Router();
const UserModel = require('./db/model/userModel'); // db파일의 models에서 User class 불러오기
const bcrypt = require('bcrypt');
router.get('/login', (req, res) => {
res.render('login.ejs')
})
router.post('/login', async (req, res, next) => {
const { email, password } = req.body;
const user = await UserModel.findOne({ email });
if (!user) {
console.log('등록된 사용자가 아닙니다.');
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
console.log('비밀번호가 일치하지 않습니다.');
}
res.redirect('/my-page');
});
router.get('/register', (req, res) => {
res.render('register.ejs')
})
router.post('/register', async (req, res, next) => {
const { email, password, name } = req.body;
// 사용자가 이미 존재하는지 확인
const checkingUser = await UserModel.findOne({ email });
if (checkingUser) {
console.log('이미 존재하는 사용자입니다');
}
// 비밀번호를 해싱함
const hashedPassword = await bcrypt.hash(password, 10); // 첫번째 인자는 password 변수 두번째 인자는 몇번 꼬을지 횟수
// 새로운 사용자 생성
const newUser = {
email,
password : hashedPassword,
name,
};
await UserModel.create( newUser );
res.redirect('/login');
});
module.exports = router;
마지막으로는 routes 폴더의 최상단 index.js에서 관리하기 쉽도록 module.exports로 내보내준다.
(단, 라우터는 객체이기 때문에 중괄호를 씌워선 안된다.)
그러면 라우터 설정을 해줬으니 이제 routes 폴더의 최상단 index.js로 가서 라우터를 아래와 같이 한번 더 내보내주자
(똑같은 방법으로 myPageRouter도 만들고 변경해주자.)
const userRouter = require("./userRouter");
const myPageRouter = require("./myPageRouter");
// 라우터 exports
module.exports = {
userRouter,
myPageRouter,
};
이제 라우터로 작성했고 내보내주는 작업까지 완료했으니, app.js 파일을 정리하러 가보자.
(똑같은 방법으로 myPageRouter도 만들고 변경해주자.)
const express = require('express');
const app = express();
const bcrypt = require('bcrypt');
const { userRouter, myPageRouter } = require("./routes"); // Router를 객체에 담기
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/', userRouter); // userRouter 작동
app.use('/my-page', myPageRouter) // myPageRouter 작동
module.exports = { app };
먼저 내보내준 Router를 가져와주고, app.use('./', userRouter); 를 작성하여 app.js에서 사용할 수 있도록 해주자.
(나중에 수많은 라우터들을 가져올테니 중괄호를 씌워서 변수를 선언해준다. => 구조분해할당)
수 많은 API를 보내주고 나니, 아주 간결해졌다. UserModel도 Router로 넘겨주었기 때문에 삭제하였다.
(index.js 파일에 넣어서 내보낼땐 굳이 경로에 index.js를 쓰지 않아도 된다.)
이제 회원가입과 로그인을 할 수 있는 상태가 되었다.
우리는 인증과 인가 중에서 인증을 할 수 있도록 구현했다. 인증과 인가란 무엇일까? 간단하게 짚고 넘어가보자.
인증은 사용자가 누구인지 확인하는 과정이다. 즉, 사용자의 신원을 검증하는 것이다.
인가는 사용자가 특정 자원에 접근할 권한이 있는지 확인하는 과정이다.
쉽게 말해서
- 인증: 사용자가 누구인지를 확인한다.
- 인가: 사용자가 어떤 자원에 접근할 수 있는지를 확인한다
지금까지 아이디와 비밀번호를 입력하여 로그인 하도록 구현했기 때문에 인증을 적용했다고 볼 수 있다.
그러면 이제 이 인증을 조금 더 개선 시켜보자.
나는 passport와 JWT 토큰방식을 사용해서 인증 방식을 더 개선 시킬 것 이다.
그러면 코드를 작성하기에 앞서
$ npm i passport
$ npm i passport-local
$ npm i passport-jwt
$ npm i jsonwebtoken
차례대로 명령어를 입력한 뒤, 라이브러리를 설치 해 주자.
설치가 됐으면 먼저 passport를 등록해야한다. app.js 파일로 가서 passport를 불러온 후, npm사이트에서 기본 코드를 긁어와준다. 나는 세션이 아닌 토큰 방식을 사용할 것이기 때문에 세션과 관련한 코드들은 모두 제외한다.
https://www.npmjs.com/package/passport
예를들어, sessions 단락에 있는 serializeUser 및 deserializeUser 그리고 middleware 단락에서app.use(passport.session())는 가져오지 않아도 된다.
strategies 단락은 조금 뒤에 가져올 예정이다.
app.js에 아래 코드처럼 입력해주자.
const express = require('express');
const app = express();
const bcrypt = require('bcrypt');
const passport = require('passport'); // passport 불러오기
const { userRouter, myPageRouter } = require("./routes");
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(passport.initialize()); // passport 미들웨어 사용
app.use('/', userRouter);
app.use('/my-page', myPageRouter)
module.exports = { app };
이제 passport를 사용 할 수 있도록 설정했으니, starategy를 정의해보자.
passport의 strategies폴더로 가서 local.js 와 jwt.js 파일을 만들어준다.
먼저 local.js 파일로 가보자.
똑같이 위에 있는 npm 사이트에서 조금 뒤에 가져온다고 했던 strategies 단락의 코드를 긁어와보자. 그러면 아래와 같이 local.js에 입력하면 된다.
const LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function (err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false); }
if (!user.verifyPassword(password)) { return done(null, false); }
return done(null, user);
});
}
));
});
허나 여기에서 우리는 긁어온 localStrategy 함수의 인자들 중, username과 password는 우리의 로그인 페이지에서 'email'과 'password'로 사용한다.
또한, 우리는 mongoose를 사용하기 때문에 mongoose 내장함수로 유저를 찾을 것이다.
아래와 같이 config 변수로 LocalStrategy 함수의 인자들을 변경해주고, mongoose 내장함수를 사용하여 유저를 찾아보자. 그러면 아래와 같이 코드를 삽입할 수 있다.
const LocalStrategy = require('passport-local').Strategy;
const UserModel = require('../../db/model/userModel');
const config = {
usernameField: 'email',
passwordField: 'password',
};
passport.use(new LocalStrategy(config,
async (email, password, done) => {
await UserModel.findOne({ email }, function (err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false); }
if (!user.verifyPassword(password)) { return done(null, false); }
return done(null, user);
});
}
));
});
위와 같이 코드를 삽입해 주었고, mongoose 내장함수를 사용하기 때문에 async / await 을 사용했으며, 화살표함수로 바꿔주었다.
이 함수로 인해 사용자가 로그인을 했을때 이메일과 비밀번호를 제대로 입력했는지, 그리고 회원탈퇴 유저인지 검증할 수 있다.
이제 try / catch 문으로 바꿔주고, 기본 함수를 userRouter.js 에 넣어주었던 로그인 검증으로 바꿔보자.
그냥 try / catch 문과 if 조건문의 연속이다. 또한 이 함수를 passport 최상단 index.js를 통해 내보내줄 것 이기 때문에 local로 변수를 선언 해 주고 내보내 보자.
const LocalStrategy = require('passport-local').Strategy;
const UserModel = require('../../db/model/userModel');
const bcrypt = require('bcrypt');
const config = {
usernameField: 'email',
passwordField: 'password',
};
const local = new LocalStrategy(config, async (email, password, done) => {
try {
//로그인 할 때 이메일 확인
const user = await UserModel.findOne({ email }); // db에서 이메일로 사용자를 찾음
if (!user) {
return done(null, false, { message: '회원을 찾을 수 없습니다' });
}
//로그인 할 때 비밀번호 확인
const isMatch = await bcrypt.compare(password, user.password); // 저장된 비밀번호와 입력된 비밀번호 비교
if (!isMatch) {
return done(null, false, { message: '비밀번호가 다릅니다' });
}
//로그인 성공 시, 사용자 객체를(인증정보를) 'done' 콜백으로 전달 즉 req.user에 사용자 객체를 저장함.
return done(null, user);
} catch(error) {
done(error, null);
}
};
module.exports = local;
여러 조건문을 넣어 검증을 하고 이 검증에 통과를 할 시, user 객체를 전달하도록 하였다. 기본코드와 방식은 똑같고 그냥 구조만 바꾸어 준 것이다.
await UserModel... 옆에 달려있던 또 하나의 function 함수를 catch문으로 맨 밑으로 내렸다.
new LocalStrategy 함수를 덮어주던 passport.use() 함수는 어디로 갔는지 궁금하다면 이제 passport 폴더의 최상단 index.js로 가보자.
바로 여기에서 passport.use()함수에 local을 넣어서 내보내 줄 것이다.
아래와 같이 작성해보자.
const passport = require('passport');
const local = require('./strategies/local');
module.exports = () => {
passport.use(local);
};
이렇게 local Strategy를 만들었으며 우리는 이 전략을 사용할 수 있게 되었다. 다음은 jwt Strategy 이다.
미리 만들어 두었던 jwt.js 파일로 가보자.
똑같이 npm 사이트에서 기본 코드를 긁어와준다. 허나 jwt이기 때문에, 아래의 npm passport-jwt 에서 긁어와야한다.
https://www.npmjs.com/package/passport-jwt
var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
var opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = 'secret';
opts.issuer = 'accounts.examplesoft.com';
opts.audience = 'yoursite.net';
passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
User.findOne({id: jwt_payload.sub}, function(err, user) {
if (err) {
return done(err, false);
}
if (user) {
return done(null, user);
} else {
return done(null, false);
// or you could create a new account
}
});
}));
코드를 그대로 긁어왔다. 왜 굳이 변수로 빈 객체를 선언 해 놓고 저런식으로 객체 안에 넣어놨는지는 의문이다..
허나 var 함수를 사용하지 않고 const를 사용할 것이다. 왜냐하면 var는 현재 사용하지 않으며 굳이 let을 써서 재할당을 할 필요가 없기 때문이다.
또한 우리는 mongoose를 쓰기 때문에 local.js에서 설정했을 때와 같이 mongoose 내장함수를 넣어주고 async / await 으로 바꿔주자.
단, local과의 차이점은 바로 이것. 위에 opts 라고 보이는가? 이 값 중에 특히 secretOrKey는 환경변수로 설정하여 보안을 유지해야한다. 그러면 .env파일로 가서 secretOrKey의 값을 넣어주고 반영을 해보자.
# DB 접속 URL
DB_URL="디비주소"
# DB 프로젝트 이름
DB_NAME="디비이름"
# 서버 포트 번호
PORT=3000
# JWT토큰 발급 키
SECRET="secret"
다시 jwt.js로 가보자.
issuer와 audience는 사용하지 않을 것이기 때문에 일단 제외를 해 보자.
앞서 말했던 내용으로 코드를 짜면 아래와 같다.
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const UserModel = require('../../db/model/userModel');
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey = process.env.SECRET,
};
passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
await UserModel.findOne({ _id: jwtPayload.id }, function(err, user) {
if (err) {
return done(err, false);
}
if (user) {
return done(null, user);
} else {
return done(null, false);
// or you could create a new account
}
});
}));
payload는 id로 찾는게 기본 방법이며, 현재 나의 UserSchema에 id 필드는 _id로 되어있기 때문에 _id로 넣어주었다.
이제 local.js 와 똑같이 jwt 변수를 선언하고 그 안의 함수에 try / catch 문 과 if 조건문을 적용하여 코드를 만들어보자.
// 사용자가 JWT 토큰을 가지고 인증이 필요한 요청을 보낼 때 사용됨. 사용자의 인증 상태를 확인할 때 사용.
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const UserModel = require('../../db/model/userModel');
const opts = {
//env에서 선언한 secret 사용
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.SECRET,
};
const jwt = new JwtStrategy(opts, async (jwtPayload, done) => {
try {
const user = await UserModel.findOne({ _id: jwtPayload.id });
if (user) {
return done(null, user);
}
return done(null, false);
} catch (err) {
return done(err, false);
}
});
module.exports = jwt;
유저를 db에서 확인하여, 존재 할 경우 user 객체를 전달하도록 하였다.
역시나 기본코드와 방식은 똑같고 그냥 구조만 바꾸어 준 것이다.
local.js와 똑같이 await UserModel... 옆에 달려있던 또 하나의 function 함수를 catch문으로 맨 밑으로 내렸다.
이제 passport 폴더의 최상단 index.js 파일로 가서 jwt도 passport.use()에 담아서 내보내주자.
const passport = require('passport');
const local = require('./strategies/local');
const jwt = require('./strategies/jwt');
module.exports = () => {
passport.use(local);
passport.use(jwt);
};
이제 app.js로 가서 passport 폴더의 최상단 index.js에서 내보내준 모듈을 가져와주자.
const express = require('express');
const app = express();
const bcrypt = require('bcrypt');
const passport = require('passport');
const { userRouter, myPageRouter } = require("./routes");
require('./passport')(); // passport 초기화 및 전략 등록
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use('/', userRouter);
app.use('/my-page', myPageRouter)
module.exports = { app };
여기서 require('./passport')(); 로 특이하게 ()를 추가로 써준이유는 passport 설정을 가져와서 초기화를 해준다는 뜻인데,
왜 초기화를 해 주냐면?
passport는 세션이 설정되어 있거나 전략 등 기본으로 설정되어 있는 값이 있기 때문에 이 기본 설정을 초기화 해 주고 내가 설정한 전략과 토큰을 적용시켜야 한다.
이제 로그인에 필요한 전략을 다 만들었다. 다음은 본격적으로 토큰을 쿠키에 담아서 저장시킬 차례이다.
'개발일지 > HELLO FOLIO' 카테고리의 다른 글
HELLO FOLIO(express, mongoose) - 6(jwt 을 이용한 토큰 검증 미들웨어 구현) (0) | 2024.07.02 |
---|---|
HELLO FOLIO(express, mongoose)- 5(jwt를 이용한 토큰 발급 로그인 구현) (0) | 2024.07.02 |
HELLO FOLIO(express, mongoose)- 3(bcrypt를 이용한 회원가입 구현) (0) | 2024.06.27 |
HELLO FOLIO(express, mongoose)- 2(MongoDB,Mongoose 연결 구현) (0) | 2024.06.27 |
HELLO FOLIO(express, mongoose)- 1(기초 및 기획) (0) | 2024.06.22 |