* 생활코딩 강의(Opentutorials) - WEB5 Express passport 를 듣고 공부한 내용입니다.
https://opentutorials.org/module/3655
로그인 기능은 웹사이트를 구현할 때 꼭 필요한 기능 중 하나입니다. 안 쓸 수도 있지만요. 로그인/로그아웃 기능은 다양하게 구현할 수 있습니다. 대표적으로 쿠키를 이용할 수도 있고, session을 이용할 수도 있습니다.
하 지 만! 쿠키를 이용한 로그인과 로그아웃은 절대 구현해서는 안됩니다! 서버에 있는 쿠키는 다른 사람들도 확인이 가능하기 때문에 보안에 매우 취약하거든요. 우리 아이디와 비밀번호를 통채로 다른 사람들에게 공개해버린단 말입니다. 쿠키를 이해하는데 좋은 방법 중 하나가 쿠키를 이용한 로그인/로그아웃이니 공부용으로만 구현해보고 실제로는 절대절대 사용해선 안됩니다.
그보다 조금 더 안전한 방법으로 session(서버에는 세션 저장소가 있습니다.)에 사용자 정보 등을 저장(key와 value로 나눠서 저장)한 다음 쿠키에는 key만 담는 방법도 있습니다. 물론 session도 완전히 보안에서 자유롭다고는 할 수 없습니다.,, 만들어진 세션 key와 value를 관리하지 않으면 역시 유출되기 때문입니다. 관리를 해도 누군가 세션에 담긴 정보를 탈취할 수 있다고 합니다.
쿠키와 세션에 관한 간단한 보충 내용은 더보기를 누르면 나옵니다.
쿠키와 세션은 웹 통신을 하면서 특정 정보를 저장하기 위해 사용한다는 점에서 공통적입니다. 차이점으로 쿠키는 정보를 클라이언트의 PC에 text로 저장하며, 세션은 서버에 object로 저장합니다.
쿠키는 보안에는 취약하지만, 자원관리 차원에서 세션보다는 유리합니다. 세션은 서버자원을 사용하기 때문에 서버의 속도가 느려질 수 있습니다.
세션에는 단위가 있는데, 방문자가 웹 서버에 접속해 있는 상태를 하나의 단위로 본다고 합니다.
로그인과 로그아웃 기능은 이처럼 보안과 떼놓을 수가 없기 때문에 꼭!! 보안에 유념하며 만들어야 합니다. 그리고 이를 유념하며 코드를 만드는 건 아주 어렵죠... 이건 제가 초짜라서 어려운 게 아니라 그냥 어렵다고 했습니다. 강의에서도 그렇고 암튼 어려운거랬움!! 반박시 눈물,,,, 또르르
똑똑한 사람들은... 로그인과 로그아웃 기능을 구현하기 위해 타사 인증을 사용해 로그인과 로그아웃을 만들기도 합니다.
고도의 관리가 필요한 회원정보관리를 구글, 페이스북 등에 맡기고 자사는 회원의 식별자만을 유지하므로써 보안사고를 방지할 수 있고, 회원가입의 간편함도 높다는 장점이 있기 때문입니다.
이를 가능하게 해주는 도구가 oauth, OpenID 같은 소스(표준화된 방법)로 node.js에 존재합니다. 이 도구들은 사용자의 비밀번호를 제3의 서비스에 노출하지 않고도 인증기능을 구현할 수 있도록 해줍니다. 제3의 서비스에게 모든 권한을 주지 않고, 최소한의 권한만 주는 방식으로 작동하기에 최악의 보안사고를 방지할 수 있습니다.
하 지 만 이런 도구를 사용하는 방법도 어렵다고...강의에서 그랬습니다... 제 피셜이 아닙니다...... 어렵다고 합니다. 그리고 타사인증을 해야하기도 하죠. 자체적으로 로그인과 로그아웃을 위한 인증 기능은 node.js에 없을까요? 있습니다~😇
node.js 에는 인증 라이브러리인 passport를 제공합니다! 타사인증을 사용하지 않더라도 전략(strategy)을 사용해서, 복잡한 인증을 손쉽게 구현할 수 있게 됩니다!! passport는 미들웨어로 로그인/로그아웃을 가능하게 하고 로그인/로그아웃에 필요한 인증기능까지 할 수 있습니다. 패키지라고 하기도 하네요... 미들웨어나 인증 라이브러리, 패키지가 뭔가 다른걸까요? 다르다면 어떻게 다른걸까요? 😩 이 부분은 공부하면서 더 찾아봐야 할 것 같습니다. 어쨌거나 passport에서는 공식적으로 middleware라고 하긴 하는 것 같습니다.
헌데 강의에서는 패스포트가 배우긴 어려운데 사용하긴 쉽다고 합니다. 생활코딩의 egoing님이 계속 말씀하시죠! 우리의 목적은! 이해하는 것 보다 우선 익숙해지는 것! 직접 구현해보며 passport에 적응해 봅시다!
passport 미들웨어의 사용방법은 passport 공식 홈페이지에 잘 나와있...지만! 우리는 강의를 통해 좀 더 똑똑해집시다!
공식사이트에서는 다양한 전략(strategy)을 제공하고 있습니다. 이 전략은 어떻게 node.js에 로그인/로그아웃과 인증을 구현하면 되는지 미리 유형으로 만들어 우리가 잘 사용할 수 있도록 해준 것입니다.
우리는 이 전략들 중 Local 전략을 사용합니다. passport-local 전략은 웹 앱을 구현하는 작은 팀과 스타트업에 좋다고 합니다. local 전략은 사용자 이름과 암호를 사용해서 사용자를 인증하는 전략입니다. 전략에는 이러한 자격 증명과 사용자를 제공하는 verify 호출을 수락하는 콜백이 필요합니다.(출처 : 공식 홈페이지 https://www.passportjs.org/packages/passport-local/)
강의를 따라 차근차근 직접 만들어보았습니다!
이 기본 틀은 강의에서 제공하는 깃허브에서 다운받았습니다. 깃허브 주소가 있는 강의 링크 남겨드리겠습니다.
출처 : https://opentutorials.org/module/3655/21864
npm install 등으로 터미널을 통해 다운로드 합시다. dependency 부분에 있는 걸 모두 설치하시면 됩니다.
3-1. 템플릿(lib/template.js)
템플릿은 view 부분을 구현하는 부분입니다.
module.exports = {
HTML:function(title, list, body, control, authStatusUI = '<a href="/auth/login">login</a>'){
return `
<!doctype html>
<html>
<head>
<title>WEB1 - ${title}</title>
<meta charset="utf-8">
</head>
<body>
${authStatusUI}
<h1><a href="/">WEB</a></h1>
${list}
${control}
${body}
</body>
</html>
`;
}, list:function(filelist){
var list = '<ul>';
var i = 0;
while(i < filelist.length){
list = list + `<li><a href="/topic/${filelist[i]}">${filelist[i]}</a></li>`;
i = i + 1;
}
list = list+'</ul>';
return list;
}
};
3-2. auth (lib/auth.js)
똑같은 이름의 파일이 또 하나 더 있습니다. lib에 있는 auth 입니다!
module.exports = {
isOwner:function(request, response) {
// 로그인 되어 있으면 유저 객체가 있고 아니면 없다.
if (request.user) {
return true;
} else {
return false;
}
},
statusUI:function(request, response) {
var authStatusUI = '<a href="/auth/login">login</a>'
if (this.isOwner(request, response)) {
authStatusUI = `${request.user.nickname} | <a href="/auth/logout">logout</a>`;
}
return authStatusUI;
}
}
4-1. auth(routes/auth.js)
웹페이지 중에서 auth 루트로 들어왔을 때, 로그인 된 상태와 로그아웃 된 상태를 나눠서 다른 웹 페이지를 보여줍니다.
var express = require('express');
var router = express.Router();
var path = require('path');
var fs = require('fs');
var sanitizeHtml = require('sanitize-html');
var template = require('../lib/template.js');
router.get('/login', function (request, response) {
var title = 'WEB - login';
var list = template.list(request.list);
var html = template.HTML(title, list, `
<form action="/auth/login_process" method="post">
<p><input type="text" name="email" placeholder="email"></p>
<p><input type="password" name="pwd" placeholder="password"></p>
<p>
<input type="submit" value="login">
</p>
</form>
`, '');
response.send(html);
});
router.get('/logout', function (request, response) {
request.logout();
// 이렇게 하면 계속 로그인 로그아웃을 반복했을 때 문제가 생긴다.
// request.session.destroy(function(err) {
// response.redirect('/');
// });
// save는 현재 session의 상태를 session store에 저장한다.
// 저장한 후에 redirect를 시킨다.
request.session.save(function() {
response.redirect('/');
})
});
module.exports = router;
4-2. index(routes/index.js)
인덱스는 제일 첫 화면 부분입니다. 홈화면이라고 할 수 있습니다. localhost:3000으로 들어갔을 때 제일 처음 볼 수 있는 화면입니다.
var express = require('express');
var router = express.Router();
var template = require('../lib/template.js');
var auth = require('../lib/auth');
router.get('/', function (request, response) {
var title = 'Welcome';
var description = 'Hello, Node.js';
var list = template.list(request.list);
var html = template.HTML(title, list,
`
<h2>${title}</h2>${description}
<img src="/images/white.jpg" style="width:300px; display:block; margin-top:10px;">
`,
`<a href="/topic/create">create</a>`,
auth.statusUI(request, response)
);
response.send(html);
});
module.exports = router;
4-3. topic(routes/topic.js)
파일 이름이 topic인 이유는 이 파일이 이전 강의와 연관된 부분이기에 그렇습니다. 웹 페이지에서 creat, delete, update 중 어떤 기능을 수행하는지에 대한 부분입니다.
var express = require('express'); // main.js 1
var router = express.Router(); // main.js 2
var path = require('path');
var fs = require('fs');
var sanitizeHtml = require('sanitize-html');
var template = require('../lib/template');
var auth = require('../lib/auth');
// 새로운 페이지 생성하기
router.get('/create', function(request, response) {
if(!auth.isOwner(request, response)) {
response.redirect('/');
return false;
};
var title = 'WEB - create';
var list = template.list(request.list);
var html = template.HTML(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.isOwner(request, response)) {
response.redirect('/');
return false;
};
var post = request.body;
var title = post.title;
var description = post.description;
fs.writeFile(`data/${title}`, description, 'utf8', function(err){
response.redirect(`/topic/${title}`);
});
});
// 페이지 수정하기
router.get('/update/:pageId', function(request, response){
var filteredId = path.parse(request.params.pageId).base;
fs.readFile(`data/${filteredId}`, 'utf8', function(err, description){
var title = request.params.pageId;
var list = template.list(request.list);
var html = template.HTML(title, list,
`
<form action="/topic/update_process" method="post">
<input type="hidden" name="id" value="${title}">
<p><input type="text" name="title" placeholder="title" value="${title}"></p>
<p>
<textarea name="description" placeholder="description">${description}</textarea>
</p>
<p>
<input type="submit">
</p>
</form>
`,
`<a href="/topic/create">create</a> <a href="/topic/update/${title}">update</a>`,
auth.statusUI(request, response)
);
response.send(html);
});
});
// 수정의 기능부
router.post('/update_process', function(request, response){
var post = request.body;
var id = post.id;
var title = post.title;
var description = post.description;
fs.rename(`data/${id}`, `data/${title}`, function(error){
fs.writeFile(`data/${title}`, description, 'utf8', function(err){
response.redirect(`/topic/${title}`);
})
});
});
// 페이지 삭제
router.post('/delete_process', function(request, response){
var post = request.body;
var id = post.id;
var filteredId = path.parse(id).base;
fs.unlink(`data/${filteredId}`, function(error){
response.redirect('/');
});
});
// 주소창으로 넘어온 파라미터를 이용해 상세보기
router.get('/:pageId', function(request, response, next) {
var filteredId = path.parse(request.params.pageId).base;
fs.readFile(`data/${filteredId}`, 'utf8', function(err, description){
if(err){
next(err);
} else {
var title = request.params.pageId;
var sanitizedTitle = sanitizeHtml(title);
var sanitizedDescription = sanitizeHtml(description, {
allowedTags:['h1']
});
var list = template.list(request.list);
var html = template.HTML(sanitizedTitle, list,
`<h2>${sanitizedTitle}</h2>${sanitizedDescription}`,
` <a href="/topic/create">create</a>
<a href="/topic/update/${sanitizedTitle}">update</a>
<form action="/topic/delete_process" method="post">
<input type="hidden" name="id" value="${sanitizedTitle}">
<input type="submit" value="delete">
</form>`,
auth.statusUI(request, response)
);
response.send(html);
}
});
});
module.exports = router
이 파일은 프로젝트 폴더 바로 밑에 있는 파일입니다.
원래 유저 정보는 DB에 있거나 해야하지만, 공부를 위해 만드는 프로젝트이므로 임의의 유저 데이터를 만들어줬습니다.
authData 입니다. 원하는 다른 이메일 주소나 비밀번호로 만들어주셔도 됩니다.
/**
안쓰는 선언은 꼭꼭 지워주자
*/
var express = require('express') // topic.js 1
var app = express() // topic.js 2
var fs = require('fs');
var qs = require('querystring');
var compression = require('compression');
var bodyParser = require('body-parser');
var template = require('./lib/template.js');
var topicRouter = require('./routes/topic');
var indexRouter = require('./routes/index');
var authRouter = require('./routes/auth');
var helmet = require('helmet');
var session = require('express-session');
var FileStore = require('session-file-store')(session);
/** 정적인 파일의 서비스
정적인 파일을 서비스 하고 싶은 디렉토리를 아래와 같이 사용한다.
그러면 url로 파일에 접근할 수 있다.
*/
app.use(express.static('public'));
/** 바디파서라는 미들웨어를 불러온다.
사용자의 코드를 분석해서, 모든 데이터를 가지고 온 다음에
콜백에 있는 첫번째 인자인 req에 body를 만들어준다.
그걸 통해서 코드를 더 좋게 만들 수 있다.
*/
app.use(bodyParser.urlencoded({ extended: false }));
/** Compression이라는 미들웨어를 사용한다.
파일을 압축해서 전송해주는 미들웨어이다.
헤더를 살펴보면 content-Encoding: gzip을 확인할 수 있는데,
압축된 데이터를 전송하고 있다는 것이다.
*/
app.use(compression());
/** 공통되는 부분을 미들웨어로 직접 만듦
미들웨어는 함수이며, 첫번째 인자로 req, 두번째 인자로 res, 세번째 인자로 next를 받는다.
원래는 app.use였는데, 이러면 모두 이 미들웨어를 사용한다.
우리가 필요한 목록 읽어오기는 get에만 있으면 되기 때문에,
낭비를 막기 위해서 app.get('*')로
get의 모든 요청에만 사용하도록 바꿔줘서 낭비를 줄인다.
*/
app.use(session({
// httpOnly: true,
// secure: true,
secret: 'asadlfkj!@#!@#dfgasdg',
resave: false,
saveUninitialized: true,
store:new FileStore()
}));
var authData = {
email: 'qwer@qwer.com',
password: 'qwer',
nickname: 'egoing'
}
// 여기에 패스포트 관련 몰아놓고 나중에 쪼개기
// 패스포트는 세션을 사용하기 때문에 세션을 사용하는 코드 다음에 써야된다!
var passport = require('passport'),
LocalStrategy = require('passport-local').Strategy;
app.use(passport.initialize());
app.use(passport.session());
/** 로그인이 성공했을 때 딱 한 번 호출한다. 로그인이 성공했다는 정보를 세션에 저장한다.
* 이 콜백 함수는 첫 번째 인자(user)로 우리가 방금 done으로 주입한 데이터를 받도록 약속되어있다.
* 우리는 데이터에서 사용자의 식별자를 추출(user.email)하면 이 값은 done을 통해 session data에 저장된다.
* 그래서 passport에 있는 user의 값으로 간다.
*/
passport.serializeUser(function(user, done) {
console.log('serializeUser', user);
done(null, user.email);
});
/** 로그인에 성공한 후 페이지를 방문할 때마다 그 사람이 로그인한 사람인지 아닌지를 패스포트는 deserializeUser 호출해서 체크한다.
* 그래서 리로드를 할때마다 deserializeUser가 호출된다.
*/
passport.deserializeUser(function(id, done) {
console.log('deserializeUser', id);
done(null, authData);
});
/**
* 이 부분은 폼에서 보내는 인증정보의 이름을 우리가 사용할 이름으로 바꿔주는 부분이다.
* 우리는 폼에서 email과 pwd로 보내기 때문에 그렇게 이름을 지정해준다.
*/
passport.use(new LocalStrategy(
{
usernameField: 'email',
passwordField: 'pwd'
},
function(username, password, done) {
// 얘로 위에서 이름을 지정한 인증정보가 잘 왔는지 살펴봤다. 잘 온다!
console.log('LocalStrategy', username, password);
if(username === authData.email){
console.log(1);
if(password === authData.password){
console.log(2);
// done에 두 번째 인자로 false가 아닌 사용자의 실제 데이터를 주입하면 된다.
// 이 리턴이 호출되면 패스포트가 시리얼라이즈유저의 콜백 함수를 호출하도록 한다.
return done(null, authData);
} else {
console.log(3);
return done(null, false, {
message: 'Incorrect password.'
});
}
} else {
console.log(4);
return done(null, false, {
message: 'Incorrect username.'
});
}
}
));
/** 폼에서 받은 인증정보(아이디와 패스워드)를 /login으로 보냈을 때,
* 패스포트에서 제공하는 API로 함수가 되어서 콜백함수가 된다.
* 패스포트 전략으로 처리한다는 말이다.
* 별도의 login 프로세스를 구현하지 않아도 패스포트에서 잘 알아서 해준다...
*/
app.post('/auth/login_process',
// local은 유저네임과 패스워드를 이용해서 로그인하는 방식이다.
passport.authenticate('local', {
successRedirect: '/', // 성공했을때는 홈으로
failureRedirect: '/auth/login' // 아닐때는 다시 로그인으로 보낸다.
}));
app.get('*', function(request, response, next) {
fs.readdir('./data', function(error, filelist) {
request.list = filelist;
next();
});
});
/** 라우터로 파일 분리하기
/topic으로 된 주소에 topicRouter라는 이름의 미들웨어를 적용한다는 말이다.
*/
app.use('/', indexRouter);
app.use('/topic', topicRouter);
app.use('/auth', authRouter);
app.get('/', function(request, response) {
var title = 'Welcome';
var description = 'Hello, Node.js';
var list = template.list(request.list);
var html = template.HTML(title, list,
`
<h2>${title}</h2>${description}
<img src="/images/white.jpg" style="width:300px; display:block; margin-top:10px;">
`,
`
<a href="/topic/create">create</a>
`
);
response.send(html);
});
app.use(function(request, response, next) {
response.status(404).send('페이지를 찾지 못했습니다.')
});
app.use(function(err, request, response, next) {
console.error(err.stack);
response.status(500).send('뭔가가 잘못됐따!')
});
app.listen(3000, function() {
console.log('Example app listening on port 3000!')
});
이제 main.js를 실행하면! 아마 이미지 파일이 없으므로 이미지 파일이 깨진 웹페이지가 나타납니다!
이미지 파일을 넣고 싶으시면 public폴더 아래에 image란 폴더를 또 생성해 주신 후, 원하는 이미지를 넣어주시면 됩니다.
그리고 route/index.js에서 이미지와 관련된 코드를 수정해주세요.
module / module.exports / exports 를 알아보자! (0) | 2022.12.08 |
---|