스터디/KAKAOCLOUDSCHOOL

[Node] 개발자 지망생 스터디 - 22일차

shineIT 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;​