1. 라우터 만들기
back/routes/user.js
router.post('/login', (req, res, next)=> { })
- 로그인 작업은 GET 인지 POST 인지 애매하기 때문에 POST 를 사용
2. Redux-Saga 함수 수정
front/sagas/user.js
function logInAPI(data) {
return axios.post('/user/login', data);
}
(...)
function* logIn(action) {
try {
const result = yield call(logInAPI, action.data);
console.log(result);
yield put({
type: LOG_IN_SUCCESS,
data: action.data,
});
} catch (err) {
yield put({
type: LOG_IN_FAILURE,
data: err.response.data,
});
}
}
2-1 baseURL
- 중복되는 api 의 주소로 용량을 줄이기 위해 index.js 에서 베이스URL 설정 되어 있는 상태
axios.defaults.baseURL = 'http://localhost:3065';
3. Passport 설치
npm i passport passport-local
- passport 는 카카오 로그인, 네이버 로그인, 구글 로그인, 깃헙 로그인, 링크드 로그인 등과 같이 소셜로그인으로 인해 늘어나는 로그인 기능을 통합하는 데 사용
- passport-local 은 이메일과 패스워드를 통해 로그인하는 일반적인 로그인 방식을 구현하는 데에 도움
4. passport 세팅
1. back 폴더에서 passport 폴더 생성 후, index.js 파일 만들기
- index.js 는 패스포트 설정 파일 역할을 함
2. 패스포트 설정
- passport 폴더에서 local.js 파일 생성
- local.js 파일은 앞서 말했듯이 이메일과 패스워드로 로그인하는 로컬 로그인 기능에 대한 전략(로직)을 구현하는 역할을 함
5. 패스포트 전략 작성(유효성 검사)
const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const { User } = require('../models');
const bcrypt = require('bcrypt');
module.exports = () => {
passport.use(
new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
},
async (email, password, done) => {
try {
const user = await User.findOne({
where: { email },
});
if (!user) {
return done(null, false, { reason: '존재하지 않는 이메일입니다.' });
}
const result = await bcrypt.compare(password, user.password);
if (result) {
return done(null, user);
}
return done(null, false, { reason: '틀린 비밀번호입니다. 다시 입력해주세요.' });
} catch (err) {
console.error(err);
}
}
)
);
};
- Strategy 이름을 LocalStartegy 로 바꿔주는 건데, 바꿔주는 이유는 다른 로그인 작업에 대해서도 전략을 짜게 되는데, 그 때 이름을 통해 구분하기 위해(KakakoStrategy, GoogleStrategy)
- Strategy 는 객체와 함수를 갖는데, 객체를 우선 보면, usernameField 와 passwordField 라는 속성을 갖는다. email 과 password 는 가장 처음 front 에서 email 과 password 로 받아 리덕스 사가에서 data. 이메일 & 패스워드로 바뀌고, 서버로 보내게 되면, req. 이메일 & 패스워드로 바뀌는데 패스포트에서는 이 이메일과 패스워드를 req. 를 빼고, email 과 password 라는 데이터를 받게 된다. (email 이 아닌 id 라는 변수명이였다면, id 가 됨) 즉, 첫 번째 객체는 reqbody 에 대한 설정이 됨
- 두 번째 함수는 로그인하는 전략을 세우는 역할을 함.
- 로그인 하는 유저가 회원인지 아닌지 유효성 검사를 하게 되는데, 회원정보는 아래와 같이 model 폴더의 user.js 를 import 해서 해당 유저가 회원인지 아닌지 조회할 수 있다.
- pasport 에서는 response 를 해주지 않기 때문에, done 이라는 메서드를 이용해 메시지를 남긴다. done(서버 에러, 성공, 클라이언트 에러)
- bcyrpt 의 compare 메서드를 통해 password 의 유효성 검사를 진행할 수 있는데, compare 도 비공기 함수여서 await 을 붙여주었음.
6. passport 실행 순서
- local.js 의 moule.exports 는 passport 폴더의 index.js 파일에서 import 한 뒤, local() 을 통해 실행.
const passport = require('passport');
const local = require('./local');
module.exports = () => {
passport.serializeUser(() => {});
passport.deserializeUser(() => {});
local(); // local.js 실행
};
- passport 폴더의 index.js 파일은 루트 폴더의 중앙통제센터인 app.js 에서 passport 폴더를 import 한 뒤, 실행
const passportConfig = require('./passport');
passportConfig();
7. 패스포트 전략은 언제 쓰이는지(passport.authenticate)
local.js 의 패스포트 전략은 back/routes/user.js 에서 유저 로그인 API 요청이 올 때, 진행되는데 패스포트는 일반 요청과 다름
passport.authenticate('local', (err, user, info) => {
if (err) {
console.err(err);
next(err);
}
if (info) {
return res.status(401).send(info.reason);
}
return req.login(user, async (loginErr) => {
if (loginErr) {
console.error(loginErr);
return next(loginErr);
}
return res.json(user);
});
})
- err, user, info 는 local.js 에서 done 의 매개변수 요청을 받는 역할
- err 가 생긴다면, next(err) 로 유효성 검사에서 익스프레스가 에러를 처리할 수 있게 하고 싶지만, 패스포트가 대신하고 있어 익스프레스 메서드(next, res, req)를 사용할 수 가 없음
8. 미들웨어 확장
패스포트를 사용하면서 익스프레스의 메서드를 사용하지 못하게 되는데, 이를 해결하기 위해 미들웨어 확장이라는 방법이 존재
router.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
console.err(err);
next(err);
}
if (info) {
return res.status(401).send(info.reason);
}
return req.login(user, async (loginErr) => {
if (loginErr) {
console.error(loginErr);
return next(loginErr);
}
return res.json(user);
});
})(req, res, next);
});
- passport.authenticate 같은 경우는 원래 req, res, next 를 쓸 수 없는 미들웨어인데, 이를 확장하는 방식을 사용해 익스프레스도 사용할 수 있게 미들웨어를 확장하는 방법.
- err 는 next 를 익스프레스로 처리
- info 는 클라이언트 에러가 있다는 경우이고, 이를 401 에러와 함께 이유를 사용자에게 알려줌.
- 마지막으로 로그인에 성공했다면, 패스포트에서 제공하는 req.login 을 통해 로그인을 할 수가 있음. 혹시 패스포트에서 에러가 생길 경우를 대비해 loginErr 을 매개변수로 받아 이를 next 로 익스프레스로 처리. 그렇지 않다면, 유저 정보를 response
9. 패스포트 로그인 세션
req.login 에 패스포트로 로그인하기 위해서는 패스포트가 로그인에 대한 데이터를 저장해줘야 합니다. 이 때 세션이라는 게 필요함.
1. express-session 설치
npm i express-session
2. cookie-parser 설치
npm i cookie-parser
3. 미들웨어 작성
app.js
(...)
const session = require('express-session');
(...)
(...)
app.use(cookieParser());
app.use(session());
app.use(passport.initialize());
app.use(passport.session());
(...)
- 쿠키랑 세션이 필요한 이유는 로그인을 하게 되면, 브라우저랑 서버랑 같은 정보를 들고 있어야 하기 때문. 쿠키는 토큰과 같은 역할을 해서 서버는 유저에게 다시 이메일과 패스워드 등 유저정보를 보내게 되면 보안에 취약할 수 있기 때문에 이 정보들을 보내지 않고, 임의의 쿠키값을 보내 유저는 회원정보를 다루거나 회원 개인 서비스를 사용할 때에 api 에 쿠키를 함께 보내면서 해당 서비스를 안전하게 사용할 수 있게 됨. 세션은 서버에서 프론트에서 보낸 쿠키를 보고 해당 쿠키는 특정 회원유저에 관한 데이터라는 것을 해석하는 역할을 함.
10. req.login
req.login 을 실행하면서 inidex.Serialize User 가 실행됨
back/passport/index.js
const passport = require('passport');
const local = require('./local');
const { User } = require('../models');
module.exports = () => {
passport.serializeUser((user, done) => {
done(user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findOne({ where: { id } });
done(null, user);
} catch (err) {
console.error(err);
done(err);
}
});
local();
};
- serializeUser 는 로그인 성공했을 때, 한 번만 실행되는데, 해당 유저의 정보를 모두 담는 것이 아닌 식별할 수 있는 고유 값, user.id 만 세션에 저장.
- deserializeUser 는 반대임. 세션에서 저장된 사용자의 식별자를 받아 해당 사용자를 데이터베이스에서 찾아내어 반환. 이후 요청이 들어올 때마다 호출되어 세션에 저장된 사용자를 복원. 에러가 발생하면 에러를 콘솔에 기록하고 done 함수를 호출하여 Passport에게 에러를 알림.
11. sesseion secret key 설정, dotenv
session 과 cookie-parser 를 사용하면서 시크릿 키를 설정해두어야 만약 외부에서 쿠키를 변경하려는 시도가 있더라도 key 의 값을 모른다면, 이를 막을 수 있습니다.
back/app.js
(...)
app.use(cookieParser('nnnsecret'));
app.use(
session({
saveUninitialized: false,
resave: false,
secret: 'nnnsecret',
})
);
(...)
- cookie-parser 미들웨어는 쿠키를 파싱하고, 쿠키와 관련된 작업을 처리하는 역할을 함. 여기서 nnnsecret 라는 문자열은 시크릿 키로 사용되어 쿠키를 서명한다. 이 서명은 쿠키의 무결성을 검증하는 데 사용되며 내용을 변경하려는 시도가 있으면 일치하지 않아 검증에 실패할 수 있게 막아준다.
- express-session 미들웨어는 세션을 관리하는 역할을 함.
- saveUnitialized: false 는 세션이 초기화되지 않은 상태에서도 세션 저장소에 저자을 하지 않는다는 의미로 세션이 필요한 경우에만 저장하게 됨.
- resave: false 는 요청이 왔을 때, 세션이 변경사항이 없더라도 세션을 다시 저장하지 않음. 변경된 경우에만 저장하도록 함.
- secret 은 cookie-parser 의 서명과 같은 역할을 함.
- 이 설정들을 통해 익스프레스 애플리케이션은 클라이언트에게 쿠키를 사용해 세션 식별자를 부여하고, 서버에서는 express-session 을 통해 세션을 안전하게 관리할 수 있게 됨.
12. 시크릿 키 보안 강화, dotenv
12-1.dot env 설치
npm i dotenv
12-2. back 폴더에 .env 파일 생성
12-3. 시크릿 키 작성
back/.env
COOKIE_SECRET=*******
DB_PASSWORD=************
12-4. 시크릿 키 변경
back/app.js
(...)
const dotenv = require('dotenv');
(...)
dotenv.config();
(...)
app.use(cookieParser('nnnsecret'));
app.use(
session({
saveUninitialized: false,
resave: false,
secret: process.env.COOKIE_SECRET,
})
);
- dotenv 를 import 한 뒤, config 메서드를 실행시키면 process.env.COOKIE_SECRET 이 루트폴더의 .env 파일에 있는 COOKIE_SECRET 으로 치환이 가능하다.
back/config/config.json
const dotenv = require('dotenv');
dotenv.config();
module.exports = {
development: {
username: 'root',
password: process.env.DB_PASSWORD,
database: 'NNN',
host: '127.0.0.1',
port: '3306',
dialect: 'mysql',
},
test: {
username: 'root',
password: null,
database: 'NNN',
host: '127.0.0.1',
dialect: 'mysql',
},
production: {
username: 'root',
password: null,
database: 'NNN',
host: '127.0.0.1',
dialect: 'mysql',
},
};
로그인 흐름
1. click event 발생
front/components/LoginForm.js
function LoginForm() {
const dispatch = useDispatch();
(...)
const onSubmitForm = useCallback(() => {
console.log(email, password);
dispatch(loginRequestAction({ email, password }));
}, [email, password]);
(...)
}
- onSubmitForm 함수 실행 -> saga 로 email, password 객체를 보냄
2. saga 실행
front/sagas/user.js
// api
function logInAPI(data) {
return axios.post('/user/login', data);
}
// saga
function* logIn(action) {
try {
const result = yield call(logInAPI, action.data);
console.log(result);
yield put({
type: LOG_IN_SUCCESS,
data: action.data,
});
} catch (err) {
yield put({
type: LOG_IN_FAILURE,
data: err.response.data,
});
}
}
- action.data 로 email, password 객체를 axios.port 로 api 실행하면서 email, passord 는 req 의 body 로 담겨 서버로 보냄
3. routes 에서 api 받고, passport.authenticate 실행
back/routes/user.js
(...)
// POST /user/login
router.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
console.err(err);
next(err);
}
if (info) {
return res.status(401).send(info.reason);
}
return req.login(user, async (loginErr) => {
if (loginErr) {
console.error(loginErr);
return next(loginErr);
}
return res.json(user);
});
})(req, res, next);
});
(...)
- req 의 body 에 담긴 eamil 과 passord 를 Authenticate 의 local 이 받아서 실행해서 localStrategy 전략 실행
4. localStartegy 실행
back/passport.local.js
module.exports = () => {
passport.use(
new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
},
async (email, password, done) => {
try {
const user = await User.findOne({
where: { email },
});
if (!user) {
return done(null, false, { reason: '존재하지 않는 이메일입니다.' });
}
const result = await bcrypt.compare(password, user.password);
if (result) {
return done(null, user);
}
return done(null, false, { reason: '틀린 비밀번호입니다. 다시 입력해주세요.' });
} catch (err) {
console.error(err);
}
}
)
);
};
- req 의 body 에 담긴 email 과 password 를 받아 전략(유효성 검사) 실행
- 성공 또는 실패에 대한 결과를 done 으로 에러, 성공, 클라이언트 에러 등을 받아 다시 routes 폴더의 user.js 파일의 authenticate 로 보냄
5. authnticate 실행
// POST /user/login
router.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
console.err(err);
next(err);
}
if (info) {
return res.status(401).send(info.reason);
}
return req.login(user, async (loginErr) => {
if (loginErr) {
console.error(loginErr);
return next(loginErr);
}
return res.json(user);
});
})(req, res, next);
});
- 미들웨어 확장을 통해 res, req, next 가 사용 가능한 passport.authenticate 는 done 을 통해서 받은 결과를 적절한 response 를 익스프레스로 처리.
(로그인에 성공해다면)
6. serialize 실행
back/passport/index.js
(...)
passport.serializeUser((user, done) => {
done(null, user.id);
});
(...)
- 로그인에 성공했다면, req.login 이 실행되고, passport 폴더의 index.js 파일의 serializeUser 를 통해 서버는 해당 유저의 모든 정보를 갖고 있는 것이 아닌 식별할 수 있는 고유 키인 user.id 와 쿠만 서버에서 들고 있음.
7. res.json 실행
back/rotues.user.js
// POST /user/login
router.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
console.err(err);
next(err);
}
if (info) {
return res.status(401).send(info.reason);
}
return req.login(user, async (loginErr) => {
if (loginErr) {
console.error(loginErr);
return next(loginErr);
}
return res.json(user); // 실행
});
})(req, res, next);
});
- return json() 을 통해 해당 유저의 정보와 함께 프론트로 보냄
'Next.js' 카테고리의 다른 글
[NNN]_credentials 로 쿠키 공유하기(Unauthuorized Error) (0) | 2024.02.09 |
---|---|
[NNN]_미들웨어로 라우터 검사하기 (0) | 2024.02.03 |
[NNN]_Redux 원리 (0) | 2024.01.25 |
[NNN]_트러블 슈팅 1 error:0308010c:digital envelope routines::unsupported (0) | 2024.01.24 |
[NNN]_Next.js 의 필요성 (0) | 2024.01.21 |