본문 바로가기
Development(Web, Server, Cloud)/22) React.js & Node.js

React.js & Node.js 18일차 - JWT, 회원가입, 로그인 구현

by tonyhan18 2022. 1. 21.
728x90

atoms를 사용한 이유는 결국 태그를 재활용하기 위해서이다. 모든 컴포넌트나 html 태그를 atoms에 선언할 필요없이 pages에 선언해도 된다. 여러 컴포넌트에서 재활용되는 것들은 따로 관리를 해서 중복되지 않게끔 하는것이 중요하다.

 

이를 가르켜 do not return yourself라는 말이 있다. 복붙하지마라.

 

먼저할것은 handler/user.js에 있는 false를 반환해주는 부분을 실패한 409 코드를 보내주어야한다.

 

위와같이 409 메세지가 온것을 확인할 수 있다.

 

이제 문제가 업는 경우 db user table에 사용자를 추가해주어 가입된걸로 처리해보자

그런데 password를 그냥 db에 저장하면안된다. 개인정보유출문제때문이다. 그래서 패스워드 저장시에는 이게 무슨 글자인지 아무도 모르게 암호화해야한다. 그러기 위해서는 salt가 필요하다. salt는 랜덤한 문자인데 이 salt와 비번을 가지고 암호화된 패스워드를 만드는 것이다.

 

근데 이걸하기 위해 서버에 추가 라이브러리를 추가하자

```

npm i bcrypt

```

bcrypt는 암호화 라이브러리이다.

 

genSalt로 10번 돌리어서 salt를 만들어주자

import conn from "../db/index.js";
import bcrypt from "bcrypt";

export const postUsers = async function (req, res, next) {
  const { username, password } = req.body;
  const query = `SELECT * FROM user WHERE user_name='${username}'`;
  const [rows] = await conn.query(query);
  if (rows.length) {
    return res.status(409).send({
      success: false,
      message: "중복되는 아이디가 존재합니다.",
    });
  }

  const salt = await bcrypt.genSalt();
  const hashedPW = await bcrypt.hash(password, salt);

  console.log(salt, hashedPW);
  res.send({ success: true });
};

 

위와같이 salt와 hashedPW가 나오는 것을 확인할 수 있다.

바로 아래쪽에 쿼리를 작성하여 DB에 날려주었다.

이때 username, pw, salt는 모두 string이기 때문에 반드시 ''를 감싸서 전달해주어야한다.

 

만드는게 성공했다면 그 정보를 받아서 페이지 변환을 시켜보자

 

 

먼저 react쪽에서 보낸 데이터의 실패 결과를 보니 response.data 부분이 필요할거 같다.

 

위와같이 만들어서 받은 data에 정확하게 대응할 수 있도록 만들자

 

Signup의 handleSubmi 부분에 success에 따른 결과를 넣어주자

이때 useNavigate를 이용해 쉽게 페이지를 바꾸자

 

 

import { useState } from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { getToken } from "../../apis/user";
import {
  Box,
  BtnSubmit,
  Form,
  InputText,
  Logo,
  PageWrapper,
  SignupWrapper,
  Main,
} from "../atoms/login";

const Login = () => {
  const [loginInfo, setLoginInfo] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setLoginInfo({ ...loginInfo, [name]: value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const result = await getToken(loginInfo);
  };

  return (
    <PageWrapper>
      <Main>
        <Box>
          <Logo src="https://www.instagram.com/static/images/web/mobile_nav_type_logo-2x.png/1b47f9d0e595.png" />
          <Form onSubmit={handleSubmit}>
            <InputText
              name="username"
              placeholder="전화번호, 사용자 이름 또는 이메일"
              onChange={handleChange}
            />
            <InputText
              name="password"
              placeholder="비밀번호"
              type="password"
              onChange={handleChange}
            />
            <BtnSubmit>로그인</BtnSubmit>
          </Form>
          <FBLogin>Facebook으로 로그인</FBLogin>
          <ForgotPassword>비밀번호를 잊으셨나요?</ForgotPassword>
        </Box>
        <Box>
          <SignupWrapper>
            계정이 없으신가요? <Link to="/signup">가입하기</Link>
          </SignupWrapper>
        </Box>
      </Main>
    </PageWrapper>
  );
};

const FBLogin = styled.div`
  margin: 10px 40px 18px;
  font-size: 14px;
  color: #385185;
  font-weight: bold;
`;
const ForgotPassword = styled.div`
  font-size: 12px;
`;

export default Login;

 

login 부분도 signup 부분과 유사하게 넣어주었다.

 

서버쪽에 getToken을 처리할 부분을 만들어주자.

 

서버쪽에서 응답이 잘 오니 작업을 시작하자

 

export const postUsersToken = async function (req, res, next) {
  console.log(req.body);
  const { username, password } = req.body;
  const query = `SELECT * FROM user WHERE user_name='${username}'`;
  const [users] = await conn.query(query);
  console.log(users[0]);
  if (users.length === 0) {
    res.send({ success: false, message: "일치하는 유저가 없습니다" });
  }
  const { salt } = users;
  const hashedPW = await bcrypt.hash(password, salt);
  if (users.password != hashedPW) {
    return res.send({ success: false, message: "비밀번호가 틀렸습니다" });
  }
  res.send({ success: true });
};

 

해서 위와같이 일단 만들었다. 보면 db 데이터의 salt를 가지고 hash를 수행해서 원래의 pw를 구하고 사용자가 로그인한 정보와 비교해서 처리했다.

 

이제 토큰을 만들어서 사용자 정보를 간직하도록 만들자

JWT토큰이라고 해서 JSON WEB TOKEN을 보내주어야 한다.

 

생긴건 위와같고 `.`으로 세구역을 나누어주었다.

 

첫번째 부분이 헤더로 어떤 알고리즘이고 타입이 뭔지

두번째는 페이로드 ,데이터이다

세 번째는 토큰을 만들때 만든사람만 알 수 있는 secret key 부분(검증을 위한 부분)

 

토큰을 만들기 위해서 라이브러리를 추가핮

 

```

npm i jsonwebtoken

 

import jwt from 'jsonwebtoekn';

jwt.sign(payload, secretKey, option);

```

 

특이하게도 secretkey는 여기에서만 쓰는게 아니라서 외부에 저장해놓자

 

/config/index.js 파일을 만들어 안에 넣어야한다.

 

선언할때 이건 규칙인데 상수는 가급적 대문자와 언더바의 조합으로 쓰자.

 

 

import conn from "../db/index.js";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { JWT_SECRET_KEY as secretkey } from "../config/index.js";

export const postUsers = async function (req, res, next) {
  const { username, password } = req.body;
  const query = `SELECT * FROM user WHERE user_name='${username}';`;
  const [rows] = await conn.query(query);
  if (rows.length) {
    return res.status(409).send({
      success: false,
      message: "중복되는 아이디가 존재합니다.",
    });
  }

  const salt = await bcrypt.genSalt();
  const hashedPW = await bcrypt.hash(password, salt);

  const query2 = `
  INSERT INTO user(user_name, password, salt)
  VALUES('${username}', '${hashedPW}', '${salt}');
  `;
  await conn.query(query2);
  res.send({ success: true });
};

export const postUsersToken = async function (req, res, next) {
  console.log(req.body);
  const { username, password } = req.body;
  const query = `SELECT id, salt,password FROM user WHERE user_name='${username}';`;
  const [users] = await conn.query(query);
  //console.log(users[0]);
  if (users.length === 0) {
    res.send({ success: false, message: "일치하는 유저가 없습니다" });
  }
  // 비밀번호 검증
  const user = users[0];
  const { salt } = user;
  const hashedPW = await bcrypt.hash(password, salt);
  if (user.password != hashedPW) {
    return res
    .status(401)
    .send({ success: false, message: "비밀번호가 틀렸습니다" });
  }

  const payload = { userId: user.id };
  const option = { expiresIn: "1h" };
  const token = jwt.sign(payload, secretkey, option);
  res.send({ success: true, token });
};

코드 위쪽에서 secretkey를 받아올때 as를 써서 다른 이름으로 받아올 수 있다. 이렇게 하면 보다 쉽게 상수를 사용할 수 있다.

 

options에는 속성을 넣어줄 수 있는데 한시간동안만 유지하게끔 만들자

그리고 만들어진 토큰을 보내주자.

 

결과 success와 token을 볼 수 있다.

 

JWT에 가서 해보니 Signature Verified가 나오는 것을 확인할 수 있다.

 

이런식으로 작동한다고 생각하면 된다.

 

근데 토큰에도 여러종류가 있다.

Access Token이랑 Refresh Token이 있다. 로그인을 하면 두가지 토큰을 모두 발급한다. 서버랑 통신할때는 Access Token만 쓴다. 그리고 Access Token이 보다 expire이 짧다. 이렇게하는 이유는 토큰이 탈취되었을때 Access Token이 빨리 만료되도록 하는건데 구현이 만만치 않다.

 

로그인을 하면 토큰을 받아올텐데 이걸로 뭘해야할까? 로그인 실패시 http status code를 써야할거 같은데

 

먼저 패스워드가 틀린경우의 401에러를 처리해주어야 한다.

 

위와같이 try ~ catch로 처리해주자

 

이제 토큰을 쓸건데 문제는 이 페이지가 새로고침되면 토큰도 함께 날라간다. 그래서 이 토큰값을 코드가 아닌 다른곳에 저장해야 한다. 그건 바로 브라우저의 localStorage이다.

 

대충 콘솔에

```

localstorage.hello="hello"

```

해놓으면 위와같이 localStorage를 사용할 수 있다.

 

이건 또한 애플리케이션의 로컬 스토리지에서 확인할 수 있다. 그리고 이건 도메인별로 달라서 네이버는 자체 로컬 스토리지가 있다.

 

그래서 브라우저에는 localStorage와 sessionStorage가 있다. localStorage는 시스템 꺼도 살아있고 sessionStorage는 창 안에서만 유용하다.

 

암튼 지금 login 페이지에서 해야하는 건

1. localStorage에 token 저장

2. axios instance의 header에 token입력

3. useContext isLogin을 true로

 

이걸 위해서는 app.js에서 useContext에 정보를 담아야한다.

Router에서는 로그인이 필요한 페이지에 로그인체크 컴포넌트 감싸기를 해야한다.

728x90