[Node] 개발자 지망생 스터디 - 22일차
7. 카카오 로그인 구현
1) 사용하는 모듈: passport-kakao
도큐먼트: https://www.passportjs.org/packages/passport-kakao/
2)카카오 로그인 사용을 위한 설정
> developers.kakao.com 에 접속해서 로그인
> 애플리케이션이 없으면 추가
> REST API 키를 복사: {KAKAO KEY}
> 플랫폼 등록: Web에 자신의 도메인 과 포트 번호를 추가
> 로그인 활성화
> 왼쪽 메뉴에서 카카오 로그인을 클릭하고 활성화 설정을 ON 으로 설정하고 하단의 Redirect URI를 설정
- http://localhost/auth/kakao/callback
> 왼쪽 메뉴에서 카카오 로그인 안의 동의항목을 클릭하고 수집할 항목을 설정
> .env 파일에 복사한 키를 저장
... KAKAO_ID = REST API KAKAO KEY
#Cookie & Session
☑︎ http 와 https 는 연결형 서비스이기는 하지만 한 번 요청(Request)을 전송하고 응답(Response)을 하면 접속이 해제됨
☑︎ 이전에 무엇을 했는지를 기억할 수 없기 때문에 이전에 수행한 내용이나 상태를 연속해서 사용하고자 하면 어딘가에 이 정보를 저장을 해야 함
☑︎ 이 정보를 클라이언트에 저장을 하고 서버에 전송하는 방법이 있고,
☑︎ 다른 하나는 서버에 저장을 하는 방법이 있음.
☑︎ 클라이언트에 저장을 하게 되면 유저가 조작이 가능함
☑︎ 클라이언트의 저장소는 쿠키와 로컬 저장소(로컬 스토리지, 세션 스토리지, Web SQL, Indexed DB)가 있음
☑︎ 쿠키에 저장을 하면 서버에 매번 자동으로 전송이 되지만 로컬 저장소의 내용은 자동으로 전송 되지 않음.
☑︎ 서버에 저장하는 방법을 세션에 저장한다고 함
☑︎ request 객체 안에는 session이라고 하는 속성이 있어서 이 속성안에 저장을 하는 것 임
☑︎ session이 만들어질 때는 하나의 키를 생성해서 클라이언트의 쿠키에 전송을 함
☑︎ 세션의 키를 확인하면 어떤 클라이언트의 세션인지 확인이 가능함
☑︎ session은 기본적으로 서버의 메모리에 저장이 됨
☑︎ 클라이언트의 접속 개수가 많아지면 session의 개수도 많아지고 session의 개수가 많아지면 서버가 사용할 수 있는 메모리의 양이 줄어들어서 성능이 저하될 수 있음
☑︎ session을 메모리에 저장하면 서버가 종료되었다가 다시 실행되면 모든 세션이 소멸됨
☑︎ 메모리를 효율적으로 사용하고 종료되었다가 다시 부팅이 되었을 때도 기억하고자 하면 플랫 파일이나 데이터베이스를 이용함
3)passport 디렉토리의 index.js를 수정
📁 passport -> 📄 index.js
const passport = require('passport');
//로컬 로그인 구현
const local = require('./localStrategy');
//카카오 로그인 구현
const kakao = require('./kakaoStrategy')
const User = require('../models/user');
module.exports = () =>{
//로그인 성공했을 때 정보를 deserializeUser 함수에게 넘기는 함수
passport.serializeUser((user,done)=>{
done(null,user.id);
});
// 넘어온 id에 해당하는 데이터가 있으면 데이터베이스에서 찾아서 세션에 저장
passport.deserializeUser((id,done)=>{
User.findOne({where:{id}})
.then(user => done(null,user))
.catch(err => done(err))
})
local();
kakao();
}
4)passport 디렉토리에 kakaoStrategy.js 파일을 생성하고 작성
📁 passport -> 📄 kakaoStrategy.js
const passport = require('passport');
const kakaoStrategy = require('passport-kakao').Strategy;
//유저 정보
const User = require('../models/user');
//카카오 로그인
module.exports = () => {
passport.use(new kakaoStrategy ({
clientID : process.env.KAKAO_ID,
callbackURL:'/auth/kakao/callback'
}, async(accessToken, refreshToken, profile, done) => {
//로그인 성공했을 때 정보를 출력
console.log('kakako profile', profile);
try{
// 이전에 로그인한 적이 있는지 찾기 위해서 카카오 아이이디와 provider가
// kakao로 되어 있는 데이터가 있는지 조회
const exUser = await(User.findOne({
where:{snsId:profile.id, provider:'kakao'}
}));
if(exUser){
done(null, exUser);
}else{
const newUser = await User.create({
email:profile._json.kakao_account.email,
nick:profile.displayName,
snsId:profile.id,
provider:'kakao'
});
done(null, newUser);
}
}catch(error){
console.error(error);
done(error);
}
}))
}
5)routes 디렉토리의 auth.js 파일에 카카오 로그인 라우팅 처리 코드를 추가
📁 routers -> 📄 auth.js
...
// 카카오 로그인을 눌렀을 때 처리
router.get('/kakao', passport.authenticate('kakao'));
// 카카오 로그인 실패 했을 때
router.get('/kakao/callback', passport.authenticate('kakao', {
failureRedirect:'/'
}), (req, res) => {
res.redirect('/')
});
module.exports = router;
8. 게시글 작업
#게시글 업로드를 위한 내용을 작성하기 위한 파일(post.js)을 routers 디렉토리에 생성
📁 routers -> 📄 post.js
const express = require('express'); // 파일 업로드를 위한 모듈 const multer = require('multer'); const path = require('path'); const fs = require('fs'); // 데이터 삽입을 위한 모듈 const {Post, Hashtag} = require('../models'); // 로그인 여부 판단 const {isLoggedIn} = require('./middlewares'); const router = express.Router(); // 파일을 업로드할 디렉토리가 없으면 생성 try{ fs.readdirSync('public/img'); }catch(error){ fs.mkdirSync('public/img'); } // 파일 업로드 객체 const upload = multer({ storage:multer.diskStorage({ destination(req, file, cb){ cb(null, 'public/img/'); }, filename(req, file, cd){ const ext = path.extname(file.originalname); cb(null, path.basename(file.originalname, ext) + Date.now() + ext); } }), limits:{fileSize: 10 * 1024 * 1024} }); // 이미지 업로드 router.post('/img', isLoggedIn, upload.single('img'), (req, res) =>{ console.log(req.file) res.json({ url:`/img/${req.file.filename}` }) }) // 게시글 업로드 const upload2 = multer(); router.post('/', upload2.none(), async(req,res,next)=>{ try{ // 게시글 업로드 const post = await Post.create({ content:req.body.content, img:req.body.url, UserId:req.user.id }) // 해시태그찾기 const hashtags = req.body.content.match(/#[^\s#]*/g); if(hashtags){ // 전부 실행 const result = await Promise.all( // 배열의 전체 데이터를 순서대로 대입해서 {} 안의 내용 수행 hashtags.map(tag => { return Hashtag.findOrCreate({ where:{ title:tag.slice(1).toLowerCase() } }) }) ); await post.addHashtags(result.map(r => r[0])); } res.redirect('/'); }catch(error){ console.error(error); next(error); } }); module.exports = router;
#page.js 파일을 수정(//메인화면 주석 구역 수정)해서 기본 요청이 왔을 때 작성한 게시글을 출력
> app.get("/") 부분을 수정
📁 routers -> 📄 page.js
const express = require('express'); const {isLoggedIn, isNotLoggedIn} = require('./middlewares'); const router = express.Router(); // 공통된 처리 - 무조건 수행 router.use((req,res,next)=>{ //로그인한 유저 정보 //유저정보를 res.locals.user에 저장 res.locals.user = req.user; //게시글을 follow 하고, 되고 있는 개수 res.locals.followCount = 0; res.locals.followingCount = 0; //게시글을 follow 하고 있는 유저들의 목록 res.locals.followIdList = []; next(); }) const {Post, User} = require("../models"); //메인 화면 router.get('/', async(req,res,next)=>{ try{ // Post 모델의 모든 데이터를 찾아오는데 // 이때 User 정보와 id, nick도 같이 가져오기 const posts = await Post. findAll({ include:{ model:User, attributes:['id', 'nick'] }, order:['createdAt', 'DESC'] }); res.render('main', { title:'NodeAuthentication', twitsss: posts }) }catch(error){ console.error(error); next(error); } }) /* router.get('/',(req,res,next)=>{ const twits = []; //템플릿 엔진을 이용한 출력 //views 디렉토리의 main.html 로 출력 res.render("main",{title:"Node Authentication",twits}); }) */ //회원 가입 - 로그인이 되어있지 않은 경우에만 수행 router.get('/join', isNotLoggedIn,(req,res,next)=>{ res.render('join', {title:'회원 가입 - Node Authentication'}); }) //프로필 화면 - 로그인 되어 있는 경우에만 수행 router.get('/profile', isLoggedIn,(req,res,next)=>{ res.render('profile', {title:'나의 정보 - Node Authentication'}); }) module.exports = router;
#App.js 파일에 post 라우팅 파일을 추가
📄 App.jsconst postRouter=require('./routes/post'); app.use('/post',postRouter);
9. 팔로우 처리
#팔로우 관련 처리를 위한 내용을 routes 디렉토리에 users.js 파일을 생성하고 작성
📁 routes -> 📄 users.js
const express = require('express'); const User = require('../models/user'); const {isLoggedIn} = require('./middlewares'); const router = express.Router(); router.post('/:id/follow', isLoggedIn, async(req, res, next)=>{ try{ //현재 로그인 한 유저를 찾습니다. const user = await User.findOne( {where:{id:req.user.id}}); if(user){ //팔로우로 추가 await user.addFollwing(parseInt(req.params.id, 10)); res.send('success'); }else{ res.status(404).send('no user'); } }catch(error){ console.error(error); next(error); } }) module.exports = router;
#passport 디렉토리의 index.js 파일에서 로그인 할 때 팔로우 정보를 가져오도록 passport.deserializerUser 메서드 수정
📁 passport -> 📄 index.jspassport.deserializeUser((id, done) => { User.findOne({where:{id}, include:[{ model:User, attributes:['id', 'nick'], as:'Followers' },{ model:User, attributes:['id', 'nick'], as:'Followings' }]}) .then(user => done(null, user)) .catch(err => done(err)); });
#routes 디렉토리의 page.js 파일에 수정
> 유저 정보를 초기화 하는 미들웨어 부분을 수정
📁 routes -> 📄 page.js
const {Post, User, Hashtag} = require("../models"); //공통된 처리 - 무조건 수행 router.use((req, res, next) => { //로그인한 유저 정보 //유저정보를 res.locals.user에 저장 res.locals.user = req.user; //게시글을 follow 하고 되고 있는 개수 res.locals.followCount = req.user?req.user.Followers.length:0; res.locals.followingCount = req.user?req.user.Followings.length:0; //게시글을 follow 하고 있는 유저들의 목록 res.locals.followerIdList = req.user?req.user.Followings.map(f=>f.id):[]; next(); })
> hashtag를 가져오는 요청을 처리
router.get('/hashtag', async(req, res, next) => { //파라미터 읽어오기 const query = req.query.hashtag; if(!query){ return res.redirect('/'); } try{ const hashtag = await Hashtag.findOne({where:{title:query}}); let posts = []; if(hashtag){ posts = await hashtag.getPosts( {include:[{model:User}]}); } return res.render('main', { title:`${query} | NodeAuthentication`, twits:posts}) }catch(error){ console.error(error); return next(error); } })
#App.js 파일에 users 라우팅 파일을 사용할 수 있도록 추가
📄 App.jsconst userRouter =require('./routes/users'); app.use ('/user', userRouter);
API Server
1. API(Application Programming Interface)
- 프로그램과 프로그램을 연결시켜주는 매개체
- 다른 애플리케이션을 개발할수 있도록 도와주는 프로그램(Software, Development Kit) 또는 데이터
JDK-Java software Development Kit
Sony SDK - Sony 디바이스의 애플리케이션을 만들 수 있도록 도와주는 프로그램
Win API - Windows Application을 만들기 위한 함수(C)의 집합
프로그램 개발에 도움을 주도록 또는 여러 프로그램에서 공통으로 사용되어야 하는 데이터가 있는 경우에는 프로그램이 아니라 데이터를 제공
- 누구나 등록만 하면 사용할 수 있도록 API를 만들면 Open API라고 함
- 데이터를 제공할 때는 데이터베이스에 직접 접근하도록 하는 것이 아니라 애플리케이션 서버를 통해 제공
2. API Server가 제공하는 데이터 포맷
1) txt 또는 csv
- 일반 텍스트로 구분기호를 포함하는 경우가 있음
- 변하지 않는 데이터를 제공하는데 주로 이용
- 가끔 txt 나 csv대신에 excel이나 hwp 또는 pdf로 제공하는 경우가 있음
2) xml
- eXtensible Markup Language : 태그의 해석을 브라우저가 아닌 개발자 또는 개발자가 만든 라이브러리가 하는 형태로 문법이 HTML보다는 엄격함
- HTML은 데이터로 사용하기에는 부적합 - HTML은 구조적이지 못하기 때문
- 아직도 설정 파일이나 데이터를 제공하는 용도로 많이 사용함
3) json
- 자바스크립트 객체 형태로 표현하는 방식
- XML 보다 가볍기 때문에 데이터 전송에 유리함
- 자바스크립트 객체 표현법으로 데이터를 표현하기 때문에 JavaScript나 Python에서는 파싱하는 것이 쉬움
- 설정보다는 데이터를 제공하는 용도로 많이 사용
- Apple, Google, Twitter 등은 데이터 전송에는 json만 사용
4) yaml
- email 표기 형식으로 표현하는 방식
- 계층 구조를 가진 데이터 표현에 유리
- 구글의 프로그램들이 설정을 할 때는 yaml(확장자는 yml-야믈)을 많이 이용
3. API Server를 만들기 위한 기본 설정
#프로젝트 생성
📁 apiserver
#필요한 패키지 설치
> npm install express dotenv compression morgan file-stream-rotator multer cookie-parser express-session express-mysql-session mysql2 sequelize sequelize-cli nunjucks passport passport-kakao passport-local bcrypt uuid
> npm install --save-dev nodemon
💡 패키지 INFO
> bcrypt : 복호화가 불가능한 암호화를 위한 모듈 - 비밀번호를 저장할 목적
> uuid : 랜덤한 문자열을 생성하기 위한 모듈 - 키를 발급할 목적
> --save-dev : 배포될때는 이 패키지가 제외(개발할때는 사용하지만 배포를 할때는 쓰지 않음)
#pakage.json 수정
"scripts": { "start": "nodemon app", "test": "echo \"Error: no test specified\" && exit 1" },
# 이전 프로젝트에서 routes, models, config, passport 디렉토리 복사
🗂 authentication ->(COPY)📁 routes, models, config, passport
🗂 apiserver -> (PASTE)📁 routes, models, config, passport
# .env 파일을 만들고 작성
🗂 apiserver -> ⚙️.envPORT=8000 COOKIE_SECRET=authentication HOST='localhost' MYSQLPORT=3306 USERID='root' PASSWORD='qlxkals' DATABASE='itorigin' KAKAO_ID = 🔑KAKAO KEY
# 프로젝트에 화면에 출력되는 파일을 저장하기 위한 디렉토리를 생성 - views
🗂 apiserver -> 📁 views
# 에러가 발생했을 때 화면에 출력될 파일을 views 디렉토리에 생성하고 저장 - error.html
📁 views-> 📄 error.html<h1>{{message}}</h1> <h2>{{error.status}}</h2> <pre>{{error.stack}}</pre>
> message는 우리가 전달하는 문자열
> error.status는 에러코드
> error.stack(stack tree)는 에러가 발생하면 에러가 발생한 부분에서 호출되는 함수를 역순으로 출력함
에러를 해결할 때는 맨 위에서부터 아래로 내려오면서 자신이 작성한 코드가 있는 부분을 찾아야 함
그 부분을 수정하는데 그 부분에서 다른 코드를 호출하면 순서대로 역추적해 나가야 함
#App.js 파일을 만들고 기본 설정 코드를 작성
🗂 apiserver -> 📄 App.jsconst express = require('express'); const dotenv = require('dotenv'); dotenv.config(); //서버 설정 const app = express(); app.set('port', process.env.PORT); //로그 출력을 위한 파일 과 경로를 위한 모듈 설정 const fs = require('fs'); const path = require('path'); //static 파일의 경로 설정 app.use(express.static(path.join(__dirname, 'public'))); //view template 설정 const nunjucks = require('nunjucks'); app.set('view engine', 'html'); nunjucks.configure('views', { express:app, watch: true, }); const morgan = require('morgan'); const FileStreamRotator = require('file-stream-rotator'); const logDirectory = path.join(__dirname, 'log'); // 로그 디렉토리 생성 fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory); // 로그 파일 옵션 설정 const accessLogStream = FileStreamRotator.getStream({ date_format: 'YYYYMMDD', filename: path.join(logDirectory, 'access-%DATE%.log'), frequency: 'daily', verbose: false }); // 로그 설정 app.use(morgan('combined', {stream: accessLogStream})); //출력하는 파일 압축해서 전송 const compression = require('compression'); app.use(compression()); //post 방식의 파라미터 읽기 var bodyParser = require('body-parser'); app.use( bodyParser.json()); // to support JSON-encoded bodies app.use(bodyParser.urlencoded({ // to support URL-encoded bodies extended: true })); //쿠키 설정 const cookieParser = require('cookie-parser'); app.use(cookieParser(process. env.COOKIE_SECRET)); //세션 설정 const session = require("express-session"); var options = { host :process.env.HOST, port : process.env.MYSQLPORT, user : process.env.USERID, password : process.env.PASSWORD, database : process.env.DATABASE }; const MySQLStore = require('express-mysql-session')(session); app.use( session({ secret: process.env.COOKIE_SECRET, resave: false, saveUninitialized: true, store : new MySQLStore(options) }) ); const {sequelize} = require('./models'); sequelize.sync({force: false}) .then(() => { console.log('데이터베이스 연결 성공'); }) .catch((err) => { console. error(err); }); const passport = require('passport'); const passportConfig = require('./passport'); passportConfig(); app.use(passport.initialize()); app.use(passport.session()); //라우터 설정 const authRouter = require('./routes/auth'); app.use ('/auth',authRouter); app.use('/img', express.static(path.join(__dirname, 'uploads'))); //에러가 발생한 경우 처리 app.use((req, res, next) => { const err = new Error(`${req.method} ${req.url} 라우터가 없습니다.`); err.status = 404; next(err); }); //에러가 발생한 경우 처리 app.use((err, req, res, next) => { res.locals.message = err.message; res.locals.error = process.env.NODE_ENV !== 'production' ? err : {}; res.status(err.status || 500); res.render('error'); }); app.listen(app.get('port'), () => { console.log(app.get('port'), '번 포트에서 대기 중'); });
#실행해서 설정이 제대로 되었는지 확인
4. 도메인을 등록해서 등록한 도메인에서만 API 요청이 이루어지도록 도메인과 키를 생성해서 저장
[💡] free, premium 두가지만 구분하고자 하는 경우 자료형
> boolean : true와 false를 이용해서 구분 가능
> int : free와 premium을 0과 1 또는 1과 2 형태로 구분 가능
> string : free와 prmium을 문자열로 저장해서 구분
ENUM : 정해진 데이터만 삽입이 가능함
JAVA에서는 Enum,
DataBase에서는 check 제약조건이라 함
type varchar(100) (check type in('free', 'premium'))
#models 디렉토리에 위의 정보(host-클라이언트 URL, clientSecret-키, type-free, premium)를 저장할 모델을 생성
🗂 apiserver -> 📁 models-> 📄 domains.js
const Sequelize = require('sequelize'); module.exports = class Domain extends Sequelize.Model { static init(sequelize) { return super.init({ host:{ type:Sequelize.STRING(100), allowNull:false }, clientSecret:{ type:Sequelize.STRING(36), allowNull:false }, type:{ type:Sequelize.ENUM('free', 'premium'), allowNull:false } }, { sequelize, timestamps: true, underscored: false, modelName: 'Domain', tableName: 'domains', paranoid: true }); } static associate(db) { //User 와 Domain 은 1:N //User의 기본키가 Domain에 외래키로 추가됨 db.Domain.belongsTo(db.User); } };
#models 디렉토리의 index.js 파일에 Domain 사용을 위한 설정을 추가
📁 models-> 📄 index.jsconst Sequelize = require('sequelize'); const env = process.env.NODE_ENV || 'development'; const config = require('../config/config')[env]; const User = require('./user'); const Post = require('./post'); const Hashtag = require('./hashtag'); const Domain = require('./domain'); const db = {}; const sequelize = new Sequelize( config.database, config.username, config.password, config, ); db.sequelize = sequelize; db.User = User; db.Post = Post; db.Hashtag = Hashtag; db.Domain = Domain; User.init(sequelize); Post.init(sequelize); Hashtag.init(sequelize); Domain.init(sequelize); User.associate(db); Post.associate(db); Hashtag.associate(db); Domain.associate(db); module.exports = db;
#models 디렉토리의 user.js 파일의 associate 함수 안에 관계 추가
📁 models-> 📄 users.jsdb.User.hasMany(db.Domain);
#저장 한 후 서버를 실행하면 데이터베이스에 테이블이 생성
#views 디렉토리에 로그인을 화면 출력을 작성 - login.html
📁 views-> 📄 login.html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>API 서버 로그인</title> <style> .input-group label { width: 200px; display: inline-block; } </style> </head> <body> <!-- 로그인 된 경우 출력 --> {% if user and user.id %} <span class="user-name">안녕하세요! {{user.nick}}님</span> <a href="/auth/logout"> <button>로그아웃</button> </a> <fieldset> <legend>도메인 등록</legend> <form action="/domain" method="post"> <div> <label for="type-free">무료</label> <input type="radio" id="type-free" name="type" value="free"> <label for="type-premium">프리미엄</label> <input type="radio" id="type-premium" name="type" value="premium"> </div> <div> <label for="host">도메인</label> <input type="text" id="host" name="host" placeholder="ex) AdamSoft.com"> </div> <button>저장</button> </form> </fieldset> <table> <tr> <th>도메인 주소</th> <th>타입</th> <th>클라이언트 비밀키</th> </tr> <!-- 기존에 등록한 도메인이 있는 경우 출력 --> {% for domain in domains %} <tr> <td>{{domain.host}}</td> <td>{{domain.type}}</td> <td>{{domain.clientSecret}}</td> </tr> {% endfor %} </table> <!-- 로그인이 안된 경우 --> {% else %} <form action="/auth/login" id="login-form" method="post"> <h2>NodeSNS 계정으로 로그인하세요.</h2> <div class="input-group"> <label for="email">이메일</label> <input id="email" type="email" name="email" required autofocus> </div> <div class="input-group"> <label for="password">비밀번호</label> <input id="password" type="password" name="password" required> </div> <button id="login" type="submit" class="btn">로그인</button> <a id="join" href="http://localhost/join" class="btn">회원가입</a> <a id="kakao" href="http://localhost/auth/kakao" class="btn">카카오톡</a> </form> <script> //에러 메시지 출력 //이 페이지로 넘어올 때 loginError 를 가지고오면 //대화상자로 출력 window.onload = () => { if (new URL(location.href).searchParams.get('loginError')) { alert(new URL(location.href).searchParams.get('loginError')); } }; </script> {% endif %} </body> </html>
#로그인 처리 와 도메인 등록 처리를 위한 내용을 routes 디렉토리의 index.js 파일에 작성
📁 routes-> 📄 index.jsconst express = require('express'); const {v4:uuidv4} = require('uuid'); const {User, Domain} = require('../models'); const {isLoggedIn} = require('./middlewares'); const router = express.Router(); router.get('/', async(req, res, next) => { try{ //로그인 한 유저가 있으면 유저의 모든 데이터를 찾아서 //대입 const user = await User.findOne({ where:{id:req.user && req.user.id || null}, include:{model:Domain} }); res.render('login', { user, domains:user && user.Domains}) }catch(error){ console.error(error); next(error); } }); //도메인 등록 처리 router.post('/domain', isLoggedIn, async(req, res, next) => { try{ await Domain.create({ UserId:req.user.id, host:req.body.host, type:req.body.type, clientSecret:uuidv4() }); //삽입하고 메인 페이지로 이동 res.redirect("/"); }catch(error){ console.error(error); next(error); } }) module.exports = router;
#App.js 파일에 routes 디렉토리의 index.js 파일을 사용할 수 있도록 설정
🗂 apiserver -> 📄 App.jsconst indexRouter = require('./routes'); app.use ('/',indexRouter);
5.JWT(JSON Web Token)
https://jwt.io
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io
> > JSON 형식 데이터 구조로 표현한 토큰
> > API Server 나 로그인을 이용하는 시스템에서 매번 인증을 하지 않고 서버와 클라이언트가 정보를 주고 받을 때
> > HttpRequest Header에 JSON 토큰을 넣은 후 인증하는 방식
> > HMAC 알고리즘을 사용하여 비밀키나 RSA기법을 이용해서 Public Key와 Private Key를 이용해서 서명
:: 구성
> HEADER : 토큰 종류와 해시 알고리즘 정보
> PAYLOAD : 토큰의 내용물이 인코딩 된 부분
> SIGNTURE : 토큰이 변조되었는지 여부를 확인할 수 있는 부분
클라이언트가 서버에게 데이터를 요청할 때 키와 domain을 json token에 포함시켜 전송하고 서버는 이를 확인하여 유효한 요청인지 판단하고 데이터를 전송
기본적으로 쿠키는 동일한 도메인 내에서만 읽을 수 있음
서버와 클라이언트 애플리케이션의 도메인이 다르면 쿠키는 사용할 수 없음
설정을 하면 서로 다른 도메인 간에도 쿠키를 공유할 수 있지만 위험함
서버와 클라이언트 애플리케이션의 도메인이 다른 경우는 세션을 이용해서 사용자 인증을 할 수 가 없음
이 경우, 서버에서 클라이언트에게 키를 발급하고 클라이언트는 서버에 요청을 할 때 키를 전송을 해서 인증된 사용자라는 것을 알려주어야 함
키를 평문으로 전송하게 되면 중간에 가로채서 사용할 수 있음
키와 클라이언트 URL을 합쳐서 하나의 암호화된 문자을 생성해서 전송을 하게되면 서버는 이를 해독하고 키와
1) 노드에서 JWT 인증을 위한 모듈을 설치
> npm install jsonwebtoken
2) JWT 생성에 필요한 문자 코드를 .env 파일에 생성
암호화 키와 해독키가 한 쌍
암호화 키와 해독키를 다르게 생성 - 암호화 키는 누구나 알수 있는 형태로 공개를 하지만 해독키는 비밀로 하는 방식
🗂 apiserver -> ⚙️.env
PORT=8000 COOKIE_SECRET=authentication HOST='localhost' MYSQLPORT=3306 USERID='root' PASSWORD='qlxkals' DATABASE='itorigin' KAKAO_ID = 🔑 JWT_SECRET = jwtSecret
3) routes 디렉토리의 middlewares.js 파일에 JWT 인증을 위한 미들웨어 함수를 추가
📁 routes-> 📄 middlewares.js
const jwt = require('jsonwebtoken'); exports.verifyToken = (req, res, next) => { try{ //토큰 확인 req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SCRET); //인증에 성공하면 다음 작업 수행 return next() }catch(error){ if(error.name === 'TokenExpiredError'){ return res.status(419).json({ code:419, message:'토큰이 만료되었습니다.' }); } return res.status(401).json({ code:401, message:"유효하지 않은 토큰입니다." }) } }
> 401 - 에러가 권한이 없음을 나타내는 에러 코드 번호
4) routes 디렉토리에 토큰을 발급하는 처리를 수행해주는 v1.js 파일을 생성하고 작성
📁 routes-> 📄 v1.js
const express = require('express'); const { verifyToken } = require('./middlewares'); const router = express.Router(); // 토큰을 확인해 보기 위한 처리 router.get('/test', verifyToken, (req, res) => { res.json(req.decoded); }) module.exports = router;