Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/auth : 로컬 로그인 / 카카오 로그인 #3

Merged
merged 9 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
420 changes: 420 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,23 @@
"homepage": "https://github.com/Murakano/murakano-be#readme",
"dependencies": {
"ajv": "^8.16.0",
"ajv-formats": "^3.0.1",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"moment": "^2.30.1",
"mongoose": "^8.4.3",
"morgan": "^1.10.0",
"nodemon": "^3.1.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pm2": "^5.3.1"
},
Expand Down
6 changes: 6 additions & 0 deletions src/common/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ const conf = {
port: process.env.PORT,
corsWhiteList: process.env.CORS_WHITELIST,
mongoURL: process.env.MONGO_URL,
jwtSecret: process.env.JWT_SECRET,
cookieSecret: process.env.COOKIE_SECRET,

// social login
kakaoRestApiKey: process.env.KAKAO_REST_API_KEY,
kakaoCallback: process.env.KAKAO_CALLBACK,
};

module.exports = conf;
2 changes: 2 additions & 0 deletions src/common/constants/error-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const ErrorMessage = Object.freeze({
EMAIL_CHECK_ERROR: '이메일 중복검사중 오류가 발생하였습니다.',
EXIST_NICKNAME: '이미 존재하는 닉네임 입니다.',
EXIST_EMAIL: '이미 존재하는 이메일 입니다.',
LOGIN_ERROR: '로그인중 오류가 발생하였습니다.',
KAKAO_LOGIN_ERROR: '카카오 로그인중 오류가 발생하였습니다.',
});

module.exports = ErrorMessage;
7 changes: 6 additions & 1 deletion src/common/constants/success-message.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
const SucesssMessage = Object.freeze({
// USER
// USER - 회원가입
REGISTER_SUCCESSS: '회원가입 성공',
AVAILABLE_NICKNAME: '사용 가능한 닉네임입니다.',
AVAILABLE_EMAIL: '사용 가능한 이메일입니다.',

// USER - 로그인
LOGIN_SUCCESSS: '로그인 성공',

GET_PROFILE_SUCCESS: '유저 정보 조회 성공',
});

module.exports = SucesssMessage;
30 changes: 28 additions & 2 deletions src/common/modules/express/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
const express = require('express');
const morgan = require('morgan');
const passport = require('passport');
const session = require('express-session');
const helmet = require('helmet');
const cors = require('cors');
const crypto = require('crypto');
const cookieParser = require('cookie-parser');

const conf = require('../../config/index');
const conf = require('../../config');
const passportConfig = require('../../passport');
const router = require('../../../routes/index');

module.exports = expressLoader = (app) => {
passportConfig();

app.use(morgan('dev'));
app.use(helmet());

Expand All @@ -23,14 +28,35 @@ module.exports = expressLoader = (app) => {
cors({
credentials: true,
origin: (origin, callback) => {
if (origin !== null || conf.corsWhiteList?.indexOf(origin) !== -1) {
if (origin === undefined || (origin && conf.corsWhiteList?.indexOf(origin) !== -1)) {
return callback(null, true);
}
callback(new Error('CORS ERROR'));
},
})(req, res, next);
});

app.use(
session({
name: 'user',
resave: false,
saveUninitialized: false,
secret: conf.cookieSecret,
cookie: {
// cleint 쿠키 접근 불가
httpOnly: true,
// TODO : ssl 적용하면 true로 변경
secure: false,
// 24h
maxAge: 86400000,
},
})
);

// Passport 세팅
app.use(passport.initialize());
app.use(passport.session());

// Body Parser 세팅
app.use(
express.json({
Expand Down
23 changes: 23 additions & 0 deletions src/common/passport/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const passport = require('passport');
const localStrategy = require('./localStrategy');
const jwtStrategy = require('./jwtStrategy');
const User = require('../../routes/user/user.model');

module.exports = () => {
passport.serializeUser((user, done) => {
done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});

// 초기화
localStrategy();
jwtStrategy();
};
27 changes: 27 additions & 0 deletions src/common/passport/jwtStrategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const passport = require('passport');
const User = require('../../routes/user/user.model');
const config = require('../config'); // 비밀 키를 저장한 파일

const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.jwtSecret,
};

module.exports = () => {
passport.use(
new JwtStrategy(opts, async (jwtPayload, done) => {
try {
const user = await User.findById(jwtPayload.id);
if (user) {
return done(null, user);
} else {
return done(null, false);
}
} catch (error) {
console.error(error);
return done(error, false);
}
})
);
};
30 changes: 30 additions & 0 deletions src/common/passport/localStrategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('../../routes/user/user.model');

module.exports = () => {
passport.use(
new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
},
async (email, password, done) => {
try {
const user = await User.findOne({ email });
if (!user) {
return done(null, false, { message: '가입되지 않은 회원입니다.' });
}
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return done(null, false, { message: '비밀번호가 일치하지 않습니다.' });
}
return done(null, user);
} catch (error) {
console.error(error);
return done(error);
}
}
)
);
};
57 changes: 57 additions & 0 deletions src/common/utils/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const jwt = require('jsonwebtoken');
const passport = require('passport');
const config = require('../config');

exports.generateToken = (user) => {
return jwt.sign(
{
id: user._id,
email: user.email,
nickname: user.nickname,
},
config.jwtSecret,
{ expiresIn: '24h' }
);
};

exports.verifyToken = (token) => {
return jwt.verify(token, config.jwtSecret);
};

exports.isAuthenticated = (req, res, next) => {
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
return res.status(500).json({ message: '서버 오류' });
}
if (!user) {
return res.status(401).json({ message: '인증되지 않은 사용자' });
}
req.user = user;
next();
})(req, res, next);
};

exports.isLoggedIn = (req, res, next) => {
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
return res.status(500).json({ message: '서버 오류' });
}
if (!user) {
return res.status(401).json({ message: '로그인이 필요합니다.' });
}
req.user = user;
next();
})(req, res, next);
};

exports.isNotLoggedIn = (req, res, next) => {
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
return res.status(500).json({ message: '서버 오류' });
}
if (user) {
return res.status(403).json({ message: '이미 로그인된 상태입니다.' });
}
next();
})(req, res, next);
};
37 changes: 37 additions & 0 deletions src/common/utils/kakao.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const axios = require('axios');
const conf = require('../../common/config');

const header = {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
Authorization: 'Bearer ',
};
exports.getKakaoToken = async (code) => {
const data = {
grant_type: 'authorization_code',
client_id: conf.kakaoRestApiKey,
code,
};

const queryString = Object.keys(data)
.map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(data[k]))
.join('&');

const token = await axios.post('https://kauth.kakao.com/oauth/token', queryString, { headers: header });
return { accessToken: token.data.access_token };
};
exports.getUserInfo = async (accessToken) => {
// Authorization: 'Bearer access_token'
header.Authorization += accessToken;

// 카카오 사용자 정보 조회
const get = await axios.get('https://kapi.kakao.com/v2/user/me', { headers: header });
const result = get.data;

return {
snsId: result.id,
email: result.kakao_account.email ? result.kakao_account.email : `${result.id}@no.agreement`,
// NOTE: 닉네임 10글자 제한 때문에, 임시 처리
// kakao 닉네임 규정은 20글자. result.id는 10글자로 추정
nickname: result.id,
};
};
2 changes: 2 additions & 0 deletions src/common/utils/request.validator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const ErrorMessage = require('../../common/constants/error-message');
const Ajv = require('ajv');
const addFormats = require('ajv-formats');
const ajv = new Ajv();
addFormats(ajv);

exports.validateRequest = (schema, req) => {
const validate = ajv.compile(schema);
Expand Down
Loading