유저 인증용 스키마 작성
MongoDB에 저장할 데이터 구조 즉, schema를 작성하고 외부에서 사용할 수 있도록 export 했다.
일단은 유저 회원가입 및 로그인에 필요한 내용만 작성했다.
root에서 model 디렉토리를 만들고 그안에 User.js 파일을 생성했다.
const mongoose = require('mongoose');
const saltRounds = 10;
/* userSchema */
const userSchema = new mongoose.Schema(
{
nickname: {
type: String,
maxlength: 10,
trim: true,
unique: true,
// required: true,
},
email: {
type: String,
trim: true,
unique: true,
// required: true,
},
password: {
type: String,
minlength: 5,
trim: true,
required: true,
},
role: {
type: Number,
default: 0,
},
token: {
type: String,
},
tokenExp: {
type: Number,
},
},
{ autoIndex: false },
);
const User = mongoose.model('User', userSchema);
module.exports = { User };
로그인/로그아웃(feat.Bycrypt & JWT)
회원가입 및 로그인 기능을 구현하면서 암호화와 토큰생성하는 법을 자세하게 작성해놓은 포스트를 찾게되어 적용해봤다.
Bycrypt와 JWT를 설치하고 아래와 같은 과정을 거치게 만들었다.
Bcrypt로 비밀번호 암호화
데이터베이스에 회원가입 정보를 저장하기 전에 (처음 회원 가입할 때, 비밀번호를 바꿀 때) bcrypt.genSalt를 이용해서 비밀번호를 암호화한 후 데이터베이스에 보내도록 한다.
/* Bcrypt */
// 비밀번호 암호화
userSchema.pre('save', function (next) {
const user = this;
// salt를 이용해서 비밀번호 암호화한 후 보내줌 (비밀번호와 관련될 때만)
if (user.isModified('password')) {
bcrypt.genSalt(saltRounds, (err, salt) => {
if (err) return next(err);
bcrypt.hash(user.password, salt, (err, hash) => {
if (err) return next(err);
user.password = hash;
next();
});
});
} else {
// 그 외에는 그냥 내보냄
next();
}
});
로그인 기능 및 토큰 생성
- client로부터 로그인을 위한 정보들이 입력되면 findOne을 통해 요청된 이메일이 데이터베이스에 있는지찾는다.
- 요청된 이메일이 데이터베이스에 있다면, 비밀번호가 맞는 비밀번호인지 확인하기 위해 comparePassword 를 통해 입력된 평문의 비밀번호를 복호화하여 암호화된 비밀번호와 같은지 확인한다.
- 비밀번호까지 맞다면 누구인지 구분하기 위한 토큰을 generateToken을 통해 생성한다.
- 토큰이 생성되면 쿠키에 저장한다.
// 로그인 - 비밀번호 비교
userSchema.methods.comparePassword = function (plainPassword, cb) {
// 입력된 비밀번호와 데이터베이스에 있는 암호화된 비밀번호가 같은지 확인(비교) -> 평문을 암호화해서 비교
bcrypt.compare(plainPassword, this.password, (err, isMatch) => {
if (err) return cb(err);
cb(null, isMatch); // 즉, true
});
};
// 로그인 - 토큰 생성
userSchema.methods.generateToken = function (cb) {
const user = this;
// jsonwebtoken을 이용해서 토큰 생성
const token = jwt.sign(user._id.toHexString(), 'secretToken');
// user._id + 'secretToken' = token 을 통해 토큰 생성
// 토큰 해석을 위해 'secretToken' 입력 -> user._id 가 나옴
// 토큰을 가지고 누구인지 알 수 있는 것
user.token = token;
user.save(function (err, user) {
if (err) return cb(err);
cb(null, user);
});
};
인증기능
- 페이지를 이동할 때마다 로그인이 되어있는지에 대한 인증을 위해 사용한다.
- 클라이언트 쿠키에서 토큰을 가져와 복호화한 후, 클라이언트의 토큰과 데이터베이스의 토큰이 일치하는지 확인한다.
const { User } = require('../model/User');
let auth = (req, res, next) => {
// 클라이언트 쿠키에서 토큰을 가져온다.
let token = req.cookies.x_auth;
// 토큰을 복호화한 후 유저를 찾는다.
User.findByToken(token, (err, user) => {
if (err) throw err;
if (!user) return res.status(400).json({ isAuth: false, error: true });
req.token = token;
req.user = user;
next();
});
// 유저가 있으면 인증 OK!
// 유저가 없으면 인증 NO!
};
module.exports = { auth };
// auth 인증 - 복호화(토큰 디코드)
userSchema.statics.findByToken = function (token, cb) {
const user = this;
jwt.verify(token, 'secretToken', (err, decoded) => {
// 유저 아이디를 이용해서 유저를 찾은 다음에
// 클라이언트에서 가져온 token과 DB에 보관된 토큰이 일치하는지 확인
user.findOne({ _id: decoded, token: token }, function (err, user) {
if (err) return cb(err);
cb(null, user);
});
});
};
로그아웃
클라이언트에서 findOneAndUpdate를 이용해 클라이언트의 토큰을 token: "" 으로 지워주게 되면자동으로 인증이 풀리게 되어 로그아웃된다.
router.post('/users/logout', auth, (req, res, next) => {
User.findOneAndUpdate({ token: req.user.token }, { token: '' }, (err, user) => {
if (err) {
return res.json({ success: false, err });
}
return res.status(200).send({
success: true,
});
});
});
ReactQuery
유저의 접속 상태 관리를 위해 SWR과 ReactQuery중 고민하다가 뭐가 더 추세인지 여러 사람에게 물어봤더니 ReactQuery가 추세라고 하여 사용해봤다.
물론 사용하기 위해 또 공부.. 또 공부..
처음엔 그저 사용법과 코드만 알고 무작정 프로젝트에 끼얹다가 혼났다.
언제 useQuery를 사용하는지 언제 useMutation을 사용하는지 알게됐고 일단은 로그인 기능에만 사용해봤다.
일단 사용할 로그인용 api 함수를 따로 만들어 놓고
import axios from 'axios';
const loginApi = (body) => {
axios
.post('/api/users/login', body, { withCredentials: true })
.then((res) => {
console.log('로그인 성공');
})
.catch((error) => {
console.log('로그인 실패');
console.log(error.response);
});
};
export default loginApi;
사용하지는 않지만 useMutation이 돌려주는 상태들에 대해 적어보고 이를 이용해서 useEffect와 함께 페이지 이동을 시켰다.
import loginApi from '@apis/loginApi';
import useInput from '@hooks/useInput';
import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useEffect, useState } from 'react';
const LogIn = () => {
const navigate = useNavigate();
const [logInError, setLogInError] = useState(false);
const [email, onChangeEmail] = useInput('');
const [password, onChangePassword] = useInput('');
/* useMutation */
const loginMutation = useMutation(loginApi, {
onMutate: (variable) => {
console.log('onMutate', variable);
},
onError: (error, variable, context) => {
console.log('Error');
},
onSuccess: (data, variables, context) => {
// console.log('Success', data, variables, context);
},
onSettled: () => {
// console.log('End');
},
});
/* useEffect */
useEffect(() => {
if (isLoggedIn) navigate('/'); // 로그인 성공하면 메인페이지로 이동
}, [isLoggedIn, navigate]);
/* Submit */
const onSubmit = useCallback(
(e) => {
e.preventDefault();
setLogInError(false);
loginMutation.mutate({ email, password }); // mutation함수로 보낼 변수
},
[email, password, loginMutation],
);
return (
// 로그인용 UI...
);
};
export default LogIn;
하지만 늦게 안 사실.. useMutation을 사용해서 돌려 받는 값으로 후처리를 할 수 있을 것 같다.
주말에 수정해봐야지. 일단 성공은 했다.
Redux
이것 또한 늦게 안 사실.. ReactQuery만으로 유저의 접속상태를 관리할 수 있다.
근데 바보처럼 Redux까지 사용해가면서 환경을 만들고 프로젝트에 끼얹고 그렇게 로그인 상태 변경해보는짓을 해버렸다.
rootReducer를 만들고
/* root reducer */
import loginReducer from './loginReducer';
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
loginReducer,
});
export default rootReducer;
메인 client파일을 Provider로 감싸주고 store와 연결하고
const store = createStore(rootReducer, enhancer);
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
</QueryClientProvider>
</React.StrictMode>,
);
로그인용 reducer까지 만들었던 나..
export const USER_LOGIN = 'USER_LOGIN';
export const loginSuccess = (isLoggedIn) => ({ type: USER_LOGIN, isLoggedIn });
const initalState = {
isLoggedIn: false,
};
const loginReducer = (state = initalState, action) => {
switch (action.type) {
case USER_LOGIN:
return {
...state,
isLoggedIn: true,
};
default:
return state;
}
};
export default loginReducer;
마지막으로 useMutation에서 onMutate상태가 되면 디스패치로 로그인 성공한 함수를 보내주고..
const loginMutation = useMutation(loginApi, {
onMutate: (variable) => {
console.log('onMutate', variable);
dispatch(loginSuccess());
},
...
물론 성공은 했다..
이번 주말에는 React Query만을 사용해서 유저 인증 서비스를 다 구축해보자.