게시판 - 접근제한

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 게시판 만들기 / 게시판 - Post-User 관계(relationship) 만들기에서 이어집니다.

board.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard 4edf060
git reset --soft ad95763
npm install
atom .

board.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 board.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/board.git
cd board
git reset --hard 4edf060
git reset --soft ad95763
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/board/tree/4edf060fb5786112aba6d76bbcb6b3952fcf7e65


이번 강의에서는 로그인을 하지 않았을 경우 글 작성을 할 수 없도록 글쓰기 기능을 제한하고, 자신이 작성한 글이 아닌 경우에는 글의 수정 및 삭제를 제한하며, 마지막으로 자신이 아닌 사용자 정보의 접근을 제한해봅시다.

접근제한은 두가지 단계로 이루어 지는데, 첫번째로 front-end에서의 조건에 맞지 않는 경우 해당 기능들이 보이지 않게하여 아예 접근이 불가능하도록 하고, 다음으로 back-end에서 해당 요청이 오는 경우 사용자를 비교하여 error를 내도록 합니다.

폴더구조

코드 - js

// util.js

var util = {};

util.parseError = function(errors){
 ...
}

util.isLoggedin = function(req, res, next){
  if(req.isAuthenticated()){
    next();
  } 
  else {
    req.flash('errors', {login:'Please login first'});
    res.redirect('/login');
  }
}

util.noPermission = function(req, res){
  req.flash('errors', {login:"You don't have permission"});
  req.logout();
  res.redirect('/login');
}

module.exports = util;

모든 route에서 공용으로 사용될 isLoggedinnoPermission함수를 util.js에 만듭니다.

isLoggedin함수는 사용자가 로그인이 되었는지 아닌지를 판단하여 로그인이 되지 않은 경우 사용자를 에러 메세지("Please login first")와 함께 로그인 페이지로 보내는 함수입니다.

route에서 callback으로 사용될 함수이므로reqresnext를 받습니다. 로그인이 된 상태라면 다음 callback함수를 호출하게 되고, 로그인이 안된 상태라면 로그인 페이지로 redirect합니다.

noPermission함수는 어떠한 route에 접근권한이 없다고 판단된 경우에 호출되어 에러 메세지("You don't have permission")와 함께 로그인 페이지로 보내는 함수입니다.reqres가 있지만 callback으로 사용하지는 않고 일반 함수로 사용할 예정입니다. isLoggedin과 다르게 접근권한이 있는지 없는지를 판단하지는 않는데, 상황에 따라서 판단 방법이 다르기 때문입니다.

// routes/posts.js

...

// New
router.get('/new', util.isLoggedin, function(req, res){ // 2
 ...
});

// create
router.post('/', util.isLoggedin, function(req, res){ // 2
 // ...
});

...

// edit
router.get('/:id/edit', util.isLoggedin, checkPermission, function(req, res){ // 2, 3
 // ...
});

// update
router.put('/:id', util.isLoggedin, checkPermission, function(req, res){ // 2, 3
 // ...
});

// destroy
router.delete('/:id', util.isLoggedin, checkPermission, function(req, res){ // 2, 3
 // ...
});

module.exports = router;

// private functions // 1
function checkPermission(req, res, next){
  Post.findOne({_id:req.params.id}, function(err, post){
    if(err) return res.json(err);
    if(post.author != req.user.id) return util.noPermission(req, res);

    next();
  });
}

1. Post에서checkPermission함수는 해당 게시물에 기록된 author와 로그인된 user.id를 비교해서 같은 경우에만 계속 진행(next())하고, 만약 다르다면 util.noPermission함수를 호출하여 login 화면으로 돌려보냅니다.

2. new, create, edit, update, destroy route에 util.isLoggedin를 사용해서 로그인이 된 경우에만 해당 route을 사용할 수 있습니다.. 즉 게시물 목록(index)을 보는 것과, 게시물 본문을 보는 것(show) 외의 행동은 login이 되어야만 할 수 있습니다.

3. edit, update, destroy route에 checkPermission를 사용해서 본인이 작성한 post인 경우에만 계속 해당 route을 사용할 수 있습니다.

// routes/users.js

...

// Index - ** Index는 지워줍니다 // 1

...

// show
router.get('/:username', util.isLoggedin, checkPermission, function(req, res){ // 3
 ...
});

// edit
router.get("/:username/edit", util.isLoggedin, checkPermission, function(req, res){ // 3
 // ...
});

// update
router.put("/:username", util.isLoggedin, checkPermission, function(req, res, next){ // 3
 // ...
});

// Destroy- ** Destory는 지워줍니다 // 1

module.exports = router;

// private functions // 2
function checkPermission(req, res, next){
 User.findOne({username:req.params.username}, function(err, user){
  if(err) return res.json(err);
  if(user.id != req.user.id) return util.noPermission(req, res);

  next();
 });
}

user의 목록을 보여주는 기능(index)와 user를 삭제하는 기능(destroy)은 더이상 필요하지 않으므로 지워줍시다.

2. User에서checkPermission함수는 해당 user의 id와 로그인된 user.id를 비교해서 같은 경우에만 계속 진행(next())하고, 만약 다르다면 util.noPermission함수를 호출하여 login 화면으로 돌려보냅니다.

3. show, edit, update에util.isLoggedincheckPermission를 사용해서 로그인이 되고 자신의 데이터에 접근하는 경우에만 해당 route을 사용할 수 있습니다.

코드 - ejs

<!-- views/posts/index.ejs -->

...

      <div>
        <% if(isAuthenticated){ %> <!-- 1 -->
          <a class="btn btn-primary" href="/posts/new">New</a>
        <% } %>
      </div>

    </div>
  </body>
</html>

1. index.ejs에서 new 버튼은 로그인된 경우에만 보이게 됩니다. 참고로 isAuthenticated함수는 게시판 - Login 기능 추가강좌에서 index.js속에 만들었던 함수입니다. req.locals에 들어있어서 ejs에서 바로 사용할 수 있습니다.

<!-- views/posts/show.ejs -->

...

      <div class="mt-3">
        <a class="btn btn-primary" href="/posts">Back</a>
        <% if(isAuthenticated && post.author && currentUser.id == post.author.id){ %> <!-- 1 -->
          <a class="btn btn-primary" href="/posts/<%= post._id %>/edit">Edit</a>
          <form action="/posts/<%= post._id %>?_method=delete" method="post" class="d-inline">
            <a class="btn btn-primary" href="#" onclick="confirm('Do you want to delete this?')?this.parentElement.submit():null;">Delete</a>
          </form>
        <% } %>
      </div>

...

1. 로그인이 된 상태이고, 게시물의 작성자 id(post.author.id)와 현재 로그인된 사용자의 id(currentUser.id)가 일치하는 경우에만 edit, delete 버튼을 보여줍니다. currentUser 역시 req.locals에 들어있어서 ejs에서 바로 사용할 수 있습니다.

<!-- views/users/show.ejs -->

...

      <div>
        <% if(isAuthenticated && currentUser.id == user.id){ %> <!-- 1 -->
          <a class="btn btn-primary" href="/users/<%= user.username %>/edit">Edit</a>
        <% } %>
      </div>

    </div>
  </body>
</html>

1. 로그인이 된 상태이고, 해당 user id(user.id)와 현재 로그인된 사용자의 id(currentUser.id)가 일치하는 경우에만 edit 버튼을 보여줍니다.

실행결과

로그인 전

index view에 new 버튼이 없습니다.

show view에 edit, delete 버튼이 없습니다.

로그인 후

new 버튼이 보입니다.

자신이 작성한 게시물의 경우 edit, delete 버튼이 보입니다.

로그인 하지 않은 상태에서 주소창에 /posts/new를 입력하여 가려고 하는 경우,

에러메세지와 함께 로그인화면으로 redirect됩니다.

마치며...

이제 1. data의 CRUD 구현, 2. data 간의 관계(relationship) 형성, 3. 회원가입 및 로그인 구현, 4. 접근 제한까지 웹사이트 제작의 큰 틀은 모두 익히셨습니다!

게시판 만들기 강의는 사실 '가장 기본적인 기능을 가지고 있는 웹사이트'를 만드는 것이 목적이였습니다. 그렇기 때문에 게시판뿐만 아니라 어떠한 사이트를 만들든지 회원가입/로그인 기능이 있다면 위 코드를 그대로 가져간 다음, 자신의 목적에 맞게 코드를 추가/변경하여 쓰셔도 됩니다. (어떠한 상업적/비상업적 프로젝트라도 일일이 제 허락을 구하실 필요없이 이 코드를 가져다 쓰셔도 괜찮습니다. 다만, 만약 제 코드에 어떠한 오류가 있어서 문제가 발생하더라도 저는 책임은 지지 않습니다.)

하지만 기초강의에 초점을 맞추고 웹사이트 제작의 전반에 대해 설명하다 보니 사이트의 범용적인 기능들은 가지고 있으나, 게시판으로는 많이 부족한 것이 사실입니다. 이어지는 게시판(고급)강의에서는 좀 더 게시판을 게시판답게 만들어 보겠습니다.


** 현재 항목(Node JS 첫걸음/게시판 만들기)의 마지막 글입니다.

댓글

-
-19 2020.01.11
이때까지 찾아본 node.js 강좌보다 자세하고 실용적으로 설명이 되어있어 어제부터 정독해가면서 배워나가고 있습니다. 덕분에 실무에도 바로 적용할수 있네요. 감사합니다. 앞으로도 많은 글 올려주셨으면 하네요 ㅎㅎ
I
Ian H 2020.01.13
@-19,
격려의 말씀 감사합니다! 현재 게시판 후속 강의를 준비중입니다.
h
hongki jung 2020.02.23
너무 좋네요!!  nodejs - mysql  관련 글도 많이 올려주시면 감사하겠습니다 .ㅠㅠ
I
Ian H 2020.02.24
@hongki jung,
mysql은 제가 사용해본적이 없어서 강의를 만들수 없어요. 죄송합니다 ㅠㅠ
K
Kairo 2020.02.27
Ian H님 passport를 이용하여 네이버 로그인을 구현 하였는데 Ian H님이 작성하신 코드에  local passport가 아니라 naver 아이디로 로그인 한 사람이 글을 작성하고 답글을 작성하려면 어느 부분에 함수를 만들어야 하는건가요 ??
K
Kairo 2020.02.28
passport.deserializeUser에서 기존의  User.findOne({_id:id}과 그 네이버로 로그인한 것을 작성하고, util.isLoggedin,util.noPermission 이분을 고치면 되는게 맞나요?? _id:id 대신 어떤 값으로 작성해야 하는지 잘 모르겠습니다! 도와주시면 감사하겠습니다!
I
Ian H 2020.02.28
@Kairo,
네이버로 로그인 하더라도 user db 데이터는 생성해야 합니다. 최초 로그인시에는 user 모델에 naverId를 추가하시고 naver에서 받아온 userId를 저장합니다. 이 후 로그인시에는 네이버에서 받아온 id로 내 naverId를 검색하여 user를 찾습니다.
즉 1. naver로그인 2. 내 db에 naverId가 일치하는 user가 있는지 확인 2-a. 일치하는 user가 없으면 user 생성 2-b. 일치하는 user가 있으면 해당 user 사용 이후 결국 내 db에서 user가 호출되므로 util.isLoggedIn이나 util.noPermission은 바꿀 필요가 없어요
글로 설명하려니 어렵네요. 이해가 되시는지..ㅠㅠ 이해가 잘 안되시면 또 질문주세요
K
Kairo 2020.03.04
@Ian H,
Ian H님 감사합니다. 어떻게 해야하는지 아예 몰랐는데 Ian님이 설명해주셔서 구현했습니다!! 감사합니다!!!
I
Ian H 2020.03.04
@Kairo,
도움이 되었다니 기쁘네요^^
박우림 2020.03.27
감사합니다~ㅎㅎ
박우림 2020.03.27
선생님!! 확인해보니 User의 Route 부분에서 추가로 수정해야되는 부분이 있어서 말씀드립니다!
// routes/users.js ... // create router.post('/', function(req, res){   User.create(req.body, function(err, user){     if(err){       req.flash('user', req.body);       req.flash('errors', util.parseError(err)); // 1       return res.redirect('/users/new');     }     res.redirect('/users');   }); }); ...
create부분의 하단에 res.redirect('/users')를 수정해주어야겠더라구요. Index 부분을 지워주니까 해당 경로로 리다이렉트를 할 수가 없는 문제가 생기네요. 저 같은 경우는 로그인 화면으로 리다이렉트 시켜줬습니다!
res.redirect('/users') => res.redirect('/login') 으로 수정했더니 정상 작동하네요. 감사합니다 선생님.
I
Ian H 2020.03.27
@박우림,
제보 감사합니다^^ 이렇게 피드백 주셔서 다른 분들이 더 좋은 코드를 볼 수 있게 되는거죠. 감사합니다!
박명범 2020.11.30
안녕하세요!! 드디어 게시판만들기 마지막 강좌까지 따라왔네요 ㅎㅎ 질문이 하나 있는데, 지금까지 실행 환경을 localhost에서 진행했잖아요? 저는 AWS의 EC2 서버에 업로드해서 온라인 상에서 접속하고 싶어서 진행했는데 Board나 SignUp, Login 등 데이터베이스를 사용하는 파트에서는 로딩이 안되고 콘솔에 MongooseError를 발생시키더라구요. 로컬 환경에서는 제대로 동작 했습니다!
제 생각에는 몽고디비에서 접근 권한을 설정해줘야 하는 건가 싶은데 어떻게 해야할까요? 도움 부탁드립니다!!
아래는 에러 메시지입니다.
(node:1056) UnhandledPromiseRejectionWarning: MongooseError: The `uri` parameter to `openUri()` must be a string, got "undefined". Make sure the first parameter to `mongoose.connect()` or `mongoose.createConnection()` is a string.
덧붙여 훌륭한 강의 제공해주셔서 감사합니다
박명범 2020.11.30
그런데 로컬의 환경 설정에 몽고디비의 값을 저장한 것 온라인 상에서 가져와서 참조할 수 있나요? 이 부분에 의한 오류도 의심되네요
I
Ian H 2020.11.30
@박명범,
안녕하세요, 환경변수는 해당 컴퓨터안에서만 사용할 수 있습니다. 에러 메세지를 봐도 
- The `uri` parameter to `openUri()` must be a string, got "undefined" -  uri인자는 string이여야 하는데 "undefined"를 받았다 - Make sure the first parameter to `mongoose.connect()` or `mongoose.createConnection()` is a string - mongoose.connect()나 mongoose.createConnection() 함수의 첫번째 인자가 string인지 확인할 것 
이라고 하고 있으며, DB의 connection string을 읽지 못하여 에러가 나고 있습니다. 환경변수는 코드가 올라간 컴퓨터/시스템에 설정을 해주어야 합니다. 
AWS와 같은 웹호스팅 서비스를 이용하는 경우, 해당 서비스내에 환경변수를 설정하는 방법이 존재합니다.
AWS는 제가 사용해 본 적이 없어서 정확한 방법을 제시해 드릴 순 없지만 구글에 how to set an environment variable in aws ec2 등으로 검색하면 방법을 찾을 수 있습니다.
https://www.a-mean-blog.com/ko/blog/단편강좌/_/Node-js-사이트-Heroku-헤로쿠-로-인터넷에-올리기 도 한번 참고해 보세요.
펭굴 2021.01.14
@Ian H,
node js로 게시판 만들기 글로 통해 공부했습니다만 최근 Amazon EC2 서버에 업로드해서  사용하려고합니다만 mongoose가 오류라서 실행이 안됩니다  위에 분과 같은 질문이라 죄송합니다만 환경변수를 설정하는 방법에 대해 간단히 설명 부탁해도 될까요?
좋은 강의로 게시판을 만들 수 있게 되어 감사합니다
I
Ian H 2021.01.14
@펭굴,
안녕하세요, 반갑습니다^^ 
환경변수를 설정하는 방법은 서비스 제공자(여기서는 아마존)에 따라 천차만별이기 때문에 해당 서비스내에서 어덯게 환경변수를 설정하는지를 알아보셔야 합니다. 구글에 검색할 수도 있고, 서비스 제공자에게 직접 문의하거나 공식 문서를 참고할 수도 있겠죠.
저는 Amazon EC2 계정이 없어서 Amazon EC2에서 어떻게 환경변수를 설정하는지를 실험해볼 수가 없어요. 구글링을 해보니 https://stackoverflow.com/questions/28643573/how-to-set-an-environment-variable-in-amazon-ec2 이런 글이 있는데 한번 테스트해보세요^^
펭굴 2021.01.15
@Ian H,
친절하게 설명 해주셔서 고맙습니다 ㅎㅎ
최언서 2021.01.18
안녕하십니까! 이 블로그를 통해서 nodejs에 대해서 많이 공부할 수 있어 감사인사드립니다. 한가지 질문을 하고 싶은 것이 있어 글 남깁니다! 게시판 만드는 소스코드를 이용하여 웹 사이트를 제작중에 있습니다.  후에 완성을 하고 홈페이지를 사용할 시 페이지 소스 코드 보는 기능으로 여러가지 취약점(개인 신상정보 유출)이 발생할 수 있다고 생각합니다 ㅠㅠ 죄송하지만 웹 페이지 소스를 비공개하거나 가리는 등 보안적인 요소를 추가할 수 있는 방법이 있을까해서 댓글 남깁니다. 긴 글 읽어주셔서 감사합니다. 
I
Ian H 2021.01.18
@최언서,
반갑습니다^^   브라우저에 있는 소스 보기를 말씀하는 거죠? 일단 그 기능을 막을 방법은 없습니다. 웹페이지의 작동 원리가 소스 코드를 클라이언트에 다운받아서 실행하는 것이기 때문에 어떻게든 그 정보를 찾을 수 있습니다.
제 코드는 db상의 id, 사용자 id, 사용자 이름 정도를 보여주는데, 어차피 사용자 이름이나 id는 페이지에서 보여지는 정보이고, db상의 id는 딱히 알아도 어디에 써먹을 곳이 없기 때문에 문제가 될 곳은 없어 보입니다.
현재 만들고 계신 사이트의 소스모기에서  무엇이 노출되는지, 그걸 왜 노출하시는지를 구체적으로 알려주실 수 있나요?
최언서 2021.01.19
@Ian H,
현재 인턴으로 근무중이며 소프트웨어 사내 홈페이지를 제작중에 있습니다!  그래서 혹시나 하는 마음에 질문을 드리게되었습니다!  저도 인터넷 검색 결과 소스는 클라이언트에서 작동하기에 막을 방법은 없다고 들었습니다! 이 문제에 대해서는 제가 조금 더 생각해보도록 하겠습니다. 좋은 강의 올려주셔서 너무 감사드립니다!
I
Ian H 2021.01.19
@최언서,
페이지를 구성하는데 불필요한 정보라면 router에서 ejs로 보낼 때 걸러내고 보낼 수도 있습니다. 페이지에 보여져야 하는 정보라면 당연히 코드에서 보여져도 문제가 없을 것이구요^^
최언서 2021.01.19
@Ian H,
아 네! 빠른 답변 감사드립니다 !!! ㅎㅎ 
M
Michael Jackson 2021.03.05
util.parseError = function(errors){  ... }
util.isLoggedin = function(req, res, next){   if(req.isAuthenticated()){     next();   }    else {     req.flash('errors', {login:'Please login first'});     res.redirect('/login');   } }
util.noPermission = function(req, res){   req.flash('errors', {login:"You don't have permission"});   req.logout();   res.redirect('/login'); }
이런식으로하면\ util.xxx식으로 배열같이 함수가 등록되는건가요
I
Ian H 2021.03.05
@Michael Jackson,
util.xxx 항목에 계속해서 함수들이 추가되는 건 맞지만, 이걸 '배열같다'라고 할 순 없죠. 
배열이려면 var util = [function parseError(...){...} , function isLoggedin(...){...}, function noPermission(...){...} ]; 와 같이 저장되는 거구요,
그냥 javascript object입니다. var util ={   parseError: function(...){...},   isLoggedin: function(...){...},   noPermission: function(...){...} };
javascript에서는 object 생성 후에도 항목을 계속 추가할 수 있거든요. 그래서 var util = {};로 빈 object를 생성 한 후에
util.parseError = function(...){...}; 와 같은 방법으로 계속 함수를 항목에 넣어주는 것입니다.
참고로 자바스크립트에서는 함수를 변수에 담을 수도 있어요.
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP