본문 바로가기

4. Node.js | React.js

5/8(월) IT K-DT(46일차) / twitter 예제(2)

 

6. auth 기능 실습

 

6-1. GET, POST 기능에 사용자 인증 기능 추가

Server 폴더 內 controller, data, router, middleware 각 폴더에 'auth.js' 파일 생성

 

C:\yjcho\Node.js\Project\Server\controller\auth.js

경로에 해당 내용 추가/수정.


  import jwt from 'jsonwebtoken';
  import bcrypt from 'bcrypt'
  import * as userRepository from '../data/auth.js';


  const jwtSecretKey = 'Eh$nZnt8Qz&IUE*Tb3cV90wC43Ea$6T0'; // 임의의 32bit의 비밀키를 가져옴.
  const jwtExpiresInDays = '2d' // 토큰의 만료기간은 2일.
  const bcryptSaltRounds = 10;  // 비밀번호의 해시화 반복횟수 10회

  1)
  export async function signup(req, res) {              // '회원가입' 을 처리하는 함수.

      const { username, password, name, email, url } = req.body;         // 회원정보를 req.body에서 추출.
      const sign_id = await userRepository.findByUsername(username);  // 추출한 정보를 userRepository에 저장.
      const hashed = await bcrypt.hash(password, bcryptSaltRounds);  // 비밀번호는 해시화를 진행.
      const userId = await userRepository.createUser({
         username,
         password: hashed,
         name,
         email,
         url
      });
      const token = createJwtToken(userId); // createJwtToken()의 선언: data와 상관이 없는 함수이므로, 최하단에 작성.
     
      if (sign_id) {

          res.status(409).json({message:`${username}은 이미 가입됨.`});  // 이미 존재하는 id인 경우, 409 error 반환.
      }
      res.status(201).json({token, username}); // JWT 토큰을 생성하여 응답.
    

 2)
 export async function login(req, res) {              // '로그인' 을 처리하는 함수.

    const {username, password} = req.body         // id, password를 req.body에서 추출.
    const user = await userRepository.findByUsername(username); // 추출한 정보를 userRepository에 저장.
    const isValidpassword = await bcrypt.compare(password, user.password) 
         //  compare(): 비밀번호가 일치하는지의 여부를 비교.
    const token = createJwtToken(user.id); 

    if (!user) {                                   // 로그인 구조이므로, user객체가 없다면 401 error 반환.
        return res.status(401).json({message:'아이디/비밀번호 확인좀.'});
    }
    if(!isValidpassword){                        // 비밀번호의 유효 여부가 false인 경우, 401 error 반환.
        return res.status(401).json({message:'아이디/비밀번호 확인좀.'});
    }   
    res.status(200).json({token, username}); // JWT 토큰을 생성하여 응답.

 }

 3)
 export async function me(req, res, next) {              // '사용자의 정보'를 반환하는 함수.

    const user = await userRepository.findById(req.userId); // JWT 토큰에서 추출한 사용자 ID로 해당 사용자 정보를 조회.

    if (!user) {
        return res.status(404).json({message:'사용자가 없음.'});
    }
    res.status(200).json({token:req.token, username:user.username});

 }


 function createJwtToken(id) {                            // 사용자 ID를 받아서 JWT 토큰을 생성하는 함수.
    return jwt.sign({id}, jwtSecretKey, {expiresIn: jwtExpiresInDays});   // jwt.sign() 메서드를 사용하여 토큰을 생성하고 반환.
 }

 

C:\yjcho\Node.js\Project\Server\data\auth.js

경로에 해당 내용 추가/수정.

 

  let users =             // 임의의 사용자 객체를 users에 저장.

    {
    id: '1',
    username: 'melon',
    password: '$2b$10$697D3aph6EIGKXx5fQtqwupoV8nn5Fen7qk9bw.KixWwBMYeBm7Tq',
    name: '이메론',
    email: 'melon@melon.com',
    url: ' https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS87Gr4eFO7Pt2pE8oym4dxXnxGZYL2Pl_N5A&usqp=CAU' 
    } ] ;


    export async function findByUsername(username){  //  users 배열에서 해당 username을 가진 사용자 객체를 찾는 함수.
        return users.find((user)=>user.username === username);
    }                        // user의 username과 일치할 시 반환.


    export async function createUser(user){        // 새 사용자 객체를 users 배열에 추가하는 함수.
        const created = {...user, id:Date.now().toString()};
              // user 객체를 복제하고, id 속성에 현재 시간을 문자열로 변환한 값을 할당하여 created 변수에 저장.
        users.push(created);
        return created.id;
    }


    export async function findById(id){                 // users 배열에서 해당 id를 가진 사용자 객체를 찾는 함수.
        return users.find((user)=>user.id === id);
  }


 

C:\yjcho\Node.js\Project\Server\router\auth.js

경로에 해당 내용 추가/수정.

 

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

  const router = express.Router();         // Router 객체를 생성.

  const validateCredential = [   //  credential 유효성 검사에 사용될 middleware 함수들을 배열에 담음.
    body('username')                // username의 조건. (공백 X, 빈 문자 X,최소 4글자 이상)
        .trim()
        .isLength({min:4})
        .withMessage('id는 최소 4자 이상 입력좀'),
    body('password')                // password의 조건. (공백 X, 빈 문자 X,최소 4글자 이상)
        .trim()
        .isLength({min:4})
        .withMessage('pw는 최소 4자 이상 입력좀'),
    validate
  ];

  const validateSignup = [       //  signup 유효성 검사에 사용될 middleware 함수들을 배열에 담는다.
    ...validateCredential,
    body('name').notEmpty().withMessage('이름은 꼭 입력좀'),        // name의 조건. (공백 X)
    body('email').isEmail().normalizeEmail().withMessage('이메일을 입력좀'),   // email의 조건. ('이메일'형식 사용)
    body('url').isURL().withMessage('URL 입력좀')             // URL의 조건. ('URL'형식 사용)
        .optional({nullable: true, checkFalsy:true}), // data가 null이어도 true.
    validate
  ];

  router.post('/signup', validateSignup, authController.signup);
 // '/signup' endpoint에 POST 요청 → validateSignup middleware 함수 → authController.signup 함수 실행.
  router.post('/login', validateCredential, authController.login);
 // '/login' endpoint에 POST 요청 → validateCredential middleware 함수 → authController.login 함수 실행.

  router.get('/me', isAuth, authController.me);
 // '/me' endpoint에 GET 요청 → isAuth middleware 함수 → authController.me 함수 실행.

  export default router;          // Router 객체를 내보냄.


 

C:\yjcho\Node.js\Project\Server\middleware\auth.js
경로에 해당 내용 추가/수정.



 import jwt from 'jsonwebtoken';

 import * as userRepository from '../data/auth.js';

 const AUTH_ERROR = { message: '인증에러' };    // 에러가 발생할 때 반환되는 메시지인 상수 AUTH_ERROR 정의.

 export const isAuth = async (req, res, next) =>           // 인증 여부를 판단하는 isAuth 함수 생성.
    const authHeader = req.get('Authorization')        // request header에서 Authorization 값을 가져와 authHeader 변수에 저장.
    if (!(authHeader && authHeader.startsWith('Bearer '))){
        return res.status(401).json(AUTH_ERROR);
      //  만약 authHeader 값이 없거나, Bearer 로 시작하지 않는다면, AUTH_ERROR와 401 error를 반환.

    }

        const token = authHeader.split(' ')[1];      // Bearer 뒤에 바로 나오는 값을 authHeader에서 token값으로 추출함.
        jwt.verify(         // jwt.verify(): token을 복호화하고 이에 따른 error와 decoding 값을 반환해주는 메서드.
            token,
            'Eh$nZnt8Qz&IUE*Tb3cV90wC43Ea$6T0', // server>controller>auth.js에서의 secretkey를 가져옴.
            async (error, decoded) => {                          // error와 decoding 중 하나 결정됨.
                if (error) {
                    return res.status(401).json(AUTH_ERROR);        // 복호화 실패 시, AUTH_ERROR와 401 error를 반환.
                } 
                const user = await userRepository.findById(decoded.id);
                if (!user) {                                                                   
                    return res.status(401).json(AUTH_ERROR);        // user가 없을 시, AUTH_ERROR와 401 error를 반환.
                }
                req.userId = user.id;
                next();
            }
   //  user를 찾은 경우, request 객체에 userId 값을 추가하고 next()를 호출.
       이로써 인증 여부가 판단된 후 다음 함수로 이동할 수 있게 됨.

           
        )
 }


 

C:\yjcho\Node.js\Project\Server\index.js

경로에 해당 내용 추가/수정.


import authRouter from "./router/auth.js";

app.use("/auth", authRouter);

 

Client\src 폴더 內 db, network, service 폴더에  각각 'token.js', 'http.js', 'auth.js' 파일 생성

 

C:\yjcho\Node.js\Project\Client\src\db\token.js

경로에 해당 내용 추가/수정.


 const TOKEN = 'token';


 export default class tokenStorage       

    saveToken(token){                                       // token 값을 로컬 스토리지에 저장하는 메서드.
        localStorage.setItem(TOKEN, token);
    }

    getToken(){                                                   // token 값을 반환하는 메서드.
        return localStorage.getItem(TOKEN);
    }

    clearToken(){                                                // token 값을 로컬 스토리지에서 제거하는 메서드.
        localStorage.clear(TOKEN);
    }
 }

 

C:\yjcho\Node.js\Project\Client\src\network\http.js

경로에 해당 내용 추가/수정.



 export default class HttpClient {

    constructor(baseURL){      // baseURL을 매개변수로 받아서 인스턴스 속성으로 할당한다.
        this.baseURL = baseURL;
    }

    async fetch(url, options) {
        const res = await fetch(`${this.baseURL}${url}`, { .
            ...options,
            headers: {
                'Content-Type': 'application/json',
                ...options.headers
            }
        });   // fetch 함수를 사용하여 this.baseURL과 url을 합쳐서 URL을 만들고, options 객체를 기반으로 HTTP 요청을 보냄.

        let data;
        try {
            data = await res.json();
        }catch (error){
            console.error(error);
        }

        if(res.status > 299 || res.status < 200){
            const message = data && data.message ? data.message : '문제가 발생하였습니다! 😐';
            throw new Error(message);
        }

        return data;
    }
 }


 

C:\yjcho\Node.js\Project\Client\src\service\auth.js

경로에 해당 내용 추가/수정.


  export default class AuthService {


  constructor(http, tokenStorage){
    this.http = http;
    this.tokenStorage = tokenStorage;
   }

  async signup(username, password, name, email, url) {
    const data = await this.http.fetch('/auth/signup', {
      method: 'POST',
      body: JSON.stringify({
        username,
        password,
        name,
        email,
        url
      })
    })
    this.tokenStorage.saveToken(data.token);
    return data;
   }
  }

  async login(username, password) {
    const data = await this.http.fetch('/auth/login', {
      method: 'POST',
      body: JSON.stringify({username, password})
    });
    this.tokenStorage.saveToken(data.token);
    return data;
   }

   async me() {
    return {
      username: 'admin',
      token: 'abc1234',
    };
   }

   async logout() {
     return;
 }


 

C:\yjcho\Node.js\Project\Client\src\index.js

경로에 해당 내용 추가/수정.


import React from 'react';

import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import AuthService from './service/auth';
import TweetService from './service/tweet';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { AuthErrorEventBus } from './context/AuthContext';
import HttpClient from './network/http';
import TokenStorage from './db/token';

// .env에서 읽어옴: http://localhost:8080
const baseURL = process.env.REACT_APP_BASE_URL;
const httpClient = new HttpClient(baseURL);
const authErrorEventBus = new AuthErrorEventBus();
const tokenStorage = new TokenStorage();
const authService = new AuthService(httpClient, tokenStorage);
const tweetService = new TweetService(httpClient, tokenStorage);


ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <AuthProvider
        authService={authService}
        authErrorEventBus={authErrorEventBus}
      >
        <App tweetService={tweetService} />
      </AuthProvider>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

 

C:\yjcho\Node.js\Project\Server\router\tweets.js

경로에 해당 내용 추가/수정.

(아래와 같이 isAuth를 import한 후, router의 중간에 isAuth를 삽입.)


 import { isAuth } from '../middleware/auth.js';

 // GET         키         값
 // /tweets? username=:username
 router.get('/',  isAuth , tweetController.getTweets);

 // GET
 //   /tweets/:id
 router.get('/:id',  isAuth , tweetController.getTweet); 


 // text가 4자리 이하인 경우 에러처리해보기 (5/2)
 // POST
 // id: Date.now().toString()
 router.post('/',  isAuth , validateTweet, tweetController.createTweet);


 // PUT
 // text만 수정
 router.put('/:id', isAuth ,  validateTweet, tweetController.updateTweet);

 // Delete
 router.delete('/:id', isAuth , tweetController.deleteTweet);

 

위의 내용과 같이 작성 후, 서버를 2개 모두 열어 twitter 기능을 open.

 

 

회원가입을 진행.

 

 

위와 같이 양식에 맞게 내용을 채운 후 Sign up 버튼 클릭

 

현재까지의 상태.

 

 

6-2. PUT, DELETE 기능에 사용자 인증 기능 추가

C:\yjcho\Node.js\Project\Server\controller\tweet.js

경로에 해당 내용 추가/수정.


  export async function updateTweet(req, res, next) {         
    const id = req.params.id;
    const text = req.body.text;
    const tweet = await tweetRepository.getById(id); 
    if(!tweet) {
        res.status(404).json({message: `Tweet id (${id}) not found` })
    } 
    if(tweet.userId !== req.userId){
        
        return res.sendStatus(403);  
    }
    const updated = await tweetRepository.update(id, text);
    res.status(200).json(updated); 
  } 

 export async function deleteTweet(req,res,next) {  
    const id = req.params.id;
    const tweet = await tweetRepository.getById(id); 
    if(!tweet){
        res.status(404).json({message: `Tweet id (${id}) not found` })
    } 
    if(tweet.userId !== req.userId){
        return res.sendStatus(403); 
    }
    await tweetRepository.remove(id);
    res.sendStatus(204);
  }
 

 

C:\yjcho\Node.js\Project\Server\data\tweet.js

경로에 해당 내용 추가/수정.

 
 import  * as userRepository from './auth.js';


 let tweets =  
    {
    id:'1',
    text:'첫 트윗입니다!!',
    createdAt: Date.now().toString(),
    userId: '1' 
    },
    {
        id:'2',
        text:'안녕하세요!!',
        createdAt: Date.now().toString(),
        userId: '1'
    }
 ] ;


1)
 export async function getAll(){
    return Promise.all(
    tweets.map(async (tweet) => {
        const{username, name, url} = await userRepository.findById(tweet.userId);
        return {...tweet, username, name, url};
    })
    )
 }


2)
 export async function getAllByUsername(username){ 
    return getAll().then((tweets)=>tweets.filter((tweet) => tweet.username === username));
 }
 

3)
 export async function getById(id){ 
    const found = tweets.find((tweet)=>tweet.id === id);
    if(!found){
        return null;
    }
    const {username, name, url} = await userRepository.findById(found.userId);
    return {...found, username, name, url};
 }


4)
 export async function create(text,userId){ 
    const tweet = {
        id: Date.now().toString(),
        text,
        createdAt: new Date(),
        userId
    };
    tweets = [tweet, ... tweets];
    return getById(tweet.id);
 } 


5)
 export async function update(id,text){
    const tweet = tweets.find((tweet) => tweet.id === id)
    if(tweet){
        tweet.text = text;
    }
    return tweet
 }


6)
 export async function remove(id){
    tweets = tweets.filter((tweet) => tweet.id !== id);
 }


 

me와 관련된 controller를 생성

C:\yjcho\Node.js\Project\Server\controller\auth.js

경로에 해당 내용 추가/수정.


  //로그인이 끊어졌는지의 여부를 확인
  export async function me(req, res, next){
     const user = await userRepository.findById(req.userId);
     if(!user){             
        return res.status(404).json({message:'사용자가 존재하지 않습니다.'}); 
     }
  // user 변수가 undefined이거나 null이라면(데이터베이스에서 찾은 사용자가 없다면) 오류 메시지를 보내고 함수를 종료.
    res.status(200).json({token: req.token, username: user.username})
  }

 

findById 함수를 생성

C:\yjcho\Node.js\Project\Server\data\auth.js

경로에 해당 내용 추가/수정.


export async function findById(id){
    return db.execute('select id from users where id=?', [id]).then((result)=>result[0][0]);  // id로 찾기
}

 

Postman 프로그램에서 아래와 같이 출력.

 

위 경로에 디렉토리 생성 후 내용 추가/수정.

C:\yjcho\Node.js\Project\Server\middleware\auth.js

 

Postman 프로그램에서 Me의 header부분을 아래와 같이 작성

    Key: Authorization

    Value: Bearer 'signup에서의 token값'

    (token값은 항상 바뀌므로 signup에서 token값을 매번 바꿔주어야 함)

 

 

 

4번에서 isAuth를 정의한 후, 해당 경로에 사진의 내용 추가

 

Postman 프로그램에서

     Signup을 Send.

     Signup에서 출력된 token값을 복사하여 me의 value의 Beared 뒤에 붙여넣기.

     Login을 Send.

     Me를 Send.

아래 사진은 정상적인 출력값.

 

data에 있는 users를 이용해서 로그인해보기

 

tweets의 내용에 인증 걸어주기

(isAuth 변수를 가져와서 router에 입력.)

 

token이 없기 때문에 unauthorized 상태이므로 에러가 발생.

 

Postman의 Authorization에서 아래 사진과 같이 설정

7. bcrypt, jwt 기능 실습

 

예) 제공된 project twitter에 bcrypt, jwt를 이식해보기.

 

1. postman 프로그램 실행 → My Workspace에 'Auth' 폴더 생성. (실시간 연결여부 확인 목적)

 

2. 회원가입 목적의 POST메서드 'Signup' 파일 생성.

 

 

3. 로그인 목적의 POST메서드 'Login' 파일 생성.

 

 

4. 로그인을 확인하기위한 목적의 GET메서드 'Me' 파일 생성.

 

client, server를 열고, twitter창을 열어 해당 창까지 오도록 함.

 

5. 로그인/회원가입에서 보안관련 내용을 control할 목적으로 코드 입력.

사용경로: C:\yjcho\Node.js\Project\Server\controller\auth.js


import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt'
import * as userRepository from '../data/auth.js';

const jwtSecretKey = 'Eh$nZnt8Qz&IUE*Tb3cV90wC43Ea$6T0'; // 임의의 32bit 키를 가져옴.
const jwtExpiresInDays = '2d' // 이틀동안 사용이 가능.
const bcryptSaltRounds = 10;  // 10번 반복해서 돌림.

export async function signup(req, res){
    const { username, password, name, email, url } = req.body;
    const sign_id = await userRepository.findByUsername(username);
    if (found) {
        res.status(409).json({message:`${username}은 이미 가입됨.`});
    }
    const hashed = await bcrypt.hash(password, bcryptSaltRounds);
    const userId = await userRepository.createUser({
        username,
        password: hashed,
        name,
        email,
        url
    });
    const token = createJwtToken(userId); // data와 상관이 없는 함수이므로, 아래에 생성할 예정.
    res.status(201).json({token, username});
    }   


 // req.body로 data를 받아서 회원가입을 시키는 함수.
 // 해당 id가 존재한다면 '409'를 return.
 // userRepository에 데이터를 저장(비밀번호는 bcrypt를 사용하자)하여 회원가입을 진행.
 // JWT를 이용하여 사용자에게 json으로 전달할 예정.

export async function login(req, res){
    const {username, password} = req.body

    const user = await userRepository.findByUsername(username);

    if (!user) {   // 로그인이므로, user객체가 없으면 오류가 발생.
        return res.status(401).json({message:'아이디/비밀번호 확인좀.'});
    }

    const isValidpassword = await bcrypt.compare(password, user.password) 
  // 비밀번호가 일치하는지를 compare 메서드로 비교하여 확인.

    if(! isValidpassword ){ // false인 경우, 401 error.
        return res.status(401).json({message:'아이디/비밀번호 확인좀.'});
    }
    
    const token = createJwtToken(user.id);
    res.status(200).json({token, username});
}


    // req.body로 data를 받아서 해당 id로 로그인 여부를 판단하는 함수.
    // 해당 id가 존재하지 않으면, '401'을 return.
    // bcrypt를 이용하여 비밀번호까지 모두 맞다면, 해당 정보를 JWT를 이용하여 사용자에게 json으로 전달.


export async function me(req, res, next){
    
}


function createJwtToken(id){
    return jwt.sign({id}, jwtSecretKey, {expiresIn: jwtExpiresInDays});
}

 

6. 로그인/회원가입에서 보안관련 내용 data를 보관/추출할 목적으로 코드 입력.

사용경로: C:\yjcho\Node.js\Project\Server\data\auth.js


let users =[
    {
    id: '1',
    username: 'melon',
    password: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImFwcGxlIiwibmFtZSI6Iuq5gOyCrOqzvCIsImlzQWRtaW4iOmZhbHNlLCJpYXQiOjE2ODMwNzkzMDAsImV4cCI6MTY4MzA3OTM2MH0.rnsnd5aOeAw4r6AVQctdsGwLQxwBsqz8228JGZ_3Ic8',
    name: '이메론',
    email: 'melon@melon.com',
    url: ' https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS87Gr4eFO7Pt2pE8oym4dxXnxGZYL2Pl_N5A&usqp=CAU' 
    }];

    export async function findByUsername(username){
        return users.find((user)=>user.username === username);
    } // username을 전달받아 user의 username과 일치할 시 반환.

    export async function createUser(user){
        const created = {...user, id:Date.now().toString()};
        users.push(created);
        return created.id;
    }

7.  로그인/회원 가입/내정보가져오기와 관련된 라우팅을 구현

사용경로: C:\yjcho\Node.js\Project\Server\router\auth.js

/* 주어질 조건:

    회원가입 → post로 /signup 페이지를 호출했을 때 들어오도록 함.
    name: 빈 문자 X (notEmpty())
    email: 이메일형식 체크, 모두 소문자로.
    url: URL체크(isURL())

    로그인  post로 /login 페이지를 호출했을 때 들어오도록 함.
    username: 공백 X, 빈 문자 X
    password: 공백 X, 최소 4자 이상.

*/ 

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

const router = express.Router();

const validateCredential = [
    body('username')
        .trim()
        .isLength({min:4})
        .withMessage('id는 최소 4자 이상 입력좀'),
    body('password')
        .trim()
        .isLength({min:4})
        .withMessage('pw는 최소 4자 이상 입력좀'),
    validate
];

const validateSignup = [
    ...validateCredential ,
    body('name').notEmpty().withMessage('이름은 꼭 입력좀'),
    body('email').isEmail().normalizeEmail().withMessage('이메일을 입력좀'),
    body('url').isURL().withMessage('URL 입력좀')
        .optional({nullable: true, checkFalsy:true}), // data가 null이어도 true.
    validate
];

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

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

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

export default router;

 

사용 경로: C:\yjcho\Node.js\Project\Server\index.js에 추가

import authRouter from "./router/auth.js";

app.use("/auth", authRouter);