ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • React/Jwt 연동 (access token, refresh token)
    Language/React 2022. 8. 10. 23:34

    https://github.com/dchkang83/project-board

     

    GitHub - dchkang83/project-board

    Contribute to dchkang83/project-board development by creating an account on GitHub.

    github.com

    redux-saga 및 axios도 활용해 보려다가 기본에 충실하고 너무 복잡하지 않기위해서 패스!! (이부분은 나중에라도 충분이 넣을수 있으니!)

     

    - access token

    탈취 위험이 있어 Redux를 이용하여 store에 저장 

     

    - refresh token : 

    유효기간까지 설정할수 있는  localstorage가 아닌 Cookie에 저장

     

    - refresh token을 통하여 refresh token 및 accces token 갱신

    새로고침이나 브라우저 이동시에는 refresh token 을 활용하여 refresh token 및 accces token 모두 갱신하는 방법으로 진행한다.

     

    1. index.js & redux store & reducer

    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import { BrowserRouter as Router } from "react-router-dom";
    import { Provider } from 'react-redux';
    import store from '~/store'
    
    import App from '~/App'
    
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(
      <Provider store={store}>
        <Router>
          <App />
        </Router>
      </Provider>
    );
    import { configureStore } from "@reduxjs/toolkit";
    import rootReducer from "~/store/slices/rootReducer";
    
    const initialState = {};
    const store = configureStore({
        reducer: rootReducer,
        devTools: true,
        preloadedState: initialState,
    });
    
    export default store;
    import { combineReducers } from 'redux';
    import { authReducer } from '~/store/slices/authSlice';
    
    const rootReducer = combineReducers({
      authReducer
    });
    
    export default rootReducer;

     

    2. app & router (public, private)

    import React from 'react';
    import RootRoutes from '~/routes';
    
    const App = () => {
      return (
        <React.Suspense>
          <RootRoutes />
        </React.Suspense>
      );
    }
    
    export default App;
    // This is a React Router v6 app
    import { Routes, Route, Navigate } from "react-router-dom";
    import Login from '~/views/Login';
    import Home from '~/views/Home';
    import Logout from '~/views/Logout';
    
    import PublicRoute from './PublicRoute';
    import PrivateRoute from './PrivateRoute';
    
    const RootRoutes = () => (
      <Routes>
        <Route element={<PublicRoute />}>
          <Route path="/login/" element={<Login />} />
        </Route>
    
        <Route element={<PrivateRoute />}>
            <Route path="/" element={<Home />} />
            <Route path="/logout" element={<Logout />} />
        </Route>
      </Routes>
    )
    
    export default RootRoutes;
    const theme = createTheme();
    export default function PublicRoute() {
      const location = useLocation();
      const { isAuth } = CheckToken(location.key);
    
      if (isAuth === 'Success') {
        return (
          <Navigate to="/" state={{ from: location }} />
        )
      } else if (isAuth === 'Loading') {
        return <Loading />
      }
    
      return (
        <ThemeProvider theme={theme}>
          <Container component="main" maxWidth="xs">
            <Outlet />
          </Container>
        </ThemeProvider>
      )
    }
    const theme = createTheme();
    export default function PrivateRoute() {
      const location = useLocation();
      const { isAuth } = CheckToken(location.key);
    
      if (isAuth === 'Failed') {
        return (
          <Navigate to="/login" state={{ from: location }} />
        )
      } else if (isAuth === 'Loading') {
        return <Loading />
      }
    
      return (
        <ThemeProvider theme={theme}>
          <Container component="main" maxWidth="xs">
            <Outlet />
          </Container>
        </ThemeProvider>
      )
    }

     

    3. Views

    function Home() {
    
      return(
          <div>
              Home
          </div>
      );
    }
    
    export default Home
    import React from 'react';
    import { useNavigate } from 'react-router-dom';
    import { useDispatch } from 'react-redux';
    import { useForm } from 'react-hook-form';
    
    import Avatar from '@mui/material/Avatar';
    import Button from '@mui/material/Button';
    import TextField from '@mui/material/TextField';
    import FormControlLabel from '@mui/material/FormControlLabel';
    import Checkbox from '@mui/material/Checkbox';
    import Link from '@mui/material/Link';
    import Grid from '@mui/material/Grid';
    import Box from '@mui/material/Box';
    import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
    import Typography from '@mui/material/Typography';
    
    import { setRefreshToken } from '~/utils/Cookie';
    import { loginUser } from '~/api/Auth';
    import { authActions } from '~/store/slices/authSlice';
    
    function Copyright(props) {
      return (
        <Typography variant="body2" color="text.secondary" align="center" {...props}>
          {'Copyright © '}
          <Link color="inherit" href="https://github.com/dchkang83">
            Your Website
          </Link>{' '}
          {new Date().getFullYear()}
          {'.'}
        </Typography>
      );
    }
    
    function Login() {
      const navigate = useNavigate();
      const dispatch = useDispatch();
    
      // useForm 사용을 위한 선언
      const { register, setValue, formState: { errors }, handleSubmit } = useForm();
    
      // submit 이후 동작할 코드
      // 백으로 유저 정보 전달
      const onValid = async ({ username, password }) => {
        const response = await loginUser({ username, password });
    
        if (response.status) {
          console.log('response.jwtTokens : ', response.jwtTokens);
    
          // console.log('response.test : ', test);
    
    
          // 쿠키에 Refresh Token, store에 Access Token 저장
          /*
          setRefreshToken(response.json11.refresh_token);
          dispatch(authActions.setAccessToken(response.json11.access_token));
          */
    
          dispatch(authActions.setAccessToken(response.jwtTokens.access_token));
          setRefreshToken(response.jwtTokens.refresh_token);
    
    
          return navigate("/");
        } else {
          console.log(response.json11);
        }
    
        // input 태그 값 비워주는 코드
        setValue("password", "");
      };
    
      return (
        <>
          <Box
            sx={{
              marginTop: 8,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
            }}
          >
            <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
              <LockOutlinedIcon />
            </Avatar>
            <Typography component="h1" variant="h5">
              Sign in
            </Typography>
            <Box component="form" onSubmit={handleSubmit(onValid)} noValidate sx={{ mt: 1 }}>
              <TextField
                {...register("username", { required: "Please Enter Your Email" })}
                label="Email Address"
                margin="normal"
                required
                fullWidth
                id="email"
                name="email"
                autoComplete="email"
                autoFocus
                value={"customer@naver.com"}
              />
              <TextField
                {...register("password", { required: "Please Enter Your Password" })}
    
                label="Password"
                margin="normal"
                required
                fullWidth
                name="password"
                type="password"
                id="password"
                autoComplete="current-password"
                value={"test1234"}
              />
              <FormControlLabel
                label="Remember me"
                control={<Checkbox value="remember" color="primary" />}
              />
    
              <Button
                type="submit"
                fullWidth
                variant="contained"
                sx={{ mt: 3, mb: 2 }}
              >Sign in</Button>
    
              <Grid container>
                <Grid item xs>
                  <Link href="#" variant="body2">
                    Forgot password?
                  </Link>
                </Grid>
                <Grid item>
                  <Link href="#" variant="body2">
                    {"Sign Up"}
                  </Link>
                </Grid>
              </Grid>
            </Box>
          </Box>
          <Copyright sx={{ mt: 8, mb: 4 }} />
        </>
      );
    }
    
    export default Login;
    import { useEffect } from 'react';
    import { useNavigate } from 'react-router';
    import { useDispatch, useSelector } from 'react-redux';
    import { Link } from 'react-router-dom';
    
    import { getCookieToken, removeCookieToken } from '~/utils/Cookie';
    import { authActions } from '~/store/slices/authSlice';
    import { logoutUser } from '~/api/Auth';
    
    function Logout() {
        const { accessToken } = useSelector(state => state.token);
    
        const dispatch = useDispatch();
        const navigate = useNavigate();
    
        const refreshToken = getCookieToken();
    
        async function logout() {
            const data = await logoutUser({ refresh_token: refreshToken }, accessToken);
    
            if (data.status) {
                dispatch(authActions.delAccessToken());
                removeCookieToken();
                return navigate('/login');
            } else {
                window.location.reload();
            }
        }
        useEffect( () => {
            logout();
        }, [])
    
        return (
            <>
                <Link to="/login" />
            </>
        );
    }
    
    export default Logout;

     

    4. APIS

    const TIME_OUT = 300 * 1000;
    
    const statusError = {
      status: false,
      json: {
        error: ["연결이 원활하지 않습니다. 잠시 후 다시 시도해 주세요"]
      }
    };
    
    const requestPromise = (url, option) => {
      return fetch(url, option);
    };
    
    const timeoutPromise = () => {
      return new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), TIME_OUT));
    };
    
    const getPromise = async (url, option) => {
      return await Promise.race([
        requestPromise(`http://localhost:8080${url}`, option),
        timeoutPromise()
      ]);
    };
    
    export const loginUser = async (credentials) => {
      const option = {
        method: 'POST',
        headers: {
          // 'Content-Type': 'application/json;charset=UTF-8'
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(credentials)
      };
    
      const data = await getPromise('/login', option).catch(() => {
        return statusError;
      });
    
      if (parseInt(Number(data.status) / 100) === 2) {
        const status = data.ok;
        const code = data.status;
        const text = await data.text();
        const json11 = text.length ? JSON.parse(text) : "";
    
        console.log('response.headers.authorization : ', data.headers.get('authorization'));
        for (let header of data.headers.entries()) {
          console.log(header);
        }
    
        const jwtTokens = {
          access_token: data.headers.get('authorization'),
          refresh_token: data.headers.get('refreshtoken'),
        };
    
        return {
          status,
          code,
          json11,
          jwtTokens
        };
      } else {
        return statusError;
      }
    };
    
    export const logoutUser = async (credentials, accessToken) => {
      const option = {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json;charset=UTF-8'
        },
        body: JSON.stringify(credentials)
      };
    
      const data = await getPromise('/logout-url', option).catch(() => {
        return statusError;
      });
    
      if (parseInt(Number(data.status) / 100) === 2) {
        const status = data.ok;
        const code = data.status;
        const text = await data.text();
        const json = text.length ? JSON.parse(text) : "";
    
        return {
          status,
          code,
          json
        };
      } else {
        return statusError;
      }
    }
    
    export const requestToken = async (refreshToken) => {
      const option = {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json;charset=UTF-8',
          'X-REFRESH-TOKEN': refreshToken
        },
        // body: JSON.stringify({ refresh_token: refreshToken })
      }
    
      const data = await getPromise('/api/v1/auth/refresh', option).catch(() => {
        return statusError;
      });
    
      if (parseInt(Number(data.status) / 100) === 2) {
        const status = data.ok;
        const code = data.status;
        const text = await data.text();
        const json = text.length ? JSON.parse(text) : "";
        
        const jwtTokens = {
          access_token: data.headers.get('authorization'),
          refresh_token: data.headers.get('refreshtoken'),
        };
    
        return {
          status,
          code,
          json,
          jwtTokens
        };
      } else {
        return statusError;
      }
    };
    import { useEffect, useState } from 'react';
    import { useDispatch, useSelector } from 'react-redux';
    import { getCookieToken, removeCookieToken } from '~/utils/Cookie';
    import { requestToken } from '~/api/Auth';
    import { setRefreshToken } from '~/utils/Cookie';
    
    import { authActions } from '~/store/slices/authSlice';
    
    export function CheckToken(key) {
      const [ isAuth, setIsAuth ] = useState('Loaded');
      const { authenticated, accessToken, expireTime } = useSelector(state => state.authReducer);  
      const refreshToken = getCookieToken();
      const dispatch = useDispatch();
      
      useEffect(() => {
        const checkAuthToken = async () => {
          if (refreshToken === undefined) {
            dispatch(authActions.delAccessToken());
            setIsAuth('Failed');
          } else {
            setIsAuth('Success');
    
            if (authenticated && new Date().getTime() < expireTime) {
              setIsAuth('Success');
            } else {
              const response = await requestToken(refreshToken);
    
              if (response.status) {
                const token = response.json.access_token;
                dispatch(authActions.setAccessToken(response.jwtTokens.access_token));
                setRefreshToken(response.jwtTokens.refresh_token);
    
                setIsAuth('Success');
              } else {
                dispatch(authActions.delAccessToken());
                removeCookieToken();
                setIsAuth('Failed');
              }
            }
          }
        };
        checkAuthToken();
      }, [refreshToken, dispatch, key]);
    
      return {
        isAuth
      };
    }

     

    - back end는 spring boot + jwt 형태로 작업된 포스팅을 참조

    참조)

      - https://binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial

     

    [React][CRUD] create-board-tutorial

    * 더 쉽고 유익한 튜토리얼 v2가 4월 업데이트 되었습니다! binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial-v2 [React][CRUD] create-board-tutorial-v2 * 인용시 출처 부탁드립니다. 완성 소..

    binaryjourney.tistory.com

     

    댓글

Designed by Tistory.