이 강의에서는 Node.js 의 인기있는 웹서버 프레임워크인 Express.js 서버에서 JSON Web Token 을 사용하여 회원인증 시스템을 구현하는 방법을 알아보겠습니다.
사전 지식
이 강의에서는 Node.js에 대한 배경지식이 있어야합니다. Node.js 를 잘 모르시는분들은 Node.js 기초 강의 를 먼저 읽어주세요. 추가적으로, 토큰 기반 시스템 (포스트 i)과 JWT(포스트 ii)에 대한 이해가 필요하니, 지난 포스트들을 읽지 않으신분들은 강의를 시작하기전에 한번 참조해주세요.
추가적으로, 이 강의에서는 ES6 문법을 사용합니다.
* 이 강좌를 진행 하면서 이해가 가질 않거나 궁금한것이 있으면 언제든지 덧글로 달아주세요.
소개
회원인증 시스템은 모든 어플리케이션에서 정말 중요한 부분입니다. 기존의 어플리케이션들은 세션 기반 회원인증 시스템을 많이 사용해왔습니다. 지금도, 많은 곳에서 사용되고 있죠. 하지만 요즘은 토큰을 기반으로 회원인증 시스템을 구축하는 서비스 회사들이 늘어가고있습니다. Facebook, LinkedIn, Instagram, GitHub, Google 등 수많은 공룡급 회사에서 사용을 하고있지요. 지금은 이런 인증 시스템이 가히 ‘대세‘ 라고 할 수 있겠습니다. (주관적인 표현입니다.)
이를 사용함으로서, 서버측의 확장성이 높아지고, 보안시스템을 강화 할 수 있습니다. 하지만 무조건 좋은것만은 아니겠죠. 토큰을 사용함으로서 보안시스템을 강화 할 수도 있겠지만, 오히려 잘못사용하면 매우 취약해지기때문에 잘 알고 사용하는것이 좋습니다.
토큰기반인증 시스템은 API 모델을 가진 어플리케이션에 매우 적합합니다. 요즘은 Angular2, React, Vue 등을 사용해서 많은 서비스들이 만들어져있고 그런 자바스크립트 라이브러리 혹은 프레임워크를 사용하는 어플리케이션들은 REST API 던 GraphQL API 던.. 정말 많이 의존하고 있죠. 모바일앱도 마찬가지구요.
세션기반인증 시스템도 충분히 제구실을 하지만, 여러분이 ‘모던 웹/앱 개발자’라면, 토큰기반인증 시스템은 한번쯤 배워볼 가치가있습니다. 사용해보고, 여러분의 서비스에 적합하다 싶으면 도입을 하면되죠.
자, 그럼 시작해봅시다!
이 프로젝트에 사용된 코드는 GitHub 에서 에서 열람 하실 수 있습니다.
준비물
이 강의를 진행하기 위해 필요한 주요 준비물은 다음과 같습니다.
- Node.js LTS 버전(현재 기준 6.91) 과 npm
- MongoDB 서버 (강의에서는 편의상 mLab 에서 호스팅을 받아 사용합니다. 본인이 원한다면 몽고디비 서버를 직접 설치하여 사용해도 됩니다)
- 코드 에디터 (자신이 가장 좋아하는 에디터를 사용하세요. 강의에서는 VS Code 를 사용하도록 하겠습니다)
- POSTMAN – API 테스팅 크롬 확장 프로그램
#1 프로젝트 생성 및 설정하기
프로젝트 설정
먼저 nodejs-jwt-example
이라는 디렉토리를 만들은후, 터미널로 해당 디렉토리를 열어 npm 으로 프로젝트를 생성하세요.
npm -y
그러면 디렉토리내에서 프로젝트를 기본설정으로 설정하게되면서 package.json
파일이 생성됩니다. 한번 열어보세요.
{ "name": "nodejs-jwt-example", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
잘 됐나요? 아직은 npm 을 사용하여 설치한 모듈들이 없기때문에, dependency 가 존재하지 않습니다.
의존(dependency) 모듈 설치
자, 이제 저희 프로젝트에서 필요한 의존 모듈을 설치하겠습니다.
npm install --save express body-parser jsonwebtoken mongoose morgan
- express: 저희 프로젝트에서 사용 할 웹서버 프레임워크입니다.
- body-parser: 클라이언트측에서 요청을 받을때, url-encoded 쿼리 및 json 형태의 바디를 파싱하는데 도움을 주는 모듈입니다.
- jsonwebtoken: 이 예제프로젝트에서 사용되는 핵심 모듈입니다. JSON Web Token 을 손쉽게 생성하고, 또 검증도 해줍니다.
- mongoose: 서버에서 MongoDB 를 사용하기 위하여 설치합니다.
- morgan: Express 서버에서 발생하는 이벤트들을 기록해주는 미들웨어입니다
설치가 끝났으면, 다시 package.json
을 확인해보세요. (...)
은 생략되었다는 의미 입니다.
{ (...) "license": "ISC", "dependencies": { "body-parser": "^1.15.2", "express": "^4.14.0", "jsonwebtoken": "^7.1.9", "mongoose": "^4.7.1", "morgan": "^1.7.0" } }
디렉토리 구조
프로젝트에서 사용 할 디렉토리 구조는 다음과 같습니다. 각 파일들은 진행을 하면서 하나하나 생성 할 것이며 설명 또한 진행하면서 하도록 하겠습니다.
문법 검사를 위하여
ESLint
를 사용하고있습니다. 위 프로젝트의ESLint
설정 파일의 내용은 여기서 확인할수있습니다.ESLint
를 설정하는건 필수는 아닙니다. 사용을 원하시는 분들은.eslintrc
파일을 생성하시고 npm 으로ESlint
를 설치하세요npm install --save-dev eslint
#2 User 스키마 작성하기
mongoose 에서 User 정보를 MongoDB 에 넣기 위해선, 스키마를 먼저 만들어야합니다.
models/user.js
에 다음 코드를 작성해주세요
models/user.js
const mongoose = require('mongoose') const Schema = mongoose.Schema const User = new Schema({ username: String, password: String, admin: { type: Boolean, default: false } }) // create new User document User.statics.create = function(username, password) { const user = new this({ username, password }) // return the Promise return user.save() } // find one user by using username User.statics.findOneByUsername = function(username) { return this.findOne({ username }).exec() } // verify the password of the User documment User.methods.verify = function(password) { return this.password === password } User.methods.assignAdmin = function() { this.admin = true return this.save() } module.exports = mongoose.model('User', User)
앞으로 User 모델을 사용하여 처리를 하기 위한 작업들을 멤버 / 스태틱 메소드로 미리 준비했습니다.
create
메소드는 새 유저를 생성합니다. 원래는 이 메소드처럼 비밀번호를 그대로 문자열 형태로 저장하면 보안적으로 매우 나쁩니다. 일단 지금은 배우는 과정이니 간단하게 문자열로 저장을 하지만, 포스트의 후반부에서는 비밀번호를 해쉬하여 저장하도록 하겠습니다findOneByUsername
메소드는 username 값을 사용하여 유저를 찾습니다verify
메소드는 비밀번호가 정확한지 확인을 합니다. 지금은 그냥===
를 사용해서 비교 후 결과를 반환하지만 포스트 후반부에서는 해쉬를 확인하여 결과를 반환하겠습니다assignAdmin
메소드는 유저를 관리자 계정으로 설정해줍니다. 저희 예제 프로젝트에서는, 가장 처음으로 가입한 사람과, 관리자가 나중에 API 를 사용하여 지정한사람이 관리자 권한을 부여 받습니다.
#3 MongoDB 준비 및 설정 파일 (config.js) 만들기
config.js
파일은 우리 예제 프로젝트에서 사용할 MongoDB 서버의 정보와, JWT 토큰을 만들 때 사용 될 secret
키의 정보를 지니고있습니다. 이렇게 보안에 관련된 정보는 따로 파일에 분리하여 관리를 하는게 좋습니다. 이렇게 하면, 예를들어, github 에 오픈 소스를 할 때, .gitignore 에 추가해서 해당 파일은 싱크가 되지 않도록 설정 할 수 있겠죠? (이 프로젝트의 github 저장소에서도 config.js 파일은 커밋이 되어있지 않습니다. 예제 정보가 적혀있는 config.example.js 파일의 이름을 config.js 로 수정하고 사용해야합니다.
설정 파일을 작성하기 전에, MongoDB 서버를 준비해주어야 합니다. 개인 서버에서 MongoDB 를 돌려도 되고, 아니면 mLab 이라는 MongoDB 호스팅 서비스를 사용해도 됩니다. 이 포스트에서는 mLab 을 사용하도록 하겠습니다.
mlab 가입 및 데이터베이스 생성
자, 이제 데이터베이스를 설정하는 과정이 끝났습니다. mLab 에서 데이터베이스 페이지의 상단을 보시면 다음과 같은 텍스트가 있습니다.
이 값을 복사하세요.
config.js 파일 작성
이제, config.js
파일을 생성하여 다음 코드를 작성하세요.
config.js
module.exports = { 'secret': 'SeCrEtKeYfOrHaShInG', 'mongodbUri': 'mongodb://velopert:password@ds127428.mlab.com:27428/jwt-tutorial' }
위에서 복사했던 MongoDB URI 를 코드에 붙여넣고, <dbuser>:<password>
가 있던 부분에 여러분이 아까 만들었던 계정 정보를 입력하세요.
여기서 secret
은 나중에 JWT 토큰을 검증하는 서명부분을 만들 때, 해싱 알고리즘에서 사용 될 비밀 키 입니다.
#4 서버 코드 작성하기
이제 서버의 메인 부분인 app.js
의 코드를 작성해봅시다.
주요코드를 먼저 작성하고, 추후 라우터들을 만든 다음에 또 수정 하겠습니다
app.js
/* ======================= LOAD THE DEPENDENCIES ==========================*/ const express = require('express') const bodyParser = require('body-parser') const morgan = require('morgan') const mongoose = require('mongoose') /* ======================= LOAD THE CONFIG ==========================*/ const config = require('./config') const port = process.env.PORT || 3000 /* ======================= EXPRESS CONFIGURATION ==========================*/ const app = express() // parse JSON and url-encoded query app.use(bodyParser.urlencoded({extended: false})) app.use(bodyParser.json()) // print the request log on console app.use(morgan('dev')) // set the secret key variable for jwt app.set('jwt-secret', config.secret) // index page, just for testing app.get('/', (req, res) => { res.send('Hello JWT') }) // open the server app.listen(port, () => { console.log(`Express is running on port ${port}`) }) /* ======================= CONNECT TO MONGODB SERVER ==========================*/ mongoose.connect(config.mongodbUri) const db = mongoose.connection db.on('error', console.error) db.once('open', ()=>{ console.log('connected to mongodb server') })
코드 작성이 완료되었다면, 터미널에서 node app.js
를 입력하여 서버를 실행해보세요.
> node app.js Express is running on port 3000 connected to mongodb server
잘 되었나요? 그렇다면 Postman을 열어서 http://localhost:3000/ 에 GET 요청을 해보세요.
Hello JWT 라고 응답을 하네요! 게속해서 진행해봅시다..
앞으로 서버를 수정하고 테스팅을 할 때 마다 서버를 재시작해주어야 합니다. 재시작 하는게 귀찮다면
nodemon
도구를 사용해서 서버를 실행하면 서버가 수정 될 때마다 자동으로 재시작됩니다.npm install -g nodemon nodemon server.js
#5 회원가입 구현
app.js
파일에서 모든 코드를 입력해버리면 파일도 너무 커지고 보기 힘드니까, 라우터를 작성해봅시다. 우리는 라우터의 기본 틀을 먼저 만들고 나서, 기능구현은 그 다음에 하도록 하겠습니다.
회원가입 API 준비하기
auth
라우터부터 작성을 해볼까요? 회원가입의 라우트는 /api/auth/register
로 설정하도록 하겠습니다.
routes/api/auth/controller.js
파일을 생성하여 다음 코드를 작성하세요.
routes/api/auth/controller.js
/* POST /api/auth/register { username, password } */ exports.register = (req, res) => { res.send('this router is working') }
회원가입 API 의 기본 틀을 만들어놓았습니다.
그 다음엔, routes/api/auth/index.js
파일에서 방금 만든 컨트롤러 파일을 불러온 후, 라우터를 정의해봅시다.
routes/api/auth/index.js
const router = require('express').Router() const controller = require('./auth.controller') router.post('/register', controller.register) module.exports = router
우리는 auth
라는 라우터를 정의했습니다.
이제 상위 폴더로 올라가서 api
라는 라우터를 정의 한 뒤, 해당 라우터에서 /auth
로 요청이 들어오면 위 라우터로 연결시켜줍시다.
routes/api/index.js
const router = require('express').Router() const auth = require('./auth') router.use('/auth', auth) module.exports = router
코드가 거의 비슷하지요?
마지막으로, app.js
파일을 다시 열어서 /api/
경로로 요청이 들어오면 위 라우터로 연결시켜줍시다.
app.js
// (...) // index page, just for testing app.get('/', (req, res) => { res.send('Hello JWT') }) // configure api router app.use('/api', require('./routes/api')) // open the server app.listen(port, () => { console.log(`Express is running on port ${port}`) }) // (...)
지금까지 한 작업을 정리하자면, auth
→ api
→ app
순으로 한단계 한단계 거슬러 올라간 것 입니다. 자 이제 코드를 저장 후, Postman 으로 http://localhost:3000/api/auth/register 경로에 POST 요청을 해보세요
제대로 작동 하는군요.
회원가입 API 기능 구현하기
회원가입 기능을 구현하기전에 우리 프로젝트에서 회원가입부분에서 사용할 알고리즘을 알아봅시다
회원가입 요청이 들어오면, 아이디가 중복되는지 확인을 하고, 새 회원을 등록합니다. 그리고 만약에 해당 유저가 첫번째 유저라면, 그 유저를 관리자 권한을 부여합니다.
이 작업에서 우리는 MongoDB에 3번의 쿼리를 해야합니다. 데이터베이스에 쿼리를 하는것은, 비동기 작업이죠. Node.js 에서 비동기작업은 보통 콜백으로 처리를하지요. 하지만, 이렇게 비동기작업이 많아지면 콜백안에 콜백안에 콜백이 생기게됩니다.
콜백이 많아지면… 콜백지옥이라고도 부르죠.
업데이트된 자바스크립트의 문법 ES6 에서는 Promise 라는 기능을 지원하여 조금더 코드를 보기 좋게 작성 할 수 있게 해줍니다. 만약에 Promise 에 대해서 잘 모르신다면 Webframeworks.kr 의 ES6 Promises(1) – the API 포스트를 참고하세요.
자, 이제 routes/api/auth/auth.controller.js
파일을 열어서 상단에 User 모델을 require
해주고, 함수에 기능을 구현해봅시다.
/api/auth/auth.controller.js
const User = require('../../../models/user') /* POST /api/auth { username, password } */ exports.register = (req, res) => { const { username, password } = req.body let newUser = null // create a new user if does not exist const create = (user) => { if(user) { throw new Error('username exists') } else { return User.create(username, password) } } // count the number of the user const count = (user) => { newUser = user return User.count({}).exec() } // assign admin if count is 1 const assign = (count) => { if(count === 1) { return newUser.assignAdmin() } else { // if not, return a promise that returns false return Promise.resolve(false) } } // respond to the client const respond = (isAdmin) => { res.json({ message: 'registered successfully', admin: isAdmin ? true : false }) } // run when there is an error (username exists) const onError = (error) => { res.status(409).json({ message: error.message }) } // check username duplication User.findOneByUsername(username) .then(create) .then(count) .then(assign) .then(respond) .catch(onError) }
만약에 Promise 를 사용하지 않았더라면, 조금 더 nested 되어있고 복잡한 구조였겠죠.
코드를 조금 더 깔끔하게 작성하기 위하여 각 작업들을 따로 함수로 만든다음에 Promise chain 을 만들었습니다.
위 구조도, 보기에 꽤 괜찮긴하지만, ES6 보다 더 최신문법인 ES7 에서는 비동기작업을 더 깔끔하게 할 수 있는 async
& await
이라는 문법을 지원합니다. 하지만 그 문법을 사용하려면 babel 설정을 해야하기에.. 이 강의에선 Promise 만 사용을 했습니다. 나중에 시간이 있으시면 이 문법도 알아보시길 바랍니다. 정말 편하거든요. (자세한 내용은 여기서)
코드를 저장하고, Postman 으로 회원가입 요청을 넣어보세요.
오호.. 가입에 성공했다고합니다. 관리자로도 지정이 되었습니다. 한번 똑같은 요청을 다시한번 넣어보세요.
유저네임이 중복된다고 뜨네요. 이번엔 username 을 다른 값으로 설정해서 다시 요청을 해보세요
이번엔 관리자 지정이 되지 않았습니다. 저희가 작성한 코드가 제대로 작동을 하는군요.
#6 로그인 구현
로그인 API 준비하기
자, 이제 드디어 로그인을 하고 JWT 토큰을 발급받는 방법을 다뤄보도록 하겠습니다.
우선, 회원가입 API 를 만들때 했었던것처럼 로그인 API 도 라우팅을 위한 코드를 미리 작성하겠습니다.
로그인의 라우트는 POST /api/auth/login
으로 설정하겠습니다.
routes/api/auth/auth.controller.js
// (...) /* POST /api/auth/login { username, password } */ exports.login = (req, res) => { res.send('login api is working') }
routes/api/auth/index.js
const router = require('express').Router() const controller = require('./auth.controller') router.post('/register', controller.register) router.post('/login', controller.login) module.exports = router
로그인 API 기능 구현하기
자, 이제 login 함수의 기능을 구현해줄 차례입니다. jwt 토큰을 발급하려면, 강좌의 시작부분에서 npm 을 통해 설치한 jsonwebtoken
모듈을 사용해야합니다. 우선, 컨트롤러 파일의 상단에 해당 모듈을 불러와주세요
routes/api/auth/auth.controller.js
const jwt = require('jsonwebtoken') const User = require('../../../models/user') // (...)
jsonwebtoken 으로 JWT 발급하기
사용법:
jwt.sign(payload, secret, options, [callback])
만약에
callback
이 전달되면 비동기적으로 작동하며, 콜백함수의 파라미터는(err, token)
입니다. 전달되지 않을시엔 동기적으로 작동하며, JWT 를 문자열 형태로 리턴합니다.
payload
는 객체, buffer, 혹은 문자열형태로 전달 될 수있습니다.
secret
은 서명을 만들 때 사용되는 알고리즘에서 사용되는 문자열 혹은 buffer 형태의 값 입니다.
options
:
이제 login
함수 내에서 유저 정보를 확인하고, jwt 토큰을 발급해줍시다
routes/api/auth/auth.controller.js
// (...) /* POST /api/auth/login { username, password } */ exports.login = (req, res) => { const {username, password} = req.body const secret = req.app.get('jwt-secret') // check the user info & generate the jwt // check the user info & generate the jwt const check = (user) => { if(!user) { // user does not exist throw new Error('login failed') } else { // user exists, check the password if(user.verify(password)) { // create a promise that generates jwt asynchronously const p = new Promise((resolve, reject) => { jwt.sign( { _id: user._id, username: user.username, admin: user.admin }, secret, { expiresIn: '7d', issuer: 'velopert.com', subject: 'userInfo' }, (err, token) => { if (err) reject(err) resolve(token) }) }) return p } else { throw new Error('login failed') } } } // respond the token const respond = (token) => { res.json({ message: 'logged in successfully', token }) } // error occured const onError = (error) => { res.status(403).json({ message: error.message }) } // find the user User.findOneByUsername(username) .then(check) .then(respond) .catch(onError) }
여기에서도 마찬가지로 각 작업을 함수로 만든다음에, Promise chain을 만들어주었습니다. 함수의 내부에 check
함수는 유저의 정보를 확인하고 토큰을 발급해주는데요, 여기서 토큰을 비동기적을 만들기에, 내부에서 새로운 Promise 를 만들어 리턴을 하도록 설정하였습니다. 그러면 Promise 에서 이 함수가 실행되면, 로그인이 성공 했을 때 다음. then()
에서는 토큰값을 전달받겠지요.
코드를 저장하고, Postman 을 열어 다음과 같이 로그인 요청을 해봅시다.
토큰이 정상적으로 생성이 되었네요! 한번 이 토큰을 https://jwt.io/ 에 붙여넣어볼까요?
잘 만들어졌군요. (서명 부분에서 secret 부분에 우리가 config.js 파일에서 입력한 값을 넣어주어야 합니다)
#7 JWT 검증 구현
유저가 JWT 값을 헤더에 x-access-token 으로 설정하거나, url parameter 로 서버로 전달하면, 서버측에서 그 토큰을 가지고 검증 한 후 현재 계정의 상태를 보여주는 기능을 구현해보겠습니다.
계정정보 체킹 라우트는 GET /api/auth/check
로 설정하겠습니다.
routes/api/auth/auth.controller.js
/* GET /api/auth/check */ exports.check = (req, res) => { // read the token from header or url const token = req.headers['x-access-token'] || req.query.token // token does not exist if(!token) { return res.status(403).json({ success: false, message: 'not logged in' }) } // create a promise that decodes the token const p = new Promise( (resolve, reject) => { jwt.verify(token, req.app.get('jwt-secret'), (err, decoded) => { if(err) reject(err) resolve(decoded) }) } ) // if token is valid, it will respond with its info const respond = (token) => { res.json({ success: true, info: token }) } // if it has failed to verify, it will return an error message const onError = (error) => { res.status(403).json({ success: false, message: error.message }) } // process the promise p.then(respond).catch(onError) }
routes/api/auth/index.js
const router = require('express').Router() const controller = require('./auth.controller') router.post('/register', controller.register) router.post('/login', controller.login) router.get('/check', controller.check) module.exports = router
코드를 저장하고 테스팅을 해봅시다. 첫 시도에서는 아까 발급받은 토큰을 x-access-token
헤더값으로 지정하고 다음과 같이 요청을 날려보세요.
잘 되는군요. 두번째 시도는 기존 헤더 값을 지우고, 다음과 같이 url-encoded 쿼리 값을 설정하여 전달을 해보세요.
이 또한 잘 됩니다. 이번엔 토큰 값을 막무가내로 수정하고 요청을 해봅시다.
실패도 제대로 반환이 되네요.
#8 JWT 검증 미들웨어 구현
어느정도 감을 잡았죠? 그런데, 토큰을 필요로하는 모든 요청에 토큰검증 코드를 넣기엔 너무나 자주 반복되잖아요? 이 작업을 더 간단하게 하는 방법이 있습니다. 바로 미들웨어죠.
라우터에서 주어진 요청을 설정하기전에, JWT 검증 미들웨어를 통하여 JWT 검증 작업을 하고 나서, 주어진 작업을 하게 하도록 구현을 해봅시다.
middlewares/auth.js
파일을 열어 다음 코드를 작성해주세요.
middlewares/auth.js
const jwt = require('jsonwebtoken') const authMiddleware = (req, res, next) => { // read the token from header or url const token = req.headers['x-access-token'] || req.query.token // token does not exist if(!token) { return res.status(403).json({ success: false, message: 'not logged in' }) } // create a promise that decodes the token const p = new Promise( (resolve, reject) => { jwt.verify(token, req.app.get('jwt-secret'), (err, decoded) => { if(err) reject(err) resolve(decoded) }) } ) // if it has failed to verify, it will return an error message const onError = (error) => { res.status(403).json({ success: false, message: error.message }) } // process the promise p.then((decoded)=>{ req.decoded = decoded next() }).catch(onError) } module.exports = authMiddleware
사전에 작성했었던 check
함수와 비슷하죠? 이제 이 미들웨어를 GET /api/auth/check
라우트에 적용해보겠습니다.
/routes/api/auth/index.js
const router = require('express').Router() const controller = require('./auth.controller') const authMiddleware = require('../../../middlewares/auth') router.post('/register', controller.register) router.post('/login', controller.login) router.use('/check', authMiddleware) router.get('/check', controller.check) module.exports = router
그러면, auth.controller.js
안에 있는 check
함수를 이렇게 간단하게 수정하면됩니다. 만약에 토큰 검증에 실패 될 땐, 미들웨어에서 next()
함수가 실행되지 않기때문에, 이 check
함수 내부에서는 토큰이 검증실패했을때를 고려하지 않아도 된답니다.
/routes/api/auth/auth.controller.js
// (...) /* GET /api/auth/check */ exports.check = (req, res) => { res.json({ success: true, info: req.decoded }) }
여기까지 잘 따라오셨나요? 축하합니다! 이 강의에서 배울 핵심적인 내용을 다 배우셨습니다! 이 포스트 하단부로는 유저 인증 시스템의 추가적인 기능들을 구현하고, plain text
로 저장하던 비밀번호를 해싱하여 저장하도록 수정합니다.
Node.js 실습을 추가적으로 하고 싶다면 섹션 #9 와 섹션 #10 도 진행을 하시고, 그렇지 않다면 최하단의 마치면서.. 섹션으로 스크롤하세요.
#9 유저 인증 시스템 추가기능 구현
자, 이제 유저 인증 시스템에서 필요한 두가지의 추가 기능들을 구현해보도록 하겠습니다.
- 관리자 계정으로 모든 유저 리스팅
- 관리자 계정으로 특정 유저 관리자권한 부여
user 라우터 컨트롤러 코드 작성
이 라우터에서는 jwt 와 발급 / 검증 작업을 하지 않기 때문에 (미들웨어가 해줄것이기 때문이죠) 작성 할 코드는 꽤나 간단합니다.
routes/api/user/user.controller.js
const User = require ('../../../models/user') /* GET /api/user/list */ exports.list = (req, res) => { // refuse if not an admin if(!req.decoded.admin) { return res.status(403).json({ message: 'you are not an admin' }) } User.find({}) .then( users=> { res.json({users}) } ) } /* POST /api/user/assign-admin/:username */ exports.assignAdmin = (req, res) => { // refuse if not an admin if(!req.decoded.admin) { return res.status(403).json({ message: 'you are not an admin' }) } User.findOneByUsername(req.params.username) .then( user => user.assignAdmin ).then( res.json({ success: true }) ) }
user 라우터 코드 작성
routes/api/user/index.js
const router = require('express').Router() const controller = require('./user.controller') router.get('/list', controller.list) router.post('/assign-admin/:username', controller.assignAdmin) module.exports = router
user
라우터의 경우,라우터 내부의 모든 API 들이 JWT 토큰 검증이 필요하므로, 여기서 authMiddleware
적용하지 않고, routes/api/index.js
파일에서 적용합니다.
api 라우터 수정
api
라우터에서 방금 만든 user
라우터를 불러오고, 그 라우터에 authMiddleware
도 적용을 해줍니다.
/routes/api/index.js
const router = require('express').Router() const authMiddleware = require('../../middlewares/auth') const auth = require('./auth') const user = require('./user') router.use('/auth', auth) router.use('/user', authMiddleware) router.use('/user', user) module.exports = router
코드를 저장하고, 방금 만든 API들이 잘 작동하는지 체크해봅시다.
잘 되는군요. API 요청을 할 때, x-access-token
값 설정하는것 잊지 마세요!
#10 비밀번호를 암호화하여 저장하기
비밀번호를 데이터베이스에 저장 할때, plain text
그대로 저장하는것은 매우 위험합니다. 따라서 우리는 HMAC-SHA1 으로 비밀번호를 해쉬하여 저장하도록 하겠습니다.
그러기 위해선, 유저 모델에서 node.js 내장 모듈인 crypto
를 불러와서 사용하면 됩니다. 해싱을 할 때 사용 할 비밀키는 편의상 config 파일을 다시 불러와서 jwt 에서 사용하는 비밀키와 동일하게 사용하겠습니다.
create
메소드와 verify
메소드를 다음과 같이 수정해주세요.
models/user.js
const mongoose = require('mongoose') const Schema = mongoose.Schema const crypto = require('crypto') const config = require('../config') // (...) // create new User document User.statics.create = function(username, password) { const encrypted = crypto.createHmac('sha1', config.secret) .update(password) .digest('base64') const user = new this({ username, password: encrypted }) // return the Promise return user.save() } // (...) // verify the password of the User documment User.methods.verify = function(password) { const encrypted = crypto.createHmac('sha1', config.secret) .update(password) .digest('base64') return this.password === encrypted } // (...)
주의: 이전에 만든 계정들은 작동을 안할테니, 데이터베이스를 비워주시고 새로 가입을 하세요.
자, 이제 기존에 만들었던 API 들이 제대로 되는지 테스팅을 해보세요. 도중에 막히는게 있다면 소스코드를 참고해보시길 바랍니다.
마치면서..
이번 포스트에서는 JSON Web Token 을 사용하는 간단한 인증시스템을 구현해보았습니다. 다음 이어질 글에서는 JWT 를 사용 할 때의 보안 이슈, 토큰을 클라이언트측에서 어디에 저장해야 할 지 알아보도록 하겠습니다. 추후, React 어플리케이션에서 이 JWT 기반 인증 시스템을 연동하는 방법도 알아보겠습니다.