암호화
비밀번호를 데이터베이스에 저장할 때, 클라이언트가 입력한 값 그대로 저장하게 되면 노출될 위험이 당연히 크기 때문에 테스트용이 아닌 이상 암호화는 필수이다. 따라서 DB에 저장할 때에는 암호화된 비밀번호를 저장해야 한다.
단방향 암호화란,
입력받은 비밀번호를 암호화하지만 다시 복호화하여 되돌릴 수는 없는 것이다.
양방향 암호화란,
암호화된 비밀번호를 복호화하여 기존에 입력한 비밀번호를 알 수 있다.
몽구스 모듈은 단방향으로 암호화를 하며 사용자가 입력한 값을 암호화하여 기존의 암호화 비밀번호와 비교하는 작업을 한다. 따라서 몽구스와 virtual 함수, crypto모듈 등을 통해 로그인하는 예제를 알아보겠다.
crypto 모듈 설치
npm install crypto --save
cmd창에서 crypto 모듈을 설치해준다.
crypto 모듈은 사용자가 입력한 패스워드와 salt를 합쳐서 암호화한다.
모듈 등록 및 데이터베이스 연결
require("dotenv").config();
var express = require("express");
var http = require("http");
var path = require("path");
var serveStatic = require("serve-static");
var mongoose = require("mongoose");
var crypto = require("crypto")
var database;
var UserSchema;
var UserModel;
function connectDB(){
var databaseUrl = "mongodb://localhost:27017/shopping";
mongoose.connect(databaseUrl);
database = mongoose.connection;
database.on("open",function(){
console.log("데이터베이스가 연결되었습니다: " + databaseUrl);
createUserSchema();
});
database.on("error",console.error.bind(console,"몽구스 연결 에러..."));
database.on("disconnected",function(){
console.log("DB연결이 끊겼습니다. 5초 후 재연결 합니다.");
setInterval(connectDB(),5000);
});
}
예제에서 사용하 모듈을 먼저 등록해주었다.
특히 npm으로 설치한 crypto모듈도 등록해준다.
데이터 베이스 연결을 할 connectDB 메소드를 입력해주며
스키마와 모델 객체를 다룰 createUserSchema 함수를 호출해준다. 바로 밑에서 내용은 확인할 수 있다.
그 외의 내용들은 모두 바로 지난 블로그 글에서 다루었으니 참고 바란다.
스키마 및 모델 정의 함수 - createUserSchema
function createUserSchema(){
UserSchema = mongoose.Schema({
id:{type:String,required:true,unique:true},
hashed_password:{type:String,required:true},
salt:{type:String,required:true},
name:{type:String},
age:{type:Number,'default':20},
created:{type:Date,index:{unique:false},'default':Date.now}
});
UserSchema
.virtual("pwd")
.set(function(pwd){
this._password = pwd;
this.salt = this.makeSalt();
this.hashed_password = this.encryptPassword(pwd);
})
.get(function(){
return this._password;
});
- 스키마 정의
mongoose.Schema를 통해 스키마를 정의할 것이다.
type은 데이터 타입이고
required은 not null와 같이 필수 입력 유무,
unique는 중복 값 허용 유무이다.
'default'는 입력하지 않을 시 저장되는 기본값이다.
hashed_password는 암호화된 패스워드이고
salt는 패스워드를 암호화할 때 붙일 key값이다.
- virtual 함수
스키마에 virtual함수를 넣을 것이다.
virtual함수의 괄호 속에는 가상 속성 이름이 들어간다.
set을 통해 {"info":"aa","홍길동"}처럼 값을 넣을 때 값 가운데의 ,를 구분하여 배열로 저장하게 한다.
UserSchema.method("makeSalt",function(){
console.log("date : " + new Date().valueOf());
console.log("math : " + Math.random());
return Math.round((new Date().valueOf() * Math.random())) + "";
});
UserSchema.method("encryptPassword",function(inputPwd,inSalt){
if(inSalt){
return crypto.createHmac("sha1",inSalt).update(inputPwd).digest("hex");
}else{
return crypto.createHmac("sha1",this.salt).update(inputPwd).digest("hex");
}
});
UserSchema.method("authenticate",function(inputPwd,inSalt,hashed_password){
if(inSalt){
console.log("사용자 입력 pwd: " + inputPwd);
console.log("암호화된 pwd: " + this.encryptPassword(inputPwd,inSalt));
console.log("DB에 저장되어있는 pwd: " + hashed_password);
return this.encryptPassword(inputPwd,inSalt)==hashed_password;
}else{
console.log("사용자 입력 pwd: " + inputPwd);
console.log("암호화된 pwd: " + this.encryptPassword(inputPwd,inSalt));
console.log("DB에 저장되어있는 pwd: " + hashed_password);
return this.encryptPassword(inputPwd,inSalt)==this.hashed_password;
}
});
- salt 생성
salt값을 생성하기 위한 makeSalt 메서드를 만드는 작업이다.
Date 객체의 원시값을 가져오는 Date().valueOf()와 난수를 반환하는 Math.random()을 곱하여
round 반올림을 통해 salt값을 생성하였다.
추가로 ""을 더해 데이터 타입이 String으로 되게 하였다.
- 패스워드 암호화 작업
비밀번호를 암호화하기 위한 encryptPassword 메소드이다.
inSalt의 값이 있다면 if문을 실행한다.
crypto 모듈을 통해 sha1 방식으로 패스워드를 암호화하여 16진수의 hex상태로 저장한다.
inSalt가 없다면 이미 갖고 있는 salt를 가져온다.
- 암호화된 패스워드와 비교
로그인할 때 db에서 가져온 암호화된 패스워드와 사용자가 입력한 패스워드를 비교해줄 authenticate 메소드이다. inSalt가 있다면 encryptPassword메소드에서 구한 값과 매개 변수로 들어온 hashed_password를 비교하여 true or false의 값을 반환한다.
UserSchema.static("findById",function(id,callback){
return this.find({id:id},callback);
});
UserSchema.static("findAll",function(callback){
return this.find({}, callback);
});
console.log("UserSchema 정의함.");
UserModel = mongoose.model("users3",UserSchema)
console.log("UserModel 정의함.");
}
- 스키마 객체에 메소드 추가
statice() 혹은 method()를 통해 스키마에 메소드를 넣을 수 있다.
findById는 로그인할 때 사용할 메소드이므로 id만 가져오게 하였고
findAll은 전체 데이터를 확인하기 위함이므로 {}을 입력하였다.
- Model 정의
users3 컬렉션에 UserSchema 스키마가 적용되도록 model을 설정해주었다.
익스프레스 객체 생성 + 로그인 함수
var app = express();
app.set("port",process.env.PORT||3000);
app.use(express.urlencoded({extended:false}));
app.use("/public",serveStatic(path.join(__dirname,"public")));
app.use(expressSession({
secret:"my Key",
resave:true,
saveUninitialized:true
}));
var authUser = function(database,id,pwd,callback){
console.log("authUser 함수 호출..");
UserModel.findById(id, function(err,result){
if(err){
callback(err,null);
return;
}
if(result.length>0){
console.log("아이디와 일치하는 사용자 찾음");
var user = new UserModel({id:id});
var authenticated = user.authenticate(pwd,result[0]._doc.salt,result[0]._doc.hashed_password);
if(authenticated){
console.log("비밀번호 일치함");
callback(null,result);
}else{
console.log("비밀번호 일치하지 않음");
callback(null,null);
}
}else{
console.log("아이디와 일치하는 데이터가 없습니다.");
callback(null,null);
}
});
}
익스프레스 객체 생성 역시 지난 글에서 작성했으므로 변수 authUser에 대해 집중적으로 살펴보겠다.
라우터에서 불러 쓰기 위해 authUser 함수를 작성해주었다.
위에서 만든 findById메소드에 사용자가 입력한 id를 매개변수로 넣어서 아이디를 먼저 비교한다.
에러가 났을 경우 콜백함수에서 result에 null을 넣어준다.
데이터가 있다면, 즉 findById에서 걸러진 값이 있다면 user 변수에 컬렉션을 불러온다.
authenticate 메소드에 클라이언트의 패스워드 입력값과 문서의 salt를 가져가 encryptPassword메소드에 값을 넣어 암호화한 패스워드를 구한다.
나아가 마지막 매개변수인 db에 저장된 암호화된 패스워드와 비교한다.
데이터는 당연히 1개이므로 배열의 인덱스값은 0이다.
authenticated를 통해 true값, 즉 같다는 결과가 나왔다면
에러는 없으니까 에러에 null만 보내주고
같지 않다면 result에도 null값을 보내준다.
마지막의 else문은 findById에서 통과하지 못했기에 데이터가 없다는 것이다.
여기까지가 createUserSchema 함수이다!
라우터 객체 생성
- 사용자 로그인 라우터
var router = express.Router();
router.route("/process/login").post(function(req,res){
console.log("/process/login 호출..");
var id = req.body.id;
var pwd = req.body.pwd;
if(database){
authUser(database, id, pwd, function(err,result){
if(err) {throw err;}
if(result){
var userName = result[0].name;
res.writeHead("200",{"Content-type":"text/html;charset=utf-8"});
res.write("<h1>로그인 성공</h1>");
res.write("<div>아이디: " + id + "</div>");
res.write("<div>이름: " + userName + "</div>");
res.end();
}else{
res.writeHead("200",{"Content-type":"text/html;charset=utf-8"});
res.write("<h1>로그인 실패</h1>");
res.write("<div>아이디 또는 패스워드를 확인하세요</div>");
res.write("<br/><br/><a href='/public/login.html'>다시 로그인</a>");
res.end();
}
});
}else{
res.writeHead("200",{"Content-type":"text/html;charset=utf-8"});
res.write("<h1>데이터베이스 연결 실패</h1>");
res.write("<div>데이터베이스를 연결하지 못했습니다.</div>");
res.end();
}
});
/process/login 경로가 올 때 라우터가 실행되도록 한다.
id와 pwd를 로그인 html에서 post방식으로 넘겨받는다.
database가 있을 때 위에서 만든 authUser 함수를 호출한다.
result 유무에 따라 클라이언트가 시도한 로그인에 대한 응답을 html로 보여준다.
이 부부도 지난 글에서 작성하였으니 참고 바란다.
- 사용자 리스트 라우터
router.route("/process/listUser").post(function(req,res){
console.log("/process/listUser 호출..");
if(database){
UserModel.findAll(function(err,result){
if(err){
res.writeHead("200",{"Content-type":"text/html;charset=utf-8"});
res.write("<h2>사용자 리스트 조회중 에러 발생</h2>");
res.end();
return;
}
if(result){
res.writeHead("200",{"Content-type":"text/html;charset=utf-8"});
res.write("<h2>사용자 리스트</h2>");
res.write("<div><ul>");
for(var i=0;i<result.length;i++){
var id = result[i]._doc.id;
var name = result[i]._doc.name;
var age = result[i]._doc.age;
var created = result[i]._doc.created;
res.write('<li>#' + (i+1) + ' : ' + id + ", " + name + ', ' + age + '</li>');
}
res.write("</ul></div>");
res.write("<br/><br/><a href='/public/listUser.html'>리스트</a>");
res.end();
}else{
res.writeHead("200",{"Content-type":"text/html;charset=utf-8"});
res.write("<h2>사용자 리스트 조회 실패</h2>");
res.end();
}
});
}
});
경로에 /process/listUser가 올 때 실행될 사용자 리스트 라우터이다.
database가 있다면 findAll 함수를 실행한다.
값이 있다면 사용자 데이터가 많을 수 있음을 for문으로 내용을 출력한다.
findAll에 모든 데이터를 담아왔으므로 변수명은 id, name, age 그대로 가져온다.
라우터 객체 등록 + Express 서버 시작
app.use("/",router);
http.createServer(app).listen(app.get("port"),function(){
console.log("익스프레스 서버를 시작했습니다: " + app.get("port"));
connectDB();
});
마지막으로 app에 라우터 객체를 등록해주고
express 서버를 시작하면 된다!
4월 6일 수업 🌜
오늘 함수와 매개변수가 많았고 서로 섞여있어서 이해하는 데에 쉽지 않았다.
그래서 수업이 끝나고 동기들과 스터디하면서도 헷갈리는 부분 얘기하곤 했는데
블로그를 쓰면서 설명하다 보니 찜찜했던 부분이 이해가 되면서 완벽 해소되었다!! 뿌듯하다 -
'개발 교육 TIL > back-end' 카테고리의 다른 글
[Node.js] 위치기반 서비스 서버 만들기 (Location Based Service) (0) | 2022.04.12 |
---|---|
[Node.js] Express 서버 연결, 데이터베이스, 라우터 - 파일 분리 (0) | 2022.04.12 |
[Node.js] mongoose 모듈 - 설치 및 예제 (회원가입, 로그인) (0) | 2022.04.05 |
[Node] Express 설치&서버 연결 / post 데이터 및 오류페이지(404) 출력 (0) | 2022.04.04 |
[node] 내장모듈 - path, fs (0) | 2022.04.01 |