ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Node] 개발자 지망생 스터디 - 22일차
    스터디/KAKAOCLOUDSCHOOL 2022. 12. 1. 22:23

    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을 메모리에 저장하면 서버가 종료되었다가 다시 실행되면 모든 세션이 소멸됨
    ☑︎ 메모리를 효율적으로 사용하고 종료되었다가 다시 부팅이 되었을 때도 기억하고자 하면 플랫 파일이나 데이터베이스를 이용함

    JavaScript async await

    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.js 

    const 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.js

     passport.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.js 

    const 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 -> ⚙️.env

    PORT=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.js 

    const 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.js

    const 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.js

    db.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.js

    const 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.js 

    const 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;​
Designed by Tistory.