JWT accessToken refreshToken로직

슈크림·2023년 2월 8일
2

React Native에서...

(참고) React-Native에서는 로컬스토리지중에 EncryptedStorage 라는 암호화 된 로컬스토리지를 사용함

1. 로그인 시 백앤드로부터 accessToken과 refreshToken을 발급 받고 프론트에서는 로컬스토리지에 토큰들을 담는다. (토큰은 따로따로 저장하는 것을 추천함!!! 예, accessToken은 로컬스토리지 refreshToken은 DB)


(front)
export const login = createAsyncThunk(
  'user/login',
  async (data: Object, {rejectWithValue}) => {
    try {
      const response = await axios.post('/user/login', data);
      await EncryptedStorage.setItem('accessToken', response.data.accessToken);
      await EncryptedStorage.setItem(
        'refreshToken',
        response.data.refreshToken,
      );
      return response.data;
    } catch (error: any) {
      return rejectWithValue(error.response.data);
    }
  },
);

(backend)

router.post("/login", async (req, res, next) => {
  try {
    const user = await User.findOne({
      where: { email: req.body.email },
    });

    if (!user) {
      return res.status(401).json({ message: "가입하지 않은 회원입니다." });
    }

    const passowrdsucess = await bcrypt.compare(
      req.body.password,
      user.password
    );

    if (!passowrdsucess) {
      return res.status(401).json({ message: "비밀번호가 틀렸습니다" });
    }

    const accessToken = jwt.sign(
      { sub: "access", email: req.body.email, userId: user.id },
      process.env.ACCESS_TOKEN_SECRET,
      // JWtAccessToken, // (더미데이터 용) 실제는 위에 코드처럼 꼭 Process.env에 암호화 시켜서 적용하기
      { expiresIn: "30m" }
      // { expiresIn: "30s" }
    );
    const refreshToken = jwt.sign(
      { sub: "refresh", email: req.body.email, userId: user.id },
      process.env.REFRESH_TOKEN_SECRET,
      // JWTRefreshToken, // (더미데이터 용) 실제는 위에 코드처럼 꼭 Process.env에 암호화 시켜서 적용하기
      { expiresIn: "24h" }
      // { expiresIn: "60s" }
    );

    const fulluser = await User.findOne({
      where: { id: user.id },
      attributes: {
        exclude: ["password"],
      },
      include: [
        {
          model: Post,
          attributes: ["id"],
        },
      ],
    });

    return res.status(200).json({
      user: fulluser,
      accessToken: accessToken,
      refreshToken: refreshToken,
    });
  } catch (error) {
    console.error(error);
    next(error);
  }
});

2. App.tsx혹은 AppInner.tsx에서 axios.interceptors.request.use를 사용하여 api 요청시 마다 accessToken을 보내준다.

 axios.interceptors.request.use(
     async config => {
       const accessToken = await EncryptedStorage.getItem('accessToken');

       return {
         ...config,
         headers: {
           authorization: `Bearer ${accessToken}`,
           ...config.headers,
         },
       };
     },
     async error => {
       return Promise.reject(error);
     },
   );

3. 매 요청 때 백앤드로부터 accessToken 토큰이 만료되었다는 error를 (419)받으면 로컬스토리지에서 refreshToken을 가지고 와서 accessToken을 새로 발급받는 api요청을 보내주고 refreshToken이 유효하면 accessToken을 다시 발급해주고 프론트는 새로 발급받은 accessToken을 로컬스토리지에 저장하고 다시 이전의 요청을 보낸다.

efreshToken이 만료되었다면 (420) 로그아웃 처리를 해버린다. (유저 정보를 담고있는 리덕스 초기화 혹은 로컬스토리지 초기화)

(front)
axios.interceptors.response.use(
      response => {
        // console.log('인터셉터!!!!!!!!!!!!!!');
        // console.log('response:::', response);
        return response;
      },
      async error => {
        const {
          config,

          response: { status },
        } = error;
        if (status === 419) {
          if (error.response.data.code === 'expired') {
            console.log('expired!!!!!!!!!!!!!!!!');
            const originalRequest = config;
            const refreshToken = await EncryptedStorage.getItem('refreshToken');
            // token refresh 요청
            const { data } = await axios.post(
              `${Config.API_URL}/user/refreshToken`, // token refresh api
              {},
              { headers: { authorization: `Bearer ${refreshToken}` } },
            );
            console.log('data:', data);
            // 새로운 토큰 저장
            await EncryptedStorage.setItem(
              'accessToken',
              data.data.accessToken,
            );
            originalRequest.headers.authorization = `Bearer ${data.data.accessToken}`;
            // 419로 요청 실패했던 요청 새로운 토큰으로 재요청
            // console.log('여기 갇힘!!!!!!!!');
            console.log('originalRequest::::', originalRequest);
            return axios(originalRequest);
          }
        } else if (status === 420) {
          if (error.response.data.code === 'expired') {
            dispatch(userSlice.actions.reMoveme());
            Alert.alert('알림', '다시 로그인 해주세요.');
          }
        }
        return Promise.reject(error);
      },
    );
  }, [dispatch]);



(backend)

// refreshToken이 유효한지 판단하고 유효하면 accessToken을 갱신해주는 api
router.post("/refreshToken", verifyRefreshToken, async (req, res, next) => {
  try {
    const accessToken = jwt.sign(
      { sub: "access", email: res.locals.email, userId: req.userid },

      process.env.ACCESS_TOKEN_SECRET,
      // JWtAccessToken, // (더미데이터 용) 실제는 위에 코드처럼 꼭 Process.env에 암호화 시켜서 적용하기
      { expiresIn: "30m" }
      // { expiresIn: "30s" }
    );

    const user = await User.findOne({
      where: { email: res.locals.email },
    });

    if (!user) {
      return res.status(404).json({ message: "가입되지 않은 회원입니다." });
    }
    res.json({
      data: {
        accessToken,
        email: res.locals.email,
        // name: users[res.locals.email].name,
      },
    });
  } catch (error) {
    console.error(error);
    next(error);
  }
});


미들웨어! 

verifyToken은 api 요청시 accessToken이 유효한지 판단하는 미들웨어
exports.verifyToken = (req, res, next) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ message: "토큰이 없습니다." });
  }
  try {
    const data = jwt.verify(
      req.headers.authorization.replace("Bearer ", ""),
      process.env.ACCESS_TOKEN_SECRET
      // JWtAccessToken // (더미데이터 용) 실제는 꼭 위에 코드처럼 Process.env에 암호화 시켜서 적용하기
    );
    req.userid = data.userId;
    // res.locals.userid = data.userId;
    res.locals.email = data.email;
  } catch (error) {
    console.error(error);
    if (error.name === "TokenExpiredError") {
      return res
        .status(419)
        .json({ message: "만료된 액세스 토큰입니다.", code: "expired" });
    }
    return res
      .status(401)
      .json({ message: "유효하지 않은 액세스 토큰입니다." });
  }
  next();
};


verifyRefreshToken은 refreshToken이 유효한지 판단하는 미들웨어 
exports.verifyRefreshToken = (req, res, next) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ message: "토큰이 없습니다." });
  }
  try {
    // console.log("22222222222222222");
    // console.log(" 리프레시  req.headers:", req.headers);

    const data = jwt.verify(
      req.headers.authorization.replace("Bearer ", ""),
      process.env.REFRESH_TOKEN_SECRET
      // JWTRefreshToken // (더미데이터 용) 실제는 꼭  위에 코드처럼 Process.env에 암호화 시켜서 적용하기
    );
    req.userid = data.userId;
    res.locals.email = data.email;
  } catch (error) {
    console.error(error);
    if (error.name === "TokenExpiredError") {
      console.log("리프레시토근 만료");
      return res
        .status(420)
        .json({ message: "만료된 리프레시 토큰입니다.", code: "expired" });
    }
    return res
      .status(401)
      .json({ message: "유효하지 않은 리프레시 토큰입니다." });
  }
  next();
};

RefreshToken은 언제 갱신하는가? 배달의 민족이나 인스타그램 같은 어플의 경우 자주 들어가게 되면 로그인을 다시 하라고 하지 않는다. 하지만 장 기간 어플의 접속하지 않았을 경우 로그인을 하게 되어 있다. 그렇다면 refreshToken은 언제 갱신하는가?

4. 1번의 로그인 로직을 거친 후 앱을 킬 때마다 accessToken과 refreshToken을 갱신해주는 api를 요청한다. 두 개의 api 모두 refreshToken이 유효하다면 accessToken과 refreshToken을 새로 발급해주고 프론트에서 토큰을 다시 로컬스토리지에 저장해 준다.


(만약 이때 refreshToken이 유효하지 않다면 로그아웃 처리 된다. )

장기간 앱에 접속하지 않으면 refreshToken이 만료되기 때문에 로그아웃 처리됨!!!

(front)
useEffect(() => {
    const getTokenAndRefresh = async () => {
      try {
        console.log('처음시작!!!!!!!!');
        const rtoken = await EncryptedStorage.getItem('refreshToken');
        if (!rtoken) {
          console.log('토큰이 없네요!!!!');
          return;
        }

        // accessToken을 갱신해주는 api
        const response = await axios.post(
          `${Config.API_URL}/user/refreshToken`,
          {},
          {
            headers: {
              authorization: `Bearer ${rtoken}`,
            },
          },
        );
        // refreshToken 갱신해주는 api

        const refreshresponse = await axios.post(
          `${Config.API_URL}/user/refreshRefreshToken`,
          {},
          {
            headers: {
              authorization: `Bearer ${rtoken}`,
            },
          },
        );


        await EncryptedStorage.setItem(
          'accessToken',
          response.data.data.accessToken,
        );

        await EncryptedStorage.setItem(
          'refreshToken',
          refreshresponse.data.data.refreshToken,
        );
        
       dispatch(loadMyInfo()); // 앱을 껐다가 켰을 시 유저정보를 불러오는 action
        
      } catch (error) {
        console.error(error);
        if ((error as AxiosError).response?.data.code === 'expired') {
        }
      }
    };
    getTokenAndRefresh();
  }, [dispatch]);

(backend)

// refreshToken이 유효한지 판단하고 유효하면 accessToken을 갱신해주는 api
router.post("/refreshToken", verifyRefreshToken, async (req, res, next) => {
  try {
    const accessToken = jwt.sign(
      { sub: "access", email: res.locals.email, userId: req.userid },

      process.env.ACCESS_TOKEN_SECRET,
      // JWtAccessToken, // (더미데이터 용) 실제는 위에 코드처럼 꼭 Process.env에 암호화 시켜서 적용하기
      { expiresIn: "30m" }
      // { expiresIn: "30s" }
    );

    const user = await User.findOne({
      where: { email: res.locals.email },
    });

    if (!user) {
      return res.status(404).json({ message: "가입되지 않은 회원입니다." });
    }
    res.json({
      data: {
        accessToken,
        email: res.locals.email,
        // name: users[res.locals.email].name,
      },
    });
  } catch (error) {
    console.error(error);
    next(error);
  }
});

// refreshToken이 유효한지 판단하고 유효하면 refreshToken을 갱신해주는 api
router.post(
  "/refreshRefreshToken",
  verifyRefreshToken,
  async (req, res, next) => {
    try {
      const refreshToken = jwt.sign(
        //추가한 부분
        { sub: "refresh", email: res.locals.email, userId: req.userid },
        process.env.REFRESH_TOKEN_SECRET,
        // JWTRefreshToken, // (더미데이터 용) 실제는 위에 코드처럼 꼭 Process.env에 암호화 시켜서 적용하기
        // { expiresIn: "60s" }
        { expiresIn: "24h" }
      );

      const user = await User.findOne({
        where: { email: res.locals.email },
      });

      if (!user) {
        return res.status(404).json({ message: "가입되지 않은 회원입니다." });
      }
      // if (!users[res.locals.email]) {
      //   return res.status(404).json({ message: "가입되지 않은 회원입니다." });
      // }
      // console.log("refreshToken:::", refreshToken);
      res.json({
        data: {
          refreshToken, // 추가한 부분
        },
      });
    } catch (error) {
      console.error(error);
      next(error);
    }
  }
);

5. 앱을 껐다가 키면 리덕스는 초기화 되기에 다시 유저정보를 불러올 필요가 있다 (onRefresh때는 리덕스 초기화 안됨) 이때는 위의 코드에서 dispatch(loadMyInfo());와 같은 함수를 실행해 주어 백앤드로 부터 accessToken이 유효한지 확인 후 유효하다면 유저정보를 다시 받아 리덕스에 넣어주어 로그인된 처리한다.

(frontend)

(앱 켰을 때 실행되는 함수)
useEffect(() => {
  const getTokenAndRefresh = async () => {

...
  dispatch(loadMyInfo()); // 앱을 껐다가 켰을 시 유저정보를 불러오는 action

}
  }, [dispatch]);


(리덕스 액션)
export const loadMyInfo = createAsyncThunk('user/loadMyInfo', async () => {
  const response = await axios.get('/user/loadMyinfo');
  return response.data;
});

(reducer)
.addCase(loadMyInfo.fulfilled, (state, action) => {
        state.loadMyInfoLoading = false;
        state.loadMyInfoDone = true;
        state.me = action.payload;
     })


(backend)

router.get("/loadMyInfo", verifyToken, async (req, res, next) => {
  // GET /user/loadMyInfo
  try {
    if (req.userid) {
      const fullUserWithoutPassword = await User.findOne({
        where: { id: req.userid },
        attributes: {
          exclude: ["password"],
        },
        include: [
          {
            model: Post,
            attributes: ["id"],
          },
        ],
      });
      res.status(200).json(fullUserWithoutPassword);
    } else {
      res.status(200).json(null);
    }
  } catch (error) {
    console.error(error);
    next(error);
  }
});
profile
프론트엔드 개발자입니다.

0개의 댓글