Multi User
passport 수업에서는 단일 사용자에 대한 로그인, 로그아웃을 구현해보았다.
이번에는 회원가입 기능을 구현하고, 다중 사용자가 웹 사이트를 사용할 수 있도록 해보자!
회원가입 UI
< lib/auth.js >
statusUI: function(request, response){
var authStatusUI = '<a href="/auth/login">login</a> | <a href="/auth/register">register</a>';
if(this.isLogin(request, response)){
authStatusUI = `${request.user.nickname} | <a href="/auth/logout">logout</a>`;
}
return authStatusUI;
}
< template.js >
HTML:function(title, list, body, control,
authStatusUI='<a href="/auth/login">login</a> | <a href="/auth/register">register</a>'){
return `
<!doctype html>
<html>
<head>
<title>WEB1 - ${title}</title>
<meta charset="utf-8">
</head>
<body>
${authStatusUI}
<p>새로고침을 해야 로그인이 반영돼요 ㅠㅅㅠ</p>
<h1><a href="/">WEB</a></h1>
<a href="/author">author</a>
${list}
${control}
${body}
</body>
</html>
`;
}
< routes/auth.js >
router.get('/register', function(request, response){
// var fmsg = request.flash();
// var feedback = '';
// if(fmsg.error){
// feedback = fmsg.error[0];
// }
// <div style="color:red;">${feedback}</div>
var title = 'Register';
var html = template.HTML(sanitizeHtml(title), '',
`
<form action="/auth/register_process" method="post">
<p><input type="text" name="id" placeholder="id"></p>
<p><input type="password" name="password" placeholder="password"></p>
<p><input type="password" name="password2" placeholder="password again"></p>
<p><input type="text" name="nickname" placeholder="nickname"></p>
<p>
<input type="submit" value="register">
</p>
</form>
`,
``
);
response.send(html);
});
회원 정보 저장
생활코딩에서는 lowdb를 쓰지만, 나는 MySQL을 쓰고 싶다!
그래서 register_process의 코드를 아래와 같이 작성하여 SQL에 저장하였다.
npm install -s shortid
router.post('/register_process', function(request, response){
var post = request.body;
var id = post.id;
var password = post.password;
var password2 = post.password2;
var nickname = post.nickname;
if(password !== password2){
request.flash('error', 'Password must be same!');
response.redirect('/auth/register');
} else{
db.query(`
INSERT INTO auth (count, id, password, nickname) VALUES(?, ?, ?, ?)`,
[shortid.generate(), id, password, nickname],
function(error, result){
if(error) throw error;
response.redirect(`/`);
}
);
}
});
강의에서는 password & password2 일치 여부만 확인했는데 이것 말고도 기존에 있는 사용자 정보와 같은 것이 있는지, 입력을 안한 정보가 있는지 등을 확인해야 한다.
+ password & password2가 다를 때 리다이렉트는 되는데 flash message가 뜨지 않는다 ㅠㅠ
세션 스토어에 저장 & 로그인 구현
이 부분은 강의와 다른 부분이 많아 https://gaemi606.tistory.com/entry/Nodejs-Passportjs-passport-local-MySQL 를 참고하여 코드를 작성하였다.
< passport.js >
const db = require('../lib/db.js');
module.exports = function(app){
const passport = require('passport')
, LocalStrategy = require('passport-local').Strategy;
app.use(passport.initialize());
app.use(passport.session()); // 세션 이후에 serializeUser 등 세션 이용하는 메서드들 작성
passport.serializeUser(function(user, done){
console.log('serializeUser', user);
done(null, user.id);
});
passport.deserializeUser(function(id, done){
var userinfo;
db.query('SELECT * FROM auth WHERE id=?', [id],
function(err, result){
if(err) throw err;
console.log('deserializeUser', result);
var json = JSON.stringify(result[0]);
userinfo = JSON.parse(json);
done(null, userinfo);
});
});
passport.use(new LocalStrategy(
{
usernameField: 'id'
// passwordField: 'password'
},
function(username, password, done){
db.query('SELECT * FROM auth WHERE id=? AND password=?', [username, password],
function(err, result){
console.log('result', result);
if(err) throw err;
if(result.length == 0){ // 입력한 id, password와 일치하는 회원정보가 없는 경우
return done(null, false, { message: 'Incorrect' });
} else{
var json = JSON.stringify(result[0]);
var userinfo = JSON.parse(json);
console.log('userinfo', userinfo);
return done(null, userinfo);
}
});
}
));
return passport;
}
그리고 언젠가부터 sucessRedirect가 되도 홈으로 이동만 하고, 세션 파일에 사용자 정보가 추가가 되지 않아 request.user 객체가 undefined가 되는 현상이 발생하였다.
오늘 passport 수업의 세션이용2 영상에 달린 댓글을 보고 이를 해결할 수 있었다. (o゚v゚)ノ
로그아웃에서 처리해주듯이 세션이 저장된 것을 확인하고 redirect를 해주는 것이었다.
< routes/auth.js >
router.post('/login_process',
passport.authenticate('local', {
failureRedirect: '/auth/login',
failureFlash: true
}), function(request, response){
request.session.save(function(err){
response.redirect('/');
}
);
}
);
접근제어 - 글쓰기
그리고 이제 multi user에 대한 CRUD 접근제어를 해야한다. 그러기 위해서 일단 이제까지 했던 author 관련 코드들을 지워보자...!
template.js, home.js 등등 모든 파일에서 author 관련 코드를 지웠다.
DB에서도 author 테이블을 지우고, topic 테이블도 다시 만들었다.
그리고 우선적으로 글 목록과 글쓰기에 대한 코드를 수정해주었다.
// 게시글 생성
router.get('/create', function(request, response){
if(!auth.isLogin(request, response)){
response.redirect('/');
return false;
}
db.query(`SELECT * FROM topic`, function(error, topics){
var title = 'Create';
var list = template.list(topics);
var html = template.HTML(sanitizeHtml(title), list,
`
<form action="/topic/create_process" method="post">
<p><input type="text" name="title" placeholder="title"></p>
<p>
<textarea name="description" placeholder="description"></textarea>
</p>
<p>
<input type="submit">
</p>
</form>
`,
``,
auth.statusUI(request, response)
);
response.send(html);
});
});
router.post('/create_process', function(request, response){
if(!auth.isLogin(request, response)){
response.redirect('/');
return false;
}
var post = request.body;
var title = post.title;
var description = post.description;
var author = post.author;
db.query(`
INSERT INTO topic (title, description, created, user_id)
VALUES(?, ?, NOW(), ?)`,
[title, description, request.user.count],
function(error, result){
if(error) throw error;
response.redirect(`/topic/${result.insertId}`);
}
);
});
// 상세글
router.get('/:pageId', function(request, response){
db.query(`SELECT * FROM topic`, function(error, topics){
if(error) throw error;
db.query(`SELECT * FROM topic left join auth on user_id=count WHERE topic.id=?`,
[request.params.pageId], function(error2, topic){
if(error2) throw error2;
var title = topic[0].title;
var description = topic[0].description;
var nickname = topic[0].nickname;
var list = template.list(topics);
var html = template.HTML(title, list,
`<h2>${sanitizeHtml(title)}</h2>
${sanitizeHtml(description)}
<p>by ${nickname}</p>`,
`
<a href="/topic/create">create</a>
<a href="/topic/update/${request.params.pageId}">update</a>
<form action="/topic/delete_process" method="post">
<input type="hidden" name="id" value="${request.params.pageId}">
<input type="submit" value="delete">
</form>
`,
auth.statusUI(request, response)
);
response.send(html);
});
});
});
이처럼 이제 로그인한 사용자가 작성한 글에 그 사용자의 닉네임이 뜨도록 하였다!
접근제어 - 글수정
글을 작성한 사람만 그 글을 수정할 수 있도록 하기 위해서는 현재 로그인한 사용자와 글을 작성한 사람을 구분할 수 있어야 한다.
var count = request.user.count; // 로그인한 사람의 아이디
db.query(`SELECT * FROM topic left join auth on count=user_id WHERE topic.title=?`,
[title], function(error, topic){
if(error) throw error;
console.log('topic', topic[0].user_id);
console.log('count', count);
});
request.user.count(로그인)와 left join한 테이블의 topic[0].user_id(작성자)를 통해 둘을 구분할 수 있다!
router.post('/update_process', function(request, response){
if(!auth.isLogin(request, response)){
response.redirect('/');
return false;
}
var post = request.body;
var id = post.id;
var title = post.title;
var description = post.description;
var count = request.user.count; // 로그인한 사람의 아이디
db.query(`SELECT * FROM topic left join auth on count=user_id WHERE topic.title=?`,
[title], function(error, topic){
if(error) throw error;
if(count !== topic[0].user_id){
request.flash('error', 'You cannot edit it!');
return response.redirect('/');
} else{
db.query(`UPDATE topic SET title=?, description=? WHERE id=?`,
[title, description, id],
function(error, result){
if(error) throw error;
response.redirect(`/topic/${id}`);
});
}
});
});
- 조건문으로 사용자와 작성자를 비교한 후 같을 시 수정 가능하도록, 다를 시 홈으로 리다이렉트하도록 하였다.
- 여기서도 flash message가 안 뜬다 ...
그리고 강의에서는 사용자와 작성자가 다를 때 update 버튼을 눌러도 홈으로 리다이렉트하도록 했는데 어려운 작업 같아 내 코드에서는 일단 패스하였다 ... ~~ ^ㅅ^
접근제어 - 글 삭제
router.post('/delete_process', function(request,response){
if(!auth.isLogin(request, response)){
response.redirect('/');
return false;
}
var post = request.body;
var id = post.id;
var count = request.user.count; // 로그인 한 사용자
db.query(`SELECT * FROM topic left join auth on count=user_id WHERE topic.id=?`,
[id], function(error, topic){
if(error) throw error;
if(count !== topic[0].user_id){
request.flash('error', 'You cannot delete it!');
return response.redirect('/');
} else{
db.query(`DELETE FROM topic WHERE id=?`, [id], function(error, result){
if(error){
throw error;
}
response.redirect('/');
});
}
});
});
글 수정에서는 left join 쿼리에서 id가 잘 작동하지 않아 대신 title을 이용하였는데, delete에서는 title이 제대로 작동하지 않아 id로 비교해주었다. 전체적인 동작 원리는 글 수정과 비슷하다!
끗 ~
이지만 비밀번호 보안 관련된 내용이 남았당
비밀번호 저장
bcrypt라는 모듈을 이용하여 평문의 비밀번호를 복잡한 문자열로 바꾸어 저장할 수 있다. 예제로 알아보자 ~~
const bcrypt = require('bcrypt');
const saltRounds = 10; // 노이즈
const myPlaintextPassword = '111111'; // 나의 비밀번호를 이거로 가정하자
const someOtherPlaintextPassword = '222222';
bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash){
console.log(hash);
});
- 성능이 좋은 컴퓨터는 비밀번호 무작위 입력(?)을 1초에 무수히 많이 할 수 있다. saltRounds를 이용하여 그 횟수를 1초 당 n번으로 줄일 수 있다.
- hash()를 이용하여 비밀번호를 복잡한 문자열로 바꾼다.
const bcrypt = require('bcrypt');
const saltRounds = 10; // 노이즈
const myPlaintextPassword = '111111'; // 나의 비밀번호를 이거로 가정하자
const someOtherPlaintextPassword = '222222';
bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash){
console.log(hash);
bcrypt.compare(myPlaintextPassword, hash, function(err, result){
console.log('my password', result);
});
bcrypt.compare(someOtherPlaintextPassword, hash, function(err, result){
console.log('not my password', result);
});
});
- compare()을 이용하여 해쉬와 내 비밀번호가 일치하는지 확인할 수 있다.
이거를 코드에 적용하려면 ... 또 SQL을 갈아엎어야 하는데 ... 힘드니까 나중에 하기루 ... !
Federated Identity를 제외하고 생활코딩의 웹 로드맵을 다 따라온거 같다!
앞으로 해보면 좋은 것들
- 코드 리팩토링
- bcrypt
검색 기능- 페이징 기능
- 회원정보 수정 기능
- 회원탈퇴 기능