Express에서 회원 가입 만들어 보기
2018-06-11 20:40:31

클라이언트 HTML, CSS는 FLORIN POP - DOUBLE SLIDER - SIGN IN/UP FORM 의 글을 보고 따라 했다.

클라이언트

실제 서비스는 여러 개의 데이터를 기입해야 하지만 지금은 가볍게 하기 위해 이메일, 비밀번호, 이름만 적게끔 했다.

간단하게 빈 값에 대한 validation을 한 후 ajax로 파리미터들을 전송하게 햇다.

서버에선 어떠케 처리를 하는지 코드를 적어 두자.

1
2
3
4
5
6
// TODO: 가입 페이지 랜더링.
router.get('/', security.csrfProtection(), (req, res) => {
res.render('index', {
csrfToken: req.csrfToken(),
});
});

security.csrfProtection(), {csrfToken: req.csrfToken()} 이 부분에 대해서 적을려고 한건데

저 코드가 사용된 이유는 csrf 라는 웹사이트 취약점 공격 중의 하나 인데(자세한 내용은 링크 참조) 대비책으로 사용하는 기법으로 자주 보는 npm 사이트에도 적용이 되어 잇다.

npm csrf

직접 만든 건 아니고 csurf라는 좋은 라이브러리가 잇어서 사용을 햇다.

이 기법이 쿠키 기반이라 cookie-parser가 필요함

이건 다음 글의 주제인 로그인에 대해 쓸 때 세션 때도 쓰므로 그 때 적어두자. 귀찮다..

위의 저 security.csrfProtection()의 코드를 보자.

1
2
// TODO: csrf 검증.
module.exports.csrfProtection = () => csrf({ cookie: true });

한 줄이면 끝. 이렇게 모듈로 만들어두고 쓰면 편리하더라. 나 같은 경우엔 html meta에 넣어두엇음.

예시

새로 고침을 해 보면 이 토큰은 1회성 토큰이기 때문에 계속 바뀐다.

그리고 클라이언트에서 form이든 ajax든 서버에 데이터를 보낼 때 아래 처럼 저 토큰 값을 꼭 보내줘야 된다.

1
2
3
4
5
6
7
const csrf = $('meta[name="csrf"]').attr('content');

$.ajax({
url: `/signUp?_csrf=${csrf}`,
type: 'post',
data: $('#signUpForm').serialize(),
...

이제 토큰 값과 함께 데이터를 보내면 서버에서 처리를 해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// TODO: 가입 처리.
// eslint-disable-next-line consistent-return
router.post('/signUp', security.csrfProtection(), (req, res, next) => {
const { email, password, name } = req.body;
if (!email || !password || !name) {
return res.status(200).json({ result: false, title: 'warning', comment: 'email or password or name is omitted' });
}
UserModel.findOne({
email: security.xssFilter(email),
// eslint-disable-next-line consistent-return
}, (err, data) => {
if (err) { return next(err); }
if (data) { return res.json({ result: false, title: 'warning', comment: 'This email has already been signed up.' }); }
const user = new UserModel({
email: security.xssFilter(email),
password: security.changeHash(security.xssFilter(password)),
name,
});
user.save((saveErr) => {
if (saveErr) { return next(saveErr); }
return res.status(200).json({ result: true, title: 'Your membership is complete', comment: 'Please sign in.' });
});
});
});

처리를 하는 라우터에 마찬가지로 security.csrfProtection()를 미들웨어로 넣으면

이 친구가 알아서 검증을 하기 때문에 정상 토큰 이면 다음으로 진행을 하고

비정상적인 토큰 값이거나 비어잇으면 403으로 리턴을 햇던 걸로..

사실 이건 회원 가입 뿐만 아니라 웹 사이트 전체 내에 사용자가 뭔가 입력을 하는 페이지의 같은 경우 다 넣어줘야 한다.

csrfProtection 코드를 모듈로 만든 이유가 이렇기 때문이다. 패시브처럼 써야 함.

나머지 가입 구현은 사실 위의 코드가 끝이다. 저 코드는 DB를 mongoDB를 쓰고 잇는데 클라가 보내준 email을 유저 컬렉션에서 조회를 한 후 중복된 이메일인지 체크를 해서 있으면 거절하고 업는 이메일이면 신규 유저로 인식을 하고 저장하는 게 끝이다.

저 코드에서 남겨둘려고 하는 건 xssFilter, changeHash 이 부분이다.

xss는 이거도 웹사이트 취약점 공격 중의 하나 인데(역시 이거도 자세한 내용은 링크 참조) 쉽게 적자면 사용자가 적는 form input에 스크립트를 넣는다는 소리다.

1
<input name="aa" value="<script>alert('이게뭐여'</script>"

뭐 이런식으로 해서 넘어올 때 스크립트가 만약에 잇다면 <> 이 부분만 entity 코드로 치환 해버리면 문자로 인식이 되기에 스크립트가 실행이 되질 않을 것이다.

1
2
// TODO: 간단한 xss 필터.
module.exports.xssFilter = (_val) => _val.replace(/</g, '&lt;').replace(/>/g, '&gt;');

마지막으로 changeHash는 안전한 비밀번호 저장이란 글을 참고하면 좋다.

즉 비밀번호를 사용자가 입력한 일반 텍스트로 저장을 하면 안된다는 소리. 해킹은 고사하고 db 접근 권한이 잇는 사람이 들어가서 보면 그 사람이 평소에 쓰는 비밀번호나 이런 게 노출되버리기 때문이다. 악의적인 프로그래머라면 ㅎㅎ 말 안해도 끔찍하다.

아 여담으로 올해에 비밀번호를 평문으로 저장을 하는 코드와 DB를 난 본 적이 있다. 몹시 충격이였음…

1
2
3
4
5
6
const crypto = require('crypto');

const MY_SALT = 'Always back up Hexo articles. :(';

// TODO: 단방향 암호화.
module.exports.changeHash = (_val) => crypto.createHash('sha512').update(_val + MY_SALT).digest('base64');

암튼 changeHash는 nodejs의 내장된 패키지인 crypto를 이용해 단방향 암호화 (=복호화가 안되는 암호화)로 변환을 하는 코드이다.

이걸 어떠케 사용하는거냐면

  1. 가입할 때 사용자가 비밀번호를 1q2w3e 뭐 이렇게 입력햇다고 치자.
  2. 서버에서 사용자가 보내준 비밀번호를 변환 -> 예로 7eb32f7589 뭐 이런 식으로 변환 된다.
  3. 가입을 할 때 저 변환된 값을 DB에 저장을 한다.
  4. 이제 사용자가 로그인 할 때 1q2w3e를 입력을 하면 서버에선 또 변환하여 DB에 저장된 변환된 비밀번호와 동일한지 체크를 하는 거다.

마지막으로 가입하는 짤 투척

예시

가입은 이정도로 적어두고 다음 글에서 로그인, 소셜 로그인을 이어서 작성을 해야 겟다.

코드를 일부분만 적어 둿으므로 만약 만약에 누가 보게 될 경우엔 소스 코드를 참조 바랍니다.