2024년4월30일 Node.js-Authentication
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"
}
}