Cute Bow Tie Hearts Blinking Pink Pointer

백엔드/Node.js

[nodejs]로그인 시 JWT 생성하기(인증)

청포도 에이드 2022. 3. 3. 18:01
728x90

목차

 

- 자동로그인이 가능해지는 과정

- JWT 토큰 생성 코드

     1. jwt.js 파일 - 토큰생성

     2. server.js파일 - 서버 연결 및 토큰으로 로그인

     3. auth.js 파일 - 사용자 인증

- JWT 만료

 

자동로그인이 가능해지는 과정

 

  1. 사용자 회원가입 시, 아이디와 비밀번호, 주소, 번호 등 여러 정보가 생성된다.
  2. 이 때 사용자의 비밀번호를 암호화 해서 Database에 저장한다. 나머지 정보도 같이 DB에 저장된다.
  3. 사용자가 로그인 시, 아이디와 비밀번호 입력한다.
  4. 사용자가 입력한 비밀번호를 암호화 한 후, DB의 암호화된 비밀번호와 비교한다.
  5. 일치하면 로그인 성공, 일치하지 않으면 로그인 실패.
  6. 로그인에 성공한다면 access_token을 사용자 컴퓨터(클라이언트)에 전송(f12 애플리케이션 쿠키에서 확인가능)
  7. 유저는 로그인 성공후 다음부터는 access_token을 첨부해서 request를 서버에 전송함. (자동 로그인 완성)

 

JWT 생성하는 코드

 

1. jwt.js 파일

 

코드가 길기때문에 분리해서 설명하겠음.

const crypto = require('crypto')
const salt = 'ingoo'

crypto라는 모듈을 끌어와서 암호화해줄 것이다.(단방향 암호화)

salt(암호화를 한다고 해도 늘 암호를 해독할 위험이 도사리고 있기때문에... 암호화를 조금 더 복잡하게 해줄 수 있는 장치이며 안에 들어갈 단어는 사용자 마음임. 나는 교수님 성함을 사용함ㅋ)

 

function createToken(state){
    // JWT 필요한 필수요소 header . payload . signature 
    const header = {
        tpy:"JWT",
        alg:"HS256"
    }

    const payload = {
        ...state
    }

    const encodingHeader = encoding(header)
    const encodingPayload = encoding(payload)
    const signature = createSignature(encodingHeader,encodingPayload)

    return `${encodingHeader}.${encodingPayload}.${signature}`
}

 

createToken이라는 함수를 만들 것이다. 이 함수로 JWT(토큰)을 만든다.

JWT에는 header, payload, signature가 필요한데, 그것을 하나씩 만드는 코드이다.

encodingHeader, encodingPayload, signature 역시 전부 함수화 해주었다. (한번만 사용하는 것이 아니기때문)

그리고 return으로 header, payload, signature를 합쳐 JWT 토큰형태로 합쳐서 결과값을 내보내준다.

이 때 사용한 함수들은 이 코드 아래에 작성함.

 

function encoding(value){
    return Buffer.from(JSON.stringify(value))
                 .toString('base64')
                 .replace(/[=]/g,'')
}

header, payload를 암호화주는 함수이다. JSON 형식의 값을 string으로 변경해준후, base64를 사용해 64진수로 변경한다. 암호화하는 과정에서 " = " 이라는 쓸모없는 값이 생기기 때문에 정규화식으로 " = " 를 제거해준다.

 

function createSignature(header,payload){
    // encoding한 header와
    // encoding한 payload 
    // 그 값을 가지고 SHA256 만든다. salt 
    // encoding -> base64 
    const encoding = `${header}.${payload}`
    const signature = crypto.createHmac('sha256',salt)
                            .update(encoding)
                            .digest('base64')
                            .replace(/[=]/g,'')
    return signature
}

createSignature라는 함수에서는 암호화한 header, payload를 가지고 sha256라는 해싱함수를 사용한다. sha256은 salt값과 함께 해싱을 하고, 해싱된 결과물을 비밀키와 함께 다시한번 해싱을 한다. update라는 메소드로 이과정이 이루어진다.

이렇게 함으로써, 해시의 보안이 강화된다.

 

module.exports = {
    createToken,
    createSignature
}

위 함수들을 exports로 다른 파일에서도 사용할 것이다.

 

2. server.js파일

 

const express = require('express')
const nunjucks = require('nunjucks')
const { user } = require('./models/user')
const { createToken } = require('./utils/jwt')
const { auth } = require('./middlewares/auth')
const app = express()

express, nunjucks를 npm install로 다운받은 뒤, 불러와준다.

user라는 객체에 js파일로 임의의 사용자(아무) 정보를 담아두었다.

createToken과 auth 는 아래에서 설명하겠다.

 

app.set('view engine','html')
nunjucks.configure('views',{
    express:app,
    watch:true, // nodemon 사용하기위해서 npm install chokidar
})

app.use(express.urlencoded({extended:true,})) // http body영역을 해석해주는아이 Content-type : application/x-www-form-urlencoded
app.use(auth)

app.get('/',(req,res)=>{
    res.send('hello server111')
})

app.get('/user',(req,res)=>{
    res.render('index')
})

app.get('/login',(req,res)=>{
    res.render('login')
})

 

app.set('view engine' ... )으로 html 파일을 연결하였다.

마찬가지로, nunjucks.configure로 html에서 백엔드에서 사용하는 변수를 사용할 수 있도록 연결하였다.

app.use(express.urlencoded({extended:true,})) 로 http body 안의 정보를 우리 눈에 한 눈에 보이도록 (해석해줌) 하기위해 입력한다.

나머지 코드는 단순히 메인페이지, 유저페이지, 로그인페이지의 라우터 경로 설정이다.

 

 

아래의 중괄호가 닫히기 전까진 app.post안의 코드인데, 너무 길기때문에 나누어서 설명하겠다.

app.post('/login',(req,res)=>{
    const { userid ,userpw } = req.body
    const [ item ] = user.filter( v => v.userid == userid && v.userpw == userpw )

 

req.body에 있는 정보(아마 input박스에 사용자가 입력한 내용이겠죠?)를 userid, userpw라는 변수에 담는다.

user라는 객체(임의로 만들어놓은 사용자 데이터)에 filter를 사용하여 입력한 정보가 이미 존재하는 정보를(있는 정보인지) item이라는 배열에 담는다. (있어야 로그인이 가능함)

 

    try {
        if(item === undefined) throw new Error('item undefined')

        // 로그인 성공적으로 되어야함

        // 1. JWT 토큰을 생성
        //      JWT 토큰을 생성하기위해서 필요한 값이 무엇일까?  payload에 해당되는 값 Object
        //      객체 필요하구나! 
        //      JWT 토큰을 만드는 함수를 만들자!

try catch문 사용.

 

try 안의 값이 참이라면, 그 안의 구문을 수행하고

만약 error가 발생한다면 catch 영역에서 구문을 수행한다.

 

item 이 undefined라면(일치하는 정보가 없다면), Error를 생성해라. 그 때  error 의 구문은 item undefined이다.

 

 

// 이런건 저도 만들때 보고만듬 JWT 만들기 <<
        const payload = {
            userid:item.userid,
            username:item.username,
            level:1
        }

 

JWT토큰 안에 넣어줄 payload의 정보를 적어주는 것이다.

item안의 userid, username을 payload에 담는다.

        const token = createToken(payload)
        // JWT 토큰이 잘 만들어졌는지. 확인하는 작업을 할거에요. 표준에 맞춰어져있는지. 
        // https://jwt.io 
        console.log(token)
        // 2. 생성한 토큰을 쿠키로 생성해서 보내주어야 합니다.
        res.setHeader('Set-cookie',`AccessToken=${token}; HttpOnly; Secure; Path=/;`)
        res.redirect('/')
    } catch (e) {
        console.log(e)
        res.status(500).send('실패')
    }

    console.log(item)
    // request message  HTTP
    // response message HTTP  200 OK http/1.1
})

token을 생성해준다.

생성된 토큰을 쿠키로 생성해서 던져준다.

HttpOnly : httpOnly Cookie는 client-side에서 데이터에 접근하는 것을 막는다. 특히 서버를 제외하고 다른 곳에서의 접근을 막는 쿠키이다.

 

만약, 입력한 정보가 DB에 없다면, 500이라는 오류 번호를 보내주고, '실패'라는 글자를 랜더한다.

app.get('/admin',(req,res)=>{
    try{
        if( req.user === undefined ) throw new Error('로그인한 사용자만 이용 가능합니다.')
        console.log(req.user)
        res.send('관리자 페이지')
    } catch( e ) {
        res.send('로그인을 해주세요.')
    }
})

app.listen(3000,()=>{
    console.log('서버 시작')
})

admin 관리자 페이지도 login페이지와 같은 맥락이다.

 

 

3. auth.js 파일 (인증하는 곳)

 

const { createSignature } = require('../utils/jwt')

exports.auth = (req,res,next) => {
    // 1. code
    const cookies = req.headers.cookie
   

    // 토큰이 썩었는지 안썩었는지 확인할려면 어떤 로직을 구현해야하는지
    // 1. token 잇는 . 기준으로 내용을 뽑아온다. [header,payload,sign] OK
    // 2. 가져온 header, payload 가지고 새로운 signature 를 만듭니다. 
    // 3. 새로운 signature 와 기존의 sign 과 비교를 해서 같은지 아닌지를 판다합니다.
    //    이때 같으면 훌륭한 쿠키 같지 않으면 썩은 쿠키

cookies라는 변수에 http header의 cookie값을 담는다.

 

    try {
        const [[,token]] = cookies.split(';').map(v=>v.trim().split('=')).filter(v=>v[0]=='AccessToken')
        const [header,payload,sign] = token.split('.')
        // 2. code
        const signature = createSignature(header,payload)
        // 3. code
        if (signature !== sign) throw new Error('Token error')

        // 3.1 payload에 대한 내용을 decoding 해서 가져온다 ( base64 -> Object )
        const user = JSON.parse(Buffer.from(payload,'base64').toString('utf-8'))
        // 3.2 req객체에다가 user를 추가해서 보냈다. 
        req.user = {
            ...user
        }
    } catch (e) {
        console.log(e)
    }

    next()
}

token이라는 값에 AccessToken 값만 담아줄 것이다. (그냥 console.log 찍어보면 됨)

 

이 token을 " . "을 기준으로 나누어 다시 header, payload, sign으로 분리해준다.

 

이 때, 쿠키에 등록되어있던 sign와 우리가 생성한 signature 값이 같은지 확인한다.

 

같다면 http body에서 암호화 된 payload값을 해독하여 사용한다.

 

 

JWT 만료

 

payload 값 안에 expires 를 사용하여 만료기한(토큰)을 설정할 수도 있다.

 

참고하면 좋을 사이트

https://velog.io/@_woogie/JWT-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EB%B0%A9%EC%8B%9D-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-feat.-session%EC%97%90%EC%84%9C-jwt%EB%A1%9C

 

JWT 로그인방식 구현하기 (feat. session에서 jwt로)

Session에서 Cool해보이는 JWT로 바꾸며 겪은 일에 대해 적어보았습니다

velog.io

 

728x90