구현 환경
react, mongoose, mongoDB
server/models/User.js
const mongoose=require('mongoose');
//스키마 생성
const userSchema=mongoose.Schema({
name: {
type: String,
},
email: {
type:String,
trim:true, //공백 제거
unique:1,
},
password: {
type:String,
},
lastname: {
type:String,
},
image: String,
token: {
type:String,
},
tokenExp: {
type:Number, //토큰 유효기간
},
});
//스키마를 모델로 감싸줌
const User = mongoose.model('User',userSchema);
module.exports = { User }
server/routes/signup
app.post('/', (req,res) => {
const user = new User(req.body);
user.save((err, doc) => {
if(err) return res.status(400).json({ success:false, err });
return res.status(200).json({ success:true });
});
});
bcrypt를 이용해 비밀번호를 암호화 해 데이터베이스에 안전하게 저장한다.
유저 정보를 데이터베이스에 저장하기 전에 모델에서pre
로 비밀번호 암호화한 후next
넘겨 저장한다.
설치 :npm i bcrypt --save
server/models/User.js
const bcrypt=require('bcrypt');
const saltRounds=10; //암호 자릿수
//스키마 생성
userSchema.pre("save", function (next) {
var user = this; //useSchema
//다른 유저 정보(이메일 등)가 아닌 패스워드 변경시에만 암호화 시키도록 함
if (user.isModified("password")) {
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) return next(err);
//솔트를 이용해 해시값 생성
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
user.password = hash;
//save 메서드 내부 수행
next();
});
});
} else {
next();
}
});
//스키마를 모델로 감싸줌
const User = mongoose.model("User", userSchema);
module.exports = { User };
쿠키를 사용하기 위해 설정해준다.
설치 :npm install cookie-parser --save
server/app
const cookieParser = require('cookie-parser');
app.use(cookieParser());
db에 암호화된 비밀번호를 입력된 비밀번호(plainPassword)와 비교하는 인스턴스 메서드인
comparePassword
를 생성한다.
복호화할 수 없으므로 입력된 비밀번호를 암호화한 후 비교한다.
server/models/User.js
userSchema.methods.comparePassword = function (plainPassword, cb) {
//plainPassword와 암호화 된 비밀번호(해싱값)가 같은지 체크
bcrypt.compare(plainPassword, this.password, function (err, isMatch) {
if (err) return cb(err);
cb(null, isMatch);
});
};
jwt를 이용해 user의 id와 토큰 키'secretToken'를 결합해 토큰을 생성한다.
로그인을 하면 토큰을 생성한 후 데이터 베이스의 유저 다큐먼트의 token 필드에 저장한다.
그리고 토큰을 브라우저에게 보내준다.
토큰은 쿠키에 저장될 것이다.
npm install jsonwebtoken --save
server/models/User.js
const jwt = require('jsonwebtoken');
userSchema.methods.generateToken = function (cb) {
var user = this;
//jsonwebtoken을 이용해 토큰 생성
var token = jwt.sign(user._id.toHexString(), "secretToken");
user.token = token;
user.save(function (err, user) {
if (err) return cb(err);
cb(null, user);
});
};
비밀번호 확인과 토큰 생성을 위해 미리 만들어 둔 인스턴스 메서드인
comparePassword와 generateToken을 사용한다.
- comparePassword에서는 isMatch를 받아오고
- generateToken에서는 user를 받아온다.
로그인 인증이 끝나면 토큰을 쿠키에 저장한다.
server/routes/user
app.post("/login", (req, res) => {
//이메일이 데이터베이스에 존재하는지 확인
User.findOne({ email: req.body.email }, (err, user) => {
if (!user)
return res.status(400).json({ success: false, message: "이메일이 틀림" });
//비밀번호가 데이터베이스에 있는 것과 일치하는지 확인
user.comparePassword(req.body.password, (err, isMatch) => {
if (!isMatch)
return res.json({ success: false, message: "비밀번호가 틀림" });
//비밀번호까지 일치하면, 토큰 생성
user.generateToken((err, user) => {
if (err) return res.status(400).send(err);
res
.cookie("x_auth", user.token)
.status(200)
.json({ success: true, userId: user.id });
});
});
});
});
로그인이 성공한 것을 확인.
데이터베이스에서 해당 다큐먼트에 토큰이 생성된 것 확인.
로그인 후 브라우저에 쿠키가 들어온 것을 확인.
페이지 이동 때마다 로그인 여부를 체크하는 것이 필요하다.
이를 Auth를 통해 구현할 수 있다.
이 전에 서버 부분에서 데이터베이스에 토큰 정보를 저장하고, 클라이언트 부분에서는 쿠키에 토큰을 저장하였다.
이를 이용한 인증 절차는 다음과 같다.
클라이언트에서 서버로 쿠키에 담겨져 있는 토큰을 전달
서버에서 토큰을 복호화해 user id를 얻어냄
해당 id를 가진 유저가 데이터베이스에 있는지에 대한 여부를 확인
인증을 구현하는 함수인 findByToken을 생성할 것이다.
생성되는 함수는 스태틱 메서드이다.
토큰을 복호화 한 후 해당 토큰의 유저를 찾아낸 후 미들웨어 auth로 user를 보내준다.
server/models/User.js
userSchema.statics.findByToken = function (token, cb) {
var user = this;
//auth 미들웨어에서 받아온 토큰 복호화
jwt.verify(token, "secretToken", function (err, decoded) {
if (err) return cb(err);
user.findOne({ _id: decoded, token: token }, function (err, user) {
if (err) return cb(err);
cb(null, user);
});
});
};
server/middleware/auth.js
const { User } = require("../models/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.json({ isAuth: false, error: true });
//유저가 있으면 인증 성공
req.token = token;
req.user = user;
next();
});
};
module.exports = { auth };
생성해 놓은 auth 미들웨어를 통해 인증을 구현할 것이다.
미들웨어에서 req.token과 req.user를 보내줬으므로 가져와서 사용할 수 있다.
server/routes/user
const { auth } = require('./middleware/auth');
//auth 미들웨어를 통해 인증 절차 수행
app.get("/auth", auth, (req, res) => {
//미들웨어를 통과해 여기까지 왔으면 인증이 성공했다는 것
res.status(200).json({
_id: req.user._id,
isAuth: true,
email: req.user.email,
lastname: req.user.lastname,
role: req.user.role,
image: req.user.image,
});
});
인증 후 유저 정보를 받아온 것을 확인.
auth api를 이용.
프로필, 정보수정 등과 같이 로그인 되어 있어야 접근할 수 있는 페이지에 접근 했을때 HOC를 이용해 브라우저에서 로그인 되어 있는지 체크하고,
로그인 되어있지 않았다면 로그인 페이지로 넘어가도록 설계한다.
Auth HOC를 생성하여 준다.
hoc폴더 생성 -> Auth 파일 생성
option
- true : 로그인한 유저만 출입이 가능한 페이지
- false: 아무나 출입이 가능한 페이지
auth.js
export default function (Component, option) {
function AuthCheck(props) {
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
dispatch(auth()).then((response) => {
//로그인 하지 않은 상태일 때
if (!response.payload.isAuth) {
//로그인이 필요한 페이지 접근
if (option) {
//로그인 페이지로 이동
navigate("/login");
}
}
});
}, []);
return <Component />;
}
return AuthCheck;
}
auth()액션을 통해 리덕스에서 서버에 인증 api 처리를 request 해준다.
export function auth() {
const request = Axios.get("/auth").then(
(response) => response.data
);
return {
type: AUTH_USER,
payload: request,
};
}
export default function (state = {}, action) {
switch (action.type) {
case AUTH_USER:
return { ...state, userData: action.payload };
break;
default:
return state;
}
}
Auth의 첫 번째 인자로 해당 컴포넌트를 넣고, 두번 째 인자로 option이 true/false인지 넣어준다.
next 환경일 경우
import Auth from './hoc/auth'
function Profile(props) {}
export default Auth(Profile, true);
react 환경일 경우
import Auth from './hoc/auth'
function App() {
return (
<div className="App">
<Router>
<div>
<Switch>
<Route path="/profile" component={Auth(Profile, true)} />
</Switch>
</div>
</Router>
</div>
);
}
로그아웃 하려는 유저를 데이터베이스에서 찾은 후 해당 유저의 토큰을 지워준다.
토큰이 없으면 인증이 불가능 하기 때문에 로그아웃된다.
먼저 미들웨어로 인증 과정을 수행하고, 그 후 해당 아이디를 가진 유저의 토큰을 지워준다.
server/routes/user
app.get("/logout", auth, (req, res) => {
//req.user를 미들웨어에서 가져올 수 있다.
User.findOneAndUpdate({ _id: req.user._id }, { token: "" }, (err, user) => {
if (err) return res.json({ success: false, err });
return res.status(200).send({ success: true });
});
});
success를 반환. (로그아웃 성공)
token부분이 빈문자열로 사라져 있는것을 확인.
로그인 api를 작성할 때 쿠키에 x_auth 값으로 token을 보내줬던 것을 이용해 로그아웃을 구현하였다.
res .cookie("x_auth", user.token) .status(200) .json({ success: true, userId: user.id });
server/routes/user
router.get("/logout", (req, res) => {
const { x_auth } = req.cookies;
User.findOneAndUpdate({ token: x_auth }, { token: "" }, (err, user) => {
if (err) return res.json({ success: false, err });
return res.status(200).send({ success: true, user });
});
});
포스트맨으로 로그아웃 했을 때는 잘 됬다. 아마 포스트맨에서 자동으로 브라우저에서 x_auth를 쿠키로 받아온 듯하다.
실제 localhost에서 사용했을 땐 브라우저에서 쿠키를 자동으로 받아오질 못하므로 req.cookies.x_auth로 직접 넣어줬다.
미들웨어인 auth를 빼버렸고, 브라우저에서 가져온 x_auth를 데이터 베이스에서 token을 갖고있는 user 데이터와 대조하여 그 user 데이터를 찾고 토큰을 제거하여 쿠키를 없애 로그아웃 시켰다.
추후에 개인 프로젝트 또는 실무에서 회원가입에서 인증까지의 기능을 구현할 때 참고할 수 있도록 정리해 보았습니다.