[react] JWT토큰 & 전역스테이트변수(context api)

Suji Kang·2023년 10월 4일
0

🐾 로그인 정보를 넘겨주기전에 유효성검사

유효성 검사를 통과했다 != 로그인에 성공했다
유효성 검사를 통과했다 => 이 값을 서버에 전달할 수 있겠다

🌟 react 👉 express 👉 mysql

값이 있을때 : 응답(200)
값이 없을때 : 응답은 서버 만드는사람 마음대로(404나 오류 메세지)

login.js

import { Link } from "react-router-dom";
import { AuthBody, AuthBox, AuthFooter, AuthForm, BgImg, Button, CancelIcon, ErrMsg, Input, InputBoxWrap, Line, LogoImg, Wrap } from "../../styles/auth/auth.styles";
import CloseIcon from '@mui/icons-material/Close';
import { useState } from "react";

const LoginPage = () => {
    // 유효성 검사
    // 이메일 입력시 비어있으면안됨, 이메일형식(@포함)
    // 비밀번호입력시 비어있으면 안됨, 6자리 이상()

    // submit 할때도 이메일, 비밀번호 잘 입력되었는지 확인
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');

    const [emailErrMsg, setEmailErrMsg] = useState('');
    const [passwordErrMsg, setPasswordErrMsg] = useState('');

    const onEmailChange = (e) => {

        const emailText = e.target.value;
        setEmail(emailText);

        if (emailText === '') {
            setEmailErrMsg('이메일은 필수 입력 값입니다.');
        } else if (!emailText.includes('@')) {
            setEmailErrMsg('이메일 형식을 지켜주세요');
        } else {
            setEmailErrMsg('');
        }
    }

    const onPasswordChange = (e) => {
        const passwordText = e.target.value;
        setPassword(passwordText);

        if (passwordText === '') {
            setPasswordErrMsg('비밀번호는 필수 입력 값입니다.');
        } else if (passwordText.length < 6) {
            setPasswordErrMsg('최소 6글자 이상으로 작성해주세요');
        } else {
            setPasswordErrMsg('');
        }
    }

    const onLoginSubmit = (e) => {
        e.preventDefault();

        let check = true;

        if (email === '') {
            setEmailErrMsg('이메일은 필수 입력 값입니다.');
            check = false;
        } else if (!email.includes('@')) {
            setEmailErrMsg('이메일 형식을 지켜주세요');
            check = false;
        } else {
            setEmailErrMsg('');
        }

        if (password === '') {
            setPasswordErrMsg('비밀번호는 필수 입력 값입니다.');
            check = false;
        } else if (password.length < 6) {
            setPasswordErrMsg('최소 6글자 이상으로 작성해주세요');
            check = false;
        } else {
            setPasswordErrMsg('');
        }

        if (check) {
            alert('로그인 유효성 검사가 끝나면 해야할 코드');
        }
    }

    return (
        <BgImg>
            <Wrap>
                <CancelIcon><CloseIcon /></CancelIcon>
                <AuthBox>
                    <LogoImg src="/logo.svg" alt="logo" />
                    <AuthBody>
                        <AuthForm onSubmit={onLoginSubmit}>
                            <InputBoxWrap>
                                <div className="input-box">
                                    <Input onChange={onEmailChange} type="text" placeholder="아이디" />
                                    <ErrMsg>{emailErrMsg}</ErrMsg>
                                </div>
                                <div className="input-box">
                                    <Input onChange={onPasswordChange} type="password" placeholder="비밀번호" />
                                    <ErrMsg >{passwordErrMsg}</ErrMsg>
                                </div>
                            </InputBoxWrap>
                            <Button>로그인 하기</Button>
                        </AuthForm>
                        <Line className="line"></Line>
                        <AuthFooter>
                            <Link to="">이메일 찾기</Link>
                            <Line className="line"></Line>
                            <Link to="">비밀번호 찾기</Link>
                            <Line className="line"></Line>
                            <Link to="/join">회원가입 하기</Link>
                        </AuthFooter>
                    </AuthBody>
                </AuthBox>
            </Wrap>
        </BgImg>
    );
}

export default LoginPage;

📌 로그인 성공시 로직 - 리액트에서 로그인 요청

📌 expressmysql에서 해당 사용자 있는지 확인 ❗️ (57:00/10/02)

사용자가 있다면, jwt 토큰 발급하여 react로 전달

reactjwt토큰 저장해놓고
앞으로 로그인 한 사람만 접근할 수 있는 페이지가 있다면
jwt토큰함께 담아서 요청

express에서jwt토큰을 받아서, 토큰유효한지 검사하여,
해당 📝토큰이 유효하면 응답하고,
📝 유효하지 않으면 오류 발생(권한이 없습니다 로그인을 다시해주세요)



🐾 JWT토큰 (Json Web Token)

JS 객체에다 저장하는게 가장 편리- 객체를 암호화해서 토큰을 만들 수 있음
공식문서 👉 https://jwt.io/

설치

npm install jsonwebtoken

import

const jwt = require("jsonwebtoken"); // 옛날방식의 import

https://www.npmjs.com/package/jsonwebtoken

🌟 sign이라는 함수는 암호화라는 토큰을 만드는 함수

먼저,
암호화를 시킬때 어떤 데이터암호화시킬건지 말해준다

📝 문법:

jwt.sign({payload(데이터가 들어가야하는 부분},'tokenkey',{언제까지 유효한지 설정가능})
//ex
jwt.sign( {email:'abc@test.com'},'tokenpw',{{expiresIn:'0.1s'}} )

📌 payload가 들어가야하는 부분이 첫번쨰 {}객체
📌 key 비밀번호 ' '가 들어간다
📌 {expiresIn:'0.1s'} 언제까지 유효한지 설정가능

lat - 만든시간 issued at
exp - 유효한시간 expired

example
app.js

const jwt = require("jsonwebtoken");

app.get('/api/jsonwebtokentest', (req, res) => {

    let myToken = jwt.sign({ email: 'abc@test.com' }, 'tokenpw', { expiresIn: '1hr' })
    
    console.log(myToken);
    // let decoded = jwt.decode(myToken );
    // console.log(decoded);

    let verify = jwt.verify(myToken, 'tokenpw');

    console.log(verify);

    res.json('응답끝');
});

📌 유효성 검사

login.js 1:00

import { useNavigate } from "react-router-dom";

const LoginPage = () => {
  
 const navigate = useNavigate();

 const onLoginSubmit = async (e) => {
        e.preventDefault();

        let check = true;
   
    if (check) {
            // 로그인한 회원 조회
            try {
                let res = await axios.post('/api/login', { email, password });
                alert(res.data.accessToken);
                // 로컬스토리지에 저장
                localStorage.setItem('accessToken', res.data.accessToken);
                //전역상태변수(App.js에있는 accessToken)에 저장
                setAccessToken(res.data.accessToken);

                navigate('/', { replace: false });
            } catch (err) {
                console.log(err);
                if (err.response.status === 404) {
                    alert('아이디 또는 비밀번호를 확인해주세요');
                }
            }
        }
 }
 export default LoginPage;

app.js

app.post('/api/login', async (req, res) => {
   const { email, password } = req.body;

   // mysql에서 해당 email 존재하는지
   try {
       let sql = 'SELECT email, pw FROM tbl_users WHERE email=?';
       const [rows, fields] = await pool.query(sql, [email]);
       console.log(rows);

       if (rows.length === 0) { 🌟 //rows가 비어있으면, //이메일 실패
           res.status(404).json('로그인 실패!'); 
           return;
       }
       // 사용자가 로그인할때 입력한 "일반 비밀번호랑", "암호화되어 저장된 비밀번호"랑 서로 "같은지" 검사
       // 일반비밀번호: password
       // 암호화된 비밀번호 : rows[0].pw
       if (!bcrypt.compareSync(password, rows[0].pw)) {
           // 🌟이메일은 맞췄지만, 비밀번호는 틀렸을때
           res.status(404).json('로그인 실패!');
           return;
       }
       // 🌟 로그인이 성공 했다면 ?
       // jwt 토큰 만들기
       // payload에는 {email:'로그인한사람이메일'}
       // 1시간짜리 유효한 토큰으로 만들기
       const accessToken = jwt.sign({ email: rows[0].email }, process.env.JWT_SECRET, { expiresIn: '1h' }); //process.env.JWT_SECRET--> .env.local 에 비번설정하고 가져오기

       console.log(accessToken);

       res.json({ accessToken }); // 만든 토큰을 "객체"에 담아서 🌟 리액트로 전달 (login.js로)

   } catch (err) {
       res.status(500).json('mysql에서 오류발생');
   }
})

1:44

**회원가입할때**
리액트           mysql
ab@ab.com      ab@ab.com //이메일그대로 입력
123456         eyJlbWFpbCI6ImFiY0B0ZXN0LmNvbSIsImlhdCI6MTY5NjQwNDYzNiwiZXhwIjoxNjk2NDA4MjM2fQ //비밀번호 암호화되서 입력
**로그인할때**
ab@ab.com     //이메일 입력하면 찾아준다.
123456        //비밀번호는 회원가입한거처럼 될까? 놉! 암호화된결과물이 서로 다르다.
gdJlbWFpbCI6ImFiY0B0ZXN0LmNvbSIsImlhdCI6MTY5NjQwNDYzNiwiZXhwIjoxNjk2NDA4MjM2fQ
//암호화해서 회원가입할때 암호화된 애랑 비교하면 되지 않을까??? 생각할수있지만, bcrypt 는 보안이 철저해서 똑같이 암호화가 되지 않는다.
그래서, **로그인할때**
ab@ab.com 이메일 가진 녀석을 찾아서
암호화된 비밀번호 가져오기!  👉  (bcrypt함수 사용해서)
bcrypt(일반 비밀번호, 암호화 된 비밀번호) //일반 비밀번호랑 암호화 된 비밀번호를 확인한다

비밀번호를 맞게쓰면 토큰이 나온다
🔎 그런데, 이 토큰을 변수에 저장하면 문제가생길수있다❗️
👉LoginPage 함수 안에 저장해봤자 다른 페이지로 이동하면 사라져버리기 때문에 ❗️
그래서, 브라우저에 저장공간이 있다(local storage, Session storage, Cookies)
🤨 어떤걸 사용하든 본인의 취향 차이(서로 약간의 장단점은 있을수 있음)

local storage 👉 페이지가 바껴도 브라우저에서 가지고있다.

cookies 👉 조금더 세세하게 설정 가능함 key와 value 로(name, value, domain,...)

📝 변수에서 만들면 그 페이지에서밖에 못쓰니까, local storage로 저장해서 브라우저에 저장한다.


📝 토큰을 loginPage에 저장을한다고 가정하면❓

📝 페이지가 dashboardPage로 바뀌면 ❓
👉 👉 토큰이 사라진걸 볼수있다 🔎

그런데, 여기서 토큰을 제일 상위 컴포넌트인, App.js에 넣어주면 🤨❓

짜잔 🌟 App.js에서 저장한 토큰이 loginpage에도 사용가능하네❓

짜잔 🌟 App.js에서 저장한 토큰이 dashboardpage에도 사용가능하네❓

🌟 그래서 최상위(App.js) component에 state변수를 저장하면 자식에서도 사용가능 ❗️❗️❗️ yeah 😇 그림으로 하니까 설명이 더 잘되쥬?👍

자 그럼 이제 더 자세한설명으로 가보자 🔎

🐾 전역 state 변수

state변수만들어져있는 컴포넌트 안에서밖에 사용을 못한다
그런데❗️ tokena컴포넌트, b컴포넌트, c컴포넌트에서 모두 사용을 해야하는데..?

📝 전역 스테이트변수는 스테이변수는 스테이트변수인데
모든 컴포넌트넌트에서 접근할 수 있는 스테이트변수를 의미한다

🔎 전역스테이트변수 라이브러리

👉 redux, context api , ....

🐾 context api

  1. context를 만든다. createContext 를 활용하여
    ex)
    	const ABC = createContext();
  1. 해당 내가 만든 context provider활용하여 감싸준다(provider로 감싼 자식들에서모두 사용가능)
    ex)
         <ABC.Provider>
           //이안에그려지는 자식 컴포넌트에서는 모두 값 접근가능
         </ABC.Provider>
  1. 이때 자식에서 사용할 수 있도록 하고 싶은 값들은
    providervalue에다 넘겨준다
    ex)
         <ABC.Provider value={ {a:, b:, c:} }>
           //이안에그려지는 자식 컴포넌트에서는 모두 
           //{a:값, b:값, c:값} 값 접근가능
         </ABC.Provider>
  1. 자식 컴포넌트에서는 useContext를 사용하여 어떤 context에서 어떤 값을 사용하고 싶은지 명시한다
    ex)
        const value = useContext(ABC);
        //구조분해할당도 사용가능
        const {b, c} = useContext(ABC);

App.js

import { createContext, useState } from 'react';

export const UserContext = createContext(); //다른데서도 써야하니까 export 써주는거 잊지말고

const App = ()=>{
  // 전역에서 사용할 변수
  const [ accessToken, setAccessToken ] = useState(null); //로그인안한상태로 null 설정
  return (
    <UserContext.Provider value={ {accessToken, setAccessToken} }>
      <RouterProvider router={router}/>
    </UserContext.Provider>
  );
}

login.js

import { UserContext } from "../../App";

const LoginPage = () => {

// App.js에 있는 accessToken 변수와 setAccessToken 함수 사용하기
    const { setAccessToken } = useContext(UserContext);
    
    if (check) {
            // 로그인한 회원 조회
            try {
                let res = await axios.post('/api/login', { email, password });
                alert(res.data.accessToken);
                // 로컬스토리지에 저장
                localStorage.setItem('accessToken', res.data.accessToken);
                //전역상태변수(App.js에있는 accessToken)에 저장
                setAccessToken(res.data.accessToken);

                navigate('/', { replace: false });
            } catch (err) {
                console.log(err);
                if (err.response.status === 404) {
                    alert('아이디 또는 비밀번호를 확인해주세요');
                }
            }
        }
    }
profile
나를위한 노트필기 📒🔎📝

0개의 댓글