이것저것

강의정리 (기초 / 암호화 /JWT / Auth기초) 본문

카테고리 없음

강의정리 (기초 / 암호화 /JWT / Auth기초)

곰태태 2020. 8. 3. 18:45
반응형
SMALL

설정

따로 react 프로젝트를 생성하지 않고, 새로운 폴더에 vscode로 npm init -y 로 package.json 파일생성했다.

그리고 index.js 파일을 생성하고 express와 mongoose를 다운로드받아서 설정해주었다.

현재 서버와 몽구스 모두 정상 연결되었는데 connect함수의 두번째 인수로 설정해준 것들은 기존에 mongoose.set으로 설정하던것을 한 메소드로 처리한 것이다. 

package.json에 scripts 부분에 start를 추가하고 "nodemon index.js"가 실행되도록 해두었다. (물론 노드몬설치함)

그리고 then과 catch로 성공 실패 분기를 나누어서 실행되도록 했다. (여기까지 3강)

 

현재 설치된 dependency : express / mongoose / nodemon

const express = require('express');
const mongoose = require('mongoose');
const app = express();
const port = 3000;

//이 부분은 기존에 따로 mongoose.set한 것과 같지만 connect에 하나로 처리한 것이라서 줄이 줄어듬
mongoose.connect('mongodb+srv://wns312:wns312@cluster0-ptxu3.mongodb.net/test?retryWrites=true&w=majority', {
    useNewUrlParser : true, useUnifiedTopology : true, useCreateIndex : true, useFindAndModify : false
})
.then(()=>{ console.log("connected")}) // 제대로 연결되었다면 then 실행
.catch((err)=>{console.log(err)}) // 연결에 실패했다면 catch 실행

app.get('/', (req,res)=>{
    res.send('Hello world')
})

app.listen(port, ()=>{
    console.log(`http://localhost:${port}`);
})

 

 


몽구스 db 스키마를 생성했다.

 

models폴더 안에 User.js 생성

const mongoose = require('mongoose');

const userSchema = mongoose.Schema({
    name : { // 이름
        type : String,
        maxlength : 50
    },
    email : { // 유일값으로 설정
        type : String,
        trim : true, // 앞뒤공백제거
        unique : 1 // 유일값
    },
    password : { //비밀번호
        type : String,
        minLength : 5
    },
    role : { // 관리자와 일반사용자 구분을 위해
        type : Number, //0이면 일반, 관리자면 1 처럼 구분
        default : 0 // 따로 정하지 않으면 role을 0을 준다
    },
    image : String, // 프로필이미지
    token : { // 토큰
        type : String,
    },
    tokenExp : { // 토큰 유효시간
        type : Number
    }
})

const User = mongoose.model('User',userSchema ) // (모델의 이름, 스키마)
module.exports = {User}

 


.gitignore 파일을 생성해서 node_modules파일을 업로드 제외해주었다.

그리고 git init / git add . / git push -u 레포지토리 주소 해서 현재 진행상황을 업로드 해주었다.

 

깃서버와 안전한 통신을 위해서 ssh가 필요하다.

 

ssh 사용여부 확인 : 터미널 열고(gitbash만 가능) ls -a ~/.ssh 입력한다.

 

 

 

만약 ./ ../ known_hosts 만 있다면 설정이 되어있지 않은 것이므로 설치하자

(id_rsa / id_rsa.pub 이 있다면 설정된 것)

 

1. ssh key 생성

 

https://help.github.com/en/enterprise/2.15/user/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent

여기에 들어가거나 git ssh generate를 검색해서 찾자

 

ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

해서 콘솔을 열고, 복사붙여넣기 하고 엔터치기전에 이메일을 내 이메일로 변경시켜준다. 그리고 엔터 계속치면끝

 

2. ssh Agent를 Background에 킨다 (이것도 나와있음)

eval $(ssh-agent -s)

 

3. private key를 sshAgent에 추가

이제 ssh키인  id_rsa / id_rsa.pub 두개중 private 역할을 하는 id_rsa를 ssh Agent에 넣어주어야 한다.

두개가 있는데 뭘 넣어야 하는지 모르겠다. (일단 위에걸로 하자)

ssh-add -K ~/.ssh/id_rsa

ssh-add ~/.ssh/id_rsa

 

4. public key를 Github에 연결

 

  1) ssh키를 클립보드에 복사

clip < ~/.ssh/id_rsa.pub

  2) github홈페이지 - Settings - SSH and GPG keys - New SSH keys에서

 

타이틀로 My Computer 하고 클립보드에 복사된 키값은 Key에 붙여넣는다

이렇게하면 깃허브와 컴퓨터가 안전하게 통신할 수 있다.

 

 

 

 


BodyParser / Postman / 회원가입기능 / nodemon

 

둘 다 설치 하고 가상의 데이터를 Postman으로 보내 줄 것이다.

...
const bodyParser = require('body-parser')
const {User} = require('./models/User');

//application/x-www-form-urlencoded 타입 데이터를 분석해서 가져올 수 있게 해준다.
app.use(bodyParser.urlencoded({extended : true}));
//application/json 타입 데이터를 분석해 가져올 수 있게 해준다.
app.use(bodyParser.json());

...

app.post('/register', (req, res)=>{
    //회원가입 정보를 client에서 가져오면,
    // 그것들을 데이터베이스에 넣어준다
    const user = new User(req.body); // 데이터를 객체에 넣어준 것(JSON)
    //save하면 객체에 넣은 데이터를 DB에 업데이트한다. save에는 콜백 function이 인수로 들어간다.
    user.save((err, doc)=>{ // 인수는 err과 doc 두가지 (여기서 doc은 유저정보)
        if(err) {return res.json({ success : false, err})} // 에러시 success변수와 에러를 보냄
        
        //200은 성공했다는 의미를 담고 있는 상태번호이다 성공했다는 신호를 보내고 json파일로
        // success 변수를 보낸다 (res.json() 은 json타입 데이터를 리턴시키는 함수이다) 
        return res.status(200).json({
            success : true
        })
    });
})

 

이렇게 해주고 경로에 맞춰서 post요청을 아래와 같이 보내주었고, {success : true} 응답을 받았다.

{
    "name" : "김준영",
    "email" : "wns312@gmail.com",
    "role" : "0"
}

 


 

참고 : nodemon 설치가 있었는데 설치를 아래처럼 했다. 이는 개발시 로컬에서만 사용하기 위해서이다.

그러면 dependencies에서 devDependencies로 이동하는 것을 볼 수 있다.

npm install nodemon --save-dev

(실행은 그냥 하면 된다)

그리고 scripts에 "backend" : "nodemon index.js" 로 설정을 해주었지만, 나는 start에 넣어버렸다.

start로 넣으면 npm start로 실행하면되고, backend로 넣었다면 npm run backend로 실행하면 된다.

 

 

 

 

 

 

 


비밀설정 정보관리

 

남들이 몽고DB를 볼 수 있으므로, 처리해주어야 한다.

우선 프로젝트 루트경로에 config폴더를 만든 뒤 그 안에 dev.js 파일을 만든다. 그리고 주소를 아래처럼 넣어준다.

module.exports = {
    mongoURI : 'mongodb+srv://wns312:wns312@cluster0-ptxu3.mongodb.net/test?retryWrites=true&w=majority'
}

 

로컬환경에서 Dev용, deploy(배포) 한 뒤 Production용으로 둘을 나누어야 한다.

 

development할때는 위의 방식으로 dev.js에서 직접 주소를 가져와서 사용하면 된다. 하지만 이상태로 배포를 해버리면 직접적인 주소가 노출되기 때문에 서버에서는 환경변수를 따로 설정해주고, 그 환경변수를 사용하게 하는것이다.

(헤로쿠의 경우에는 config vars라고 해서 직접 환경변수를 홈페이지에서 관리할 수 있다)

 

NODE_ENV가 배포일때 production으로 나온다는데 이부분은 정확히는 모르겠다.

 

 

그리고 .gitignore에 dev.js를 추가해줌으로써 깃허브에 올려도 노출이 되지 않게 할 수 있다.

위처럼 설정해두면, 조건부로 주소를 다르게 가져오기 때문에 보안면에서 좋다.

 

이제 index.js(서버파일) 로 돌아와서 아래와같이 받아와준다.

({mongoURI} 로 받아오는 이유는 객체안에 key를 속성으로 설정해놨기 때문이다)

const {mongoURI} = require('./config/key') 
...
mongoose.connect(mongoURI, {...});
...

 

git status 명령어를 입력하면, 수정되거나 새로 만들어진 파일 목록을 볼 수있다. (깃 확장프로그램으로도 가능)

 

 

 

 


bcrypt 암호화

 

민감한 정보는 암호화를 해서 데이터베이스에 저장하고,

비교할 때도 받아와서 암호화를 해서 비교하기 위해 모듈을 사용할 것이다.

 

bcrypt 설치

npm install bcrypt --save

https://www.npmjs.com/package/bcryptd

 

이 페이지 usage에서 사용법을 확인해보면 아래와 같이 salt를 가져와서 암호화시키는것을 볼 수있다.

바깥함수는 salt를 만들고 안쪽함수는 이를 이용해 해쉬화시키는 것을 알 수있다.

bcrypt.genSalt(saltRounds, function(err, salt) {
    bcrypt.hash(myPlaintextPassword, salt, function(err, hash) {
        // Store hash in your password DB.
    });
});

saltRounds : 솔트의 길이를 뜻한다.

myPlaintextPassword : 암호화되지 않은 원본 비밀번호를 의미한다.

salt : 바깥 함수에서 generate시켜서 인수로 가져온 salt이다.

 

이를 통해 안쪽함수에서 err, hash를 결과로 받아온다. 여기서 hash가 암호화된 비밀번호이다.


 

1) index.js의 /register 라우터 부분의 데이터와 유저스키마 부분을 변경한다.

 

UserSchema

 

userSchema의 save function의 실행 전에 실행될 함수를 정의하면 실제로 save function의 실행 전에 실행된다.

스키마.pre('메소드종류', 함수(next))
실행해야 할 문장을 다 적고 난 뒤 마지막에 인수로 받은 next()를 적어주어야 실제 save가 정상실행된다

문제는 여기서 arrow function을 쓰면 user부분에서 this가 먹히지 않는다.

따라서 arrow function을 사용하지 않는걸로 하자

//비밀번호 암호화
userSchema.pre('save', function(next){
    // 여기가 애매한데 여기서 this는 userSchema데이터를 가리킨다. 
    let user = this; //이제 hash함수에서 user.password를 활용할 것
    //사용자의 이름만 변경할때, 비밀번호를 암호화하면 안되기 때문에 조건문을 걸어준다.
    if(user.isModified('password')) {   
        bcrypt.genSalt(saltRounds, function(err, salt) {
            if (err) return next(err); // 에러시 next에 에러를 담아 보낸다
            bcrypt.hash(user.password, salt, function(err, hash) {
                if(err) return next(err);
                user.password = hash // 해시화된 비밀번호로 변경을 시켜준다.
                next();
            });
        });
    }else{
        next();
    }
})

 

1) 비밀번호 변경시에만 암호화가 실행되어야 하므로, 조건문을 걸어준다.

(mongoose함수인 idModified()는 변경되었는지 boolean타입으로 알려준다)

 

2) this로 데이터 정보를 받아오고, bcrypt의 genSalt()를 이용해 솔트를 만들고,

그 안에서 hash()를 이용해 암호화 한다

 

3) 암호화된 비밀번호를 user.password에 적용하고 next()로 빠져나온다

 

4) if문으로 수정될 때만 암호화하도록 변경한다.

 

 

 

 

 


로그인 기능

 

비밀번호를 받아와서 비교하기

 

로그인 기능을 만들기 위해서 로그인route가 필요하므로 로그인 route를 만들어준다.

app.post('./login', (req, res)=>{
    //1. 요청된 이메일을 db에서 조회
    User.findOne({email : req.body.email}, function (err, user) {
        if(!user) return res.json({loginSuccess : false, err})
        //유저정보가 없을경우(에러인 경우와는 다르다)
        if(!user) return res.json({
            loginSuccess : false,
            message : "이메일에 해당하는 유저가 없습니다"
        })
    //2. 조회성공시 이메일 일치여부를 확인한다
        user.comparePassword(req.body.password, (err, isMatch)=>{
            if(err) return 
        })
    })
    //3. 일치시 유저를 위한 토큰을 생성한다
})

여기서 comparePassword라는 함수는 직접 스키마에서 만들어주는 함수이다.

이제 스키마 부분을 보자

스키마에 함수는 스키마이름.methods.함수이름 = function()으로 만든다.

 

여기서 두번째 인수에 콜백함수라는 이름을 사용했는데, 실제 사용시 두번째 인수에 함수를 적어놓으면 그 함수를 들고 들어와서 사용한다. (잘 기억해두자)

//함수를 실행할 때, 두번째 인수에 콜백함수를 넣으면 이 함수를 들고 들어오게 되는 것이다.
userSchema.methods.comparePassword = function(plainPassword, callback){
    //입력한 (암호화되지않은)비밀번호와 암호화된 db의 비밀번호 비교
    //입력한 비밀번호를 암호화해서 비교해주는 compare메소드 이용
    //(암호화되지 않은 입력비번, 암호화된 비번(find에서 사용하므로), 콜백함수)
    bcrypt.compare(plainPassword, this.password, function (err, isMatch) {
        if(err) return callback(err); // 에러시 콜백함수에 err만 담아보내고
        callback(null, isMatch) // 에러아닐시 콜백함수에 err는 null, isMatch(true)를 담아보낸다
    })
}

bcrypt에서 제공하는 compare함수는 암호화 되지않은 함수와 암호화된 함수를 비교해서 일치여부를 확인해주는 함수이다. (쌩비번, db의 암호화비번, 콜백함수)

콜백함수는 첫번째인수로 에러를, 두번째 인수로 결과를 boolean으로 넘겨준다.

에러가 없을 시 이 boolean결과를 callback함수에 인수로 주어 실행시키면 된다.

 

 

이제 다시 login라우터로 돌아오자

    //2. 조회성공시 이메일 일치여부를 확인한다
    //함수정의 후 여기로 다시 돌아온다
        user.comparePassword(req.body.password, (err, isMatch)=>{
            if(!err) return res.json({loginSuccess : false, err});
            if(!isMatch) {
                return res.json({
                    loginSuccess : false,
                    message : "비밀번호가 일치하지 않습니다"
                })
            }
    //3. 일치시 유저를 위한 토큰을 생성한다
    // 다시 스키마에 generateToken함수를 만들어준다.
            user.generateToken((err, user)=>{

            });
        })

이렇게 작성을 해주고, user.generateToken이라는 함수를 실행시켰다.

사실은 실행은 맞는데, 존재하는 함수가 아니다. 또 함수 만들러 가야함 ㅋㅋ

 


웹 토큰 생성하기 (JWT)

 

 

더 작성하기 전에 JWT 설치가 필요하다.

 

npm install jsonwebtoken --save

를 사용해서 설치를 진행해주자

사용법 참고 : https://www.npmjs.com/package/jsonwebtoken

 

곧 써야하니 cookie-parser도 설치해주자

npm install cookie-parser --save

이제 JWT를 설치 완료했으니 스키마로 이동해서 다시 user.generateToken 함수를 정의해준다.

 

토큰을 생성해주는 함수를 스키마에 또 생성해준다

userSchema.methods.generateToken = function (callback) {
    //jwt을 이용해서 토큰 생성하기
    let user = this; //마찬가지로 입력 정보를 this로 가져온다 
    let token = jwt.sign(user._id.toHexString(), "secretToken") // 여기서의 id는 고유값인 _id : ObjectId("...") 를 말한다
    //이렇게 sign에 고유값_id와 문장을 넣어주면 이 둘을 더해서 토큰을 만들어준다
    //추후에 jwt 조회를 할 시 ObjectId를 받아와서 문장을 더해 토큰화 해서 둘을 비교하는 방식으로 인증한다

    user.token = token; // 로그인유저의 token정보에 암호화된 token을 넣어준다
    user.save(function (err, user) { // 토큰을 넣고, 서버에 저장한다
        if(err) return callback(err);
        callback(null, user); // 성공시 콜백에 저장한 유저정보를 넣어서 리턴시켜준다
    });
}

 

그리고 돌아와서 generateToken()을 사용하면 콜백으로 user를 받는데, 이 유저는 토큰을 가진 유저다.

 

여기서 쿠키를 사용해야 하므로 아래와 같이 사용설정을 해준다

 

const cookieParser= require('cookie-parser');
...
app.use(cookieParser());
...

 

이제 app.post('/login') 안의 User.findOne() 안의 user.comparePassword()안의 user.generateToken()을 완성

 

user.generateToken((err, user) => {
  //이제 여기에는 유저 정보가 담겨있다
  if (err) return res.status(400).send(err); //400은 에러가 있다는 뜻
  //토큰을 저장한다 어디에?? 1) 쿠키에 2) 로컬스토리지.. 우리는 쿠키에 저장할 것
  res.cookie("x_auth", user.token) // x_auth라는 이름으로 쿠키가 들어감
  .status(200)  //성공 신호보내주고
  .json({ loginSuccess : true, userId : user._id }) 
  // JSON으로 로그인 성공 변수와 ObjectID를 userId라는 이름으로 보내줌
});

 


오류가 하나 있었는데 이는 스키마의 generateToken메소드 선언에 있었다.

user._id가 아닌 user._id.toHexString()을 써주어야 하는 거였다.

 

이유는 jwt.sign메소드의 첫번째인수는 String이어야 하는데 ObjectId는 String이 아니었기 때문이다.

JSON.stringify 로 문자열로 만들수도 있지만, 더 보안을 주려고 한거같다.

 

 

 

 

 


Auth 기능 만들기

어떤 사이트를 들어갔을 때 로그인한 사용자만, 혹은 관리자 계정만 사용할 수 있는 페이지가 있을 것이다.

이런 사용자 인증을 해주는 것이 authentication기능이다.

 

위에서 쿠키에 넣어준 토큰을 DB의 토큰과 요청할 때마다 비교하는 방법으로 인증한다.

 

1) 쿠키에 든 토큰을 받아서 user._id를 인코딩할때 썼던 문자열로 디코딩한다. ('secretToken')

-> 쿠키에 저장된 토큰을 가져와서 서버에서 복호화

2) 복호화된 id와 일치하는  DB데이터를 찾고, 토큰이 일치하는지 확인한다 (토큰불일치 / 토큰일치)

 

우선 경로들을 /api/user/... 으로 바꿔주자.

그리고 루트경로에 middleware라는 폴더를 만들고 안에 auth.js 파일을 생성한다.

 

아 그리고 2줄로 고정하려면 tab이라고 설정 검색해서 detectIndentation을 끄면 2줄 된다.

 

index.js : 여기서 미들웨어 함수로 auth를 실행해준다는 것만 확인하고 넘어가자

const {auth} = require('./middleware/auth') 
...
app.get('/api/users/auth', auth,  (req, res)=>{ // auth는 ()를 적지 않는다
  //여기까지 왔다는것은, Auth가 true라는 뜻이다
  res.status(200).json({
    //원하는것만 전달해주면 된다
    userId : req.user._id,
    isAdmin : req.user.role === 0 ? false : true, // 0이 아니면 관리자인 것
    isAuth : true,
    email : req.user.email,
    name : req.user.name,
    lastname : req.user.lastname,
    image : req.user.image
  })
})

 

auth.js : 여기서 findByToken은 스키마에서 만들어준 함수이다

const {User} = require('../models/User');

let auth = (req, res, next)=>{
  // 인증 처리 담당
  //쿠키에서 토큰 가져오기
  let token = req.cookies.x_auth; // x_auth라는 이름으로 저장한 쿠키를 가져옴 (=token)
  //토큰 복호화하기
  //복호화된 토큰으로 유저 찾기
  User.findByToken(token, (err, user)=>{
    //유저가 있으면 인증 OK, 없으면 인증 NO
    if(err) throw err;
    if(!user) return res.json({isAuth : false, error : true});
    //유저가 있는 경우
    req.token = token; // req에 넣어주므로 인해서 req로 바로 사용하기 편해지기 때문이다
    req.user = user;
    next(); // 다 작성하고 나서 다시 요청으로 보내주기 위해 호출
  });
}
module.exports = {auth}

 

User.js : verify함수는 복호화함수이다 (decoded는 _id임)

//객체생성 안하고 쓸거라서 statics로 생성
userSchema.statics.findByToken = function (token, callback) {
  let user = this;
  //토큰을 복호화한다.
  jwt.verify(token, "secretToken", function (err, decoded) {//복호화 메소드
    //유저아이디로 유저를 찾고, 클라이언트에서 가져온 토큰을 비교
    user.findOne({"_id" : decoded, "token" : token}, (err, user)=>{ 
      if(err) return callback(err);
      callback(null, user)
    })
  }) 
}

 

 

auth는 끝이지만 오류 확인은 못해봤다.

 

반응형
LIST
Comments