Node.js

2024년4월30일 Node.js-Authentication

황수환 2024. 5. 1. 03:23
더보기

Authentication(인증)

1. session & cookie // 크리티컬한 정보 개입 X
쿠키
- 클라이언트 컴퓨터에 저장되는 작은 데이터  조각
- 서버로 부터 전송되어 클라이언트 웹 브라우저에 저장
- 텍스트 형식으로 주로 사용자 인증, 설정, 장바구니 등에 사용
세션
- 웹 서버 측에서 유지되는 상태 정보
- 사용자에 대한 고유한 세션ID를 통해 식별
- 서버 메모리 또는 데이터베이스에 저장할 수 있음

2. JWT(Json Web Token)
- 웹 애플리케이션과 서비스 간에 정보를 안전하게 전달하기 위한 인증 및 권한 부여 매커니즘을 구현하는 데 사용되는 표준화된 방법 중 하나
- JSON 포맷을 사용하여 정보를 표현하고 서명 및 암호화를 통해 정보의 무결성을 보장

    { Header | Payload | Signature }

Header: 토큰 유형 및 서명 알고리즘과 같은 메타데이터가 포함
Payload: 토큰에 포함될 데이터가 들어있는 부분
Signature: 헤더, 페이로드, 및 비밀 키를 사용하여 생성된 서명으로 토큰의 무결성을 검증하는 데 사용

3. bcrypt
- 해시 함수를 사용하여 비밀번호를 안전하게 저장하는 데 사용되는 암호화 라이브러리
- 단방향 해시 함수로 한 번 해시된 값을 다시 원래 값으로 역추적하는 것이 불가능
- 솔트(salt): 해시에 고유한 솔트 값을 추가하여 보안성을 높임
같은 비밀번호를 가진 사용자가 있더라도 서로 다른 해시값을 가짐
- 작업인자(Adaptive Work Factor): 매개변수를 조정하여 해시 작업의 복잡성을 조절
암호분석학적으로 안전한 해시 함수를 유지하면서도 암호화 작업에 필요한 시간을 조절할 수 있게 함

✔️해시함수
임의의 길이의 데이터를 받아서 고정된 길이의 고유한 값으로 변환하는 함수
이러한 변환된 값은 해시 값 또는 해시 코드라고 함
- 동일한 입력에 대해서 항상 동일한 해시 값을 생성
    1234 -> abcdefg
    1234 -> abcdefg
- 고정된 출력 길이를 생성
- 해시된 값을 통해 원본 값을 복구할 수 없음
    1234 -> abcdefg O
    abcdefg -> 1234 X

- 솔트 값도 같이 저장
    1234 + 10 -> abcdefghijk
    1234 + 5 -> abcdefghi

문제
controller/auth.js 에서 login()을 bcrypt를 적용하여 로그인 프로세스를 만들어보자

문제
jwt.js를 참고하여 controller/auth.js 에 토큰을 발행하고 login()에 로그인이 완료되면 클라이언트에 토큰을 출력하는 프로세스를 만들어보자

4. jsonwebtoken
- 웹 애플리케이션에서 인증 및 정보 교환을 위한 토큰 기반의 인증 방식 중 하나
- Base64로 인코딩된 JSON 객체이며 사용자 정보 및 기타 데이터를 안전하게 전송하기 위해 사용
- haeader: JWT의 유형과 해싱 알고리즘이 포함
    {
        "alg": "HS256",
        "typ": "JWT"
    }
- Payload: 토큰에 담길 정보가 포함
    {
        id:'apple',
        isAdmin: false
    }
- Signature: 헤더와 페이로드를 인코딩하고 비밀 키를 사용하여 서명된 문자열
서명은 토큰이 변조되지 않았음을 확인하는 데 사용

sign()
    jsonwebtoken.sgin(payload, secretOrPrivateKey, [options, callback])
    payload: JWT에 포함될 페이로드 데이터
    secretOrPrivateKey: JWT 서명하기 위해 사용될 비밀 키 또는 개인 키

중간에 문제있는걸 혼자서 풀어봐

 

 

1. controller-auth.js

import * as authRepository from '../data/auth.js'

export async function signup(req, res, next){
    const{username, password, name, email} = req.body
    const users = await authRepository.createUser(username, password, name, email)
    if(users){
        res.status(201).json(users)
    }
}

export async function login(req, res, next){
    const {username, password} = req.body
    const user = await authRepository.login(username)
    if(user){
        res.status(201).json(`${username} 로그인 완료`)
    }else{
        res.status(404).json({message: `${username}님 아이디 또는 비밀번호 확인하세요`})
    }
}

 

2. data-auth.js

let users = [
    {
        id: '1',
        username: 'apple',
        pasword: '1111',
        name: '김사과',
        email: 'apple@apple.com',
        url: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTJSRyel4MCk8BAbI6gT_j4DBTEIcY0WW4WWfoklymsWA&s'
    }, 
]

export async function createUser(username, password, name, email){
    const user = {
        id: '10',
        username,
        password,
        name,
        email,
        url: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTJSRyel4MCk8BAbI6gT_j4DBTEIcY0WW4WWfoklymsWA&s'
    }
    users = [user, ...users]
    return users
}

export async function login(username){
    const user = users.find((user) => user.username === username) // 정보 찾기
    return user
}

 

3. middleware-validator.js

import { validationResult } from "express-validator";

export const validate = (req, res, next) => {
    const errors = validationResult(req)
    if(errors.isEmpty()){
        return next()
    }
    return res.status(400).json({meessage: errors.array[0].msg})
}

 

4. router-tweet.js

import express from "express";
import * as tweetController from '../controller/tweet.js'
import {body} from 'express-validator'
import { validate } from "../middleware/validator.js"

const router = express.Router()

/* 
문제
Post, Put에 text에 대해 빈 문자열을 없애고, 최소 3자 이상 입력해야 데이터를 저장하도록 API에 적용
*/
const validateTweet = [
    body('text').trim().isLength({min:3}).withMessage('최소 3자 이사 입력'), validate
]

// 해당 아이디에 대한 트윗 가져오기
// GET
// http://localhost:8080/tweets?username=:username
router.get('/', tweetController.getTweets)


// 글번호에 대한 트윗 가져오기
// GET
// http://localhost:8080/tweets/:id
router.get('/:id', tweetController.getTweet);

// 트윗하기
// POST
// http://localhost:8080/tweets
// name, username, text
// json 형태로 입력 후 추가된 데이터까지 모두 json으로 출력
router.post('/', validateTweet,  tweetController.creatTweet);

// 트윗 수정하기
// PUT
// http://localhost:8080/tweets/:id
// id, username, text
// json 형태로 입력 후 변경된 데이터까지 모두 json으로 출력
router.put('/:id', validateTweet, tweetController.updateTweet);

// 트윗 삭제하기
// DELETE
// http://localhost:8080/tweets/:id
router.delete('/:id', tweetController.deleteTweet);

export default router;

 

4-1. router-auth.js

import express from "express";
import * as authController from '../controller/auth.js'
import {body} from 'express-validator'
import { validate } from "../middleware/validator.js"


const router = express.Router()

const validateSignup = [
    body('username').trim().isLength({min:3}).withMessage('최소 3자 이상 입력'),
    body('password').trim().isLength({min:4}).withMessage('최소 4자 이상 입력'),
    body('email').trim().isEmail().withMessage('이메일 형식 확인'), validate
]

router.post('/signup', validateSignup, authController.signup)

router.post('/login', authController.login)

export default router

 

이번엔 토큰을 생성해 볼꺼야

5. controller-auth.js

import * as authRepository from '../data/auth.js'
import jsonwebtoken from 'jsonwebtoken'

const secret = 'abcd1234%^&*'

async function makeToken(id){
    const token = jwt.sign({
        id: id,
        isAdmin: false
    }, secret, {expiresInL: '1h'})
    return token
}

export async function signup(req, res, next){
    const{username, password, name, email} = req.body
    const hashed = bcrypt.hashSync(password, 10)
    const users = await authRepository.createUser(username, hashed, name, email)
    if(users){
        res.status(201).json(users)
    }
}

// export async function login(req, res, next){
//     const {username, password} = req.body
//     const user = await authRepository.login(username)
//     if(user){
//         res.status(201).json(`${username} 로그인 완료`)
//     }else{
//         res.status(404).json({message: `${username}님 아이디 또는 비밀번호 확인하세요`})
//     }
// }

export async function login(req, res, next){
    const {username, password} = req.body
    const result = bcrypt.compareSync('abcd1234', hashed )
    if(user){
        if(bcrypt.compareSync(password, user.password)){
            res.status(201).json(`${username} 로그인 완료`)
            res.status(201).json('Token', makeToken(username)).json(`${username}로그인 완료`)
        }else{
            res.status(404).json({message: `${username}님 아이디 또는 비밀번호 확인하세요`})
        }    
    }else{
        res.status(404).json({message: `${username}님 아이디 또는 비밀번호 확인하세요`})
    }
}

export async function verify(req, res, next){
    const token = req.header['Token']
    if(token){
        res.status(200).json(token)
    }
}

 

6. data-auth.js

let users = [
    {
        id: '1',
        username: 'apple',
        pasword: '$2b$10$ygz4H1Txcf0xlB3Nj7wdYOf2tkrs23CLo30y1l9EbdRJti8dl47Ya',
        name: '김사과',
        email: 'apple@apple.com',
        url: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTJSRyel4MCk8BAbI6gT_j4DBTEIcY0WW4WWfoklymsWA&s'
    }, 
]

export async function createUser(username, password, name, email){
    const user = {
        id: '10',
        username,
        password,
        name,
        email,
        url: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTJSRyel4MCk8BAbI6gT_j4DBTEIcY0WW4WWfoklymsWA&s'
    }
    users = [user, ...users]
    return users
}

export async function login(username){
    const user = users.find((user) => user.username === username) // 정보 찾기
    return user
}

 

7. router/auth.js

import express from "express";
import * as authController from '../controller/auth.js'
import {body} from 'express-validator'
import { validate } from "../middleware/validator.js"


const router = express.Router()

const validateSignup = [
    body('username').trim().isLength({min:3}).withMessage('최소 3자 이상 입력'),
    body('password').trim().isLength({min:4}).withMessage('최소 4자 이상 입력'),
    body('email').trim().isEmail().withMessage('이메일 형식 확인'), validate
]

router.post('/signup', validateSignup, authController.signup)

router.post('/login', authController.login)

router.get('/me', authController.verify)

export default router

 

8. package.json

{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon app"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^5.1.1",
    "express": "^4.19.2",
    "express-validator": "^7.0.1",
    "jsonwebtoken": "^9.0.2",
    "morgan": "^1.10.0",
    "nodemon": "^3.1.0"
  }
}