React 28장 - 로그인 구현하기 (3)
포스트
취소

React 28장 - 로그인 구현하기 (3)

React

자료구조

  • client
    • pages
      • Login.js
      • Mypage.js
    • src
      • App.js
      • index.js
  • server
    • controllers
      • helper
        • tokenFunctions.js
      • users
        • login.js
        • logout.js
        • userInfo.js
      • index.js
    • db
      • data.js
    • index.js
    • .env
    • .gitignore

서버 만들기

server/.env

1
2
ACCESS_SECRET=(시크릿 )
REFRESH_SECRET=(시크릿 )
  • 서버에서 추가할 SALT 값이다.
  • 환경 변수이며, 사용자가 입력한 password에 salt값을 더한다.

server/.gitgnore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/node_modules
/.pnp
.pnp.js

/coverage

/build

key.pem
cert.pem
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
  • .gitgnore 파일에 key.pem과 cert.pem 파일을 담는다.

server/controllers/index.js

1
2
3
4
5
module.exports = {
  login: require("./users/login"),
  logout: require("./users/logout"),
  userInfo: require("./users/userInfo"),
};
  • 작성한 users 폴덩 있는 js 파일을 모듈의 이름으로 가져온다.
  • Route

server/controllers/helper/tokenFunction.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
require("dotenv").config();
const { sign, verify } = require("jsonwebtoken");

module.exports = {
  generateToken: (user, checkedKeepLogin) => {
    const payload = {
      id: user.id,
      email: user.email,
    };

    let result = {
      accessToken: sign(payload, process.env.ACCESS_SECRET, {
        expiresIn: "1d",
      }),
    };

    if (checkedKeepLogin) {
      result.refreshToken = sign(payload, process.env.REFRESH_SECRET, {
        expiresIn: "7d",
      });
    }

    return result;
  },
  verifyToken: (type, token) => {
    let secretKey, decoded;
    switch (type) {
      case "access":
        secretKey = process.env.ACCESS_SECRET;
        break;

      case "refresh":
        secretKey = process.env.REFRESH_SECRET;
        break;

      default:
        return null;
    }

    try {
      decoded = verify(token, secretKey);
    } catch (err) {
      return null;
    }
    return decoded;
  },
};
  • require("dotenv").config()
    • Node.js 애플리케이션에서 환경 변수를 로드하기 위한 코드이다.
    • dotenv는 Node.js에서 환경 변수를 설정하는 모듈이며, 이 모듈을 사용하면 .env파일을 만들어서 환경 변수를 설정하고, process.env객체를 통해 사용할 수 있다.
    • process.ev를 사용하여 .env파일에서 작성했던 salt 값을 숨겨서 사용할 수 있다.
  • const {sign, verify} = require("jsonwebtoken")
    • Node.js에서 JWT를 생성하고(sign) 검증(verify)하기 위한 모듈을 불러온다.
    • sign 함수는 JWT생성하며, 인자로 전달된 payload와 secret key를 사용하여 JWT를 생성한다.
    • verify 함수는 JWT가 유효한지 검증하며, 인자로 전달된 JWT와 secret key를 사용하여 검증을 수행한다.
  • generateToken: (user, checkedKeepLogin) => {}
    • 토큰을 생성하는 함수이며 유저에 대한 정보와 로그인에 대한 정보를 인자로 받는다.
  • const payload = {...}
    • 우리가 인자로 넘긴 user의 id와 email을 객체로 사용한다.
  • result = {...}
    • accessToken을 만드는데, 우리가 입력한 payload와 salt키를 합쳐 만든다.
    • expiresIn은 토큰의 유효 기간을 정한다.
  • if(checkedKeepLogin){...}
    • 만약 자동로그인을 선택했을 경우 refreshToken을 새롭게 만들어, 기존에 만들었던 accessToken에다가 넣어준다.
    • 마찬가지로 유효기간을 설정한다.
  • verifyToken: (type, token) => {...}
    • 만약 전달받은 type이 access일 경우, secretKey는 access를 만들 때 썼던 salt를 secretKey에 할당한다.
    • 만약 전달받은 type이 refresh일 경우, secretKey는 refresh를 만들 때 썼던 salt를 secretKey에 할당한다.
  • try{...}
    • try는 에러가 발생할 가능성이 있는 코드 블록을 정의할 때 사용한다.
    • decoded에 secretKey와 token이 일치하는지 확인한 값을 할당한다.
    • 만약 에러가 발생하면, error메세지를 출력한다.

server/controllers/users/login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const { USER_DATA } = require("../../db/data");
const { generateToken } = require("../helpers/tokenFunctions");

module.exports = async (req, res) => {
  const { userId, password } = req.body.loginInfo;
  const { checkedKeepLogin } = req.body;
  const exUser = {
    ...USER_DATA.find(
      (user) => user.userId === userId && user.password === password
    ),
  };

  if (!exUser.id) return res.status(401).send("Not Authorized");

  const { accessToken, refreshToken } = generateToken(exUser, checkedKeepLogin);
  const cookieOption = {
    domain: "localhost",
    path: "/",
    httpOnly: true,
    sameSite: "none",
    secure: true,
  };

  res.cookie("access_jwt", accessToken, cookieOption);

  if (checkedKeepLogin) {
    cookieOption.maxAge = 1000 * 60 * 24;
    res.cookie("refresh_jwt", refreshToken, cookieOption);
  }

  res.redirect("/userinfo");
};
  • const {USER_DATA} = require("../../db/data")
    • 기존에 작성했던 데이터를 불러온다.
  • const {generateToken} = require("../helper/tokenFunctions")
    • tokenFunctions에서 정의한 generateToken 함수를 불러온다.
  • const {userId, password} = req.body.loginInfo
    • 우리가 입력한 loginInfo의 userId와 password를 구조분해할당한다.
  • const {checkedKeepLogin} = req.body
    • 우리가 설정한 자동 로그인을 구조분해할당한다.
  • const exUser = {...}
    • 기존의 데이터인 USER_DATA에서 userId가 우리가 입력한 userId와 맞는지, password가 맞는지 확인하여, 일치하는 데이터를 exUser에 담는다.
  • if(!exUser.id) return res.status(401).send("Not Authorized")
    • 만약 입력한 데이터와 기존의 데이터가 일치하지 않아 exUser에 아무런 값도 없다면 에러를 내보낸다.
  • const {accessToken, refreshToken} = generateToken(exUser, checkedKeepLogin)
    • 만약 데이터가 일치하여 if문에 걸리지 않았을 시, 새로운 토큰을 만드는데 exUser와 checkedKeepLogin을 갖고 있는 Token의 accessToken과 refreshToken을 구조분해할당한다.
  • const cookieOption = {...}
    • 만들어 줄 쿠키를 정의하고, domain과 path를 설정한다.
  • res.cookie("access_jwt",accessToken,cookieOption)
    • 응답에 대한 쿠키를 만들어주는데 cookie는 메서드로, access_jwt라는 이름으로 accessToken과 cookieOption이 담긴 쿠키를 만든다.
  • if(checkedKeepLogin){...}
    • 만약 자동로그인을 선택했을 경우, refreshToken의 유효기간을 설정하고, refresh_jwt라는 이름으로 토큰을 만들어준다.
  • res.redirect("/userinfo")
    • /userinfo 경로로 리다이렉트 해준다.
    • 로그인한 유저에 응답하기 위한 endpoint로 각자의 목적에 맞는 처리를 하기 위해 사용한다.

server/controllers/users/logout.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = (req, res) => {
  const cookieOption = {
    domain: "localhost",
    path: "/",
    httpOnly: true,
    sameSite: "none",
    secure: true,
  };
  res
    .clearCookie("access_jwt", cookieOptions)
    .clearCookie("refresh_jwt", cookieOptions)
    .status(205)
    .send("logout");
};
  • res.clearCookie("access_jwt", cookieOptions). ...
    • 로그아웃 시, 부여됐던 쿠키를 모두 삭제한다.

server/controllers/users/userInfo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const { USER_DATA } = require("../../db/data");
const tokenFunctions = require("../helper/tokenFunctions");
const { verifyToken, generateToken } = require("../helper/tokenFunctions");

module.exports = async (req, res) => {
  const { access_jwt, refresh_jwt } = req.cookies;

  if (access_jwt) {
    const accessTokenPayload = verifyToken("access", access_jwt);
    if (accessTokenPayload) {
      const exUser = {
        ...USER_DATA.find((user) => user.id === accessTokenPayload.id),
      };

      delete exUser.password;
      return res.send(exUser);
    }

    if (refresh_jwt) {
      const refreshTokenPayload = verifyToken("refresh", refresh_jwt);

      if (!refreshTokenPayload) return res.status(401).send("Not Authorized");

      const exUser = {
        ...USER_DATA.find((user) => user.id === refreshTokenPayload.id),
      };

      if (!refreshTokenPayload) return res.status(404).send("Not Authorized");

      const cookieOption = {
        domain: "localhost",
        path: "/",
        httpOnly: true,
        sameSite: "none",
        secure: true,
      };

      const { accessToken } = generateToken(exUser, false);
      res.cookie("access_jwt", accessToken, cookieOption);
    }

    return res.status(401).send("Not Authorized");
  }
};
  • const {USER_DATA} =require("../../db/data")
    • 기존에 작성했던 USER_DATA를 받아온다.
  • const tokenFunctions = require("../helper/tokenFunctions")

  • const {verifyToken, generateToken} = require("../helper/tokenFunctions")
    • tokenFunctions에서 작성했던 verifyToken과 generateToken을 불러온다.
  • const {access_jwt, refresh_jwt} = req.cookies
    • 브라우저의 요청(로그인)에서 쿠키의 access_jwt와 refresh_jwt를 구조분해할당한다.
  • if(access_jwt){const accessTokenPayload = verifyToken("access",access_jwt)}
    • accessToken이 있다면 access가 유효한지 확인한다.
  • if(accessTokenPayload){const exUser = {...USER_DATA.find((user)=> user.id === accessTokenPayload.id)}}
    • 기존의 데이터에서 일치하는 유저정보를 exUser에 담는다.
    • accessTokenPayload는 accessToken이 유효하다면 실행된다.
    • delete exUser.password를 통해 예민한 정보인 비밀번호를 제외하고 일치하는 유저 정보를 return한다.
  • if(refresh_jwt){const refreshtokenPayload = verifyToken("refresh",refresh_jwt)}
    • 만약 accesToken이 없다면 refreshToken을 확인한다.
    • 들어있는 refreshToken과 우리가 부여한 refreshToken이 맞는지 확인한다.
  • if(!refreshToken) return res.status(401).send("Not Authorized.")
    • 만약 refreshToken이 일치하지 않는다면 에러를 내보낸다.
  • const exUser = {...} , if(!refreshToken) return res.status(401).send("Not Authorized.")
    • 일치하는 유저가 있는지 다시 확인하여, 없다면 에러를 내보낸다.
  • const cookieOption = {...}
    • 만약 위 if문을 모두 지나쳐, refreshToken이 유효하다는 것이 확인되면 쿠키에 새로운 accessToken을 담아낸다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.