리액트 심화반 2주차(2)

2022. 2. 7. 11:44코딩공부/React

토큰 기반 인증

[옛날 이야기 - 세션 기반 인증]

예전에는 사용자의 로그인 상태서버가 전부 가지고 있었어요.

서버의 세션에 사용자 정보를 넣고 이 사람이 로그인을 했다 안했다를 전부 기록하고 기억했습니다.

이 세션은 서버의 메모리나 데이터베이스 등에 저장해두는데, 로그인한 사용자가 많아지면 서버에 부하가 많이 오겠죠? 그렇다고 서버를 여러개 놓자니 관리가 까다로워지고요.

→ 그래서 최근에는 오늘 배울 토큰 기반 인증 방법을 많이 사용해요!

 


OAuth2.0

외부서비스의 인증 및 권한부여를 관리하는 프레임워크입니다!

→ Open Authentication, Open Authorization라고 해요. (인증과 허가를 포함해요!)

 

OAuth 동작 방식 (간단 ver.)

  1. 클라이언트와 서버 사이에 인증(로그인)을 하면 서버가 access_token 을 줍니다.
  2. 클라이언트는 access_token을 이용해서 API 요청을 할 수 있어요.
  3. 서버는 API 요청을 받고, access_token을 가지고 권한이 있나 없나 확인해서 결과를 클라이언트에 보내줍니다.

OAuth 동작 방식 (외부 서비스 엮음 ver.)

유저가 구글 로그인을 하는 상황이라고 가정하고 생각해봅시다!

공식적으로 쓰는 용어와 함께 가정해볼게요. :)

 

여기서 구글유저의 정보도 가지고 있을거고, 로그인을 검증도 해줄거예요.

그러니까 구글은 Resource Server(자원 서버) + Authorization server(권한 서버)겠죠!

유저는 그 정보를 가진 사람이니 Resource Owner(자원 소유자)라고 부릅니다. (구글에서 주는 유저 정보는 Resource(자원)이라고 부릅니다.)

 

  1. 유저가 구글 로그인을 합니다.
    • 자원 소유자가 자원서버에 권한 요청을 한거죠!
  2. 구글은 로그인할 때 유저가 입력한 정보(아이디, 비밀번호 등)을 보고 클라이언트(우리 웹사이트!)에 접근 권한을 줍니다.
  3. 클라이언트는 이 권한을 가지고 Authorization server(권한 서버)에 access_token을 요청합니다.
  4. 클라이언트는 이 access_token을 가지고 구글에서 유저 정보를 가져올 수 있어요.
  5. 구글은 클라이언트가 보낸 access_token을 가지고 권한이 있나 없나 확인해서 결과를 클라이언트에 보내줍니다.

JWT(Json Web Token)

토큰의 한 형식입니다! 전자 서명이 포함되어 있다. 데이터가 JSON 형태로 이루어져 있는 토큰이에요.

 

생김새 : [header].[payload(내용)].[signature(서명)]

  • header: 토큰 타입과 암호화 방식 정보가 들어갑니다.
  • payload: 토큰에 담을 정보가 name: value 쌍으로 들어갑니다.
  • signature: 서명 정보입니다. secret key를 포함해서 header와 payload 정보가 암호화 되어 들어갑니다.

동작 방식 : 토큰 기반 동작 방식대로 움직여요!

  • 유저가 로그인을 시도하면,
  • 서버가 요청을 확인하고 secret key를 가지고 access_token을 발급합니다.
  • 클라이언트에 JWT를 전달하고
  • 클라이언트는 API 요청을 할 때 Authorization header에 JWT를 담아서 보냅니다.
  • 서버는 JWT의 서명을 확인하고 payload에서 정보를 확인해서 API 응답을 보냅니다.

JWT vs OAuth?

WT와 OAuth는 로그인에 많이 쓰이는 두 인증 방식입니다.

뭘 택할지 자주 고민하게 되지만 사실 비교하긴 조금 애매해요.

JWT는 토큰의 한 형식이고 OAuth는 프레임워크거든요.

(OAuth에서 토큰으로 JWT를 사용할 수도 있고요. 🙂 )

 


웹 저장소(feat. 토큰)

Http는 1번 요청을 하고 응답을 받으면 연결이 해제됩니다!

즉, 우리가 access_token을 클라이언트 어딘가에 저장을 해두어야한단 이야기예요!

그럼 이 토큰을 어디에 저장하면 좋을 지, 클라이언트에서 쓸 수 있는 저장소를 알아봅시다. 😉

 

쿠키

클라이언트 로컬에 저장되는 key : value 형태의 저장소입니다! 약 4KB 정도 저장할 수 있어요.

 

쿠키 만들기

어떤 사이트에서 브라우저 개발자 도구를 열고 해보자!

// key는 MY_COOKIE, value는 here, 
document.cookie = "MY_COOKIE=here;";

쿠키에 만료일 설정하는 방법

let date = new Date('2022-10-05');
let date_str = date.toUTCString();
document.cookie= "MY_COOKIE=here; expires=" + date_str;

쿠키 가져오기

document.cookie

쿠키 삭제

쿠키를 삭제할 때는 만료일을 이전으로 돌려서 지우는 방법을 많이 씁니다.

document.cookie = "MY_COOKIE=here; expires=new Date('2020-12-12').toUTCString()";

 

세션 스토리지

HTML5에서 추가된 저장소입니다! 쿠키와 마찬가지로 key:value 형태의 저장소예요.

세션 스토리지에 저장된 데이터는 브라우저를 닫으면 제거돼요!

→ 자동 로그인이나, 장바구니같은 다음에 브라우저를 열었을 때도 유지해야하는 데이터는 넣기 어렵겠죠!

 

추가하기

// key는 MY_SESSION, value는 here인 세션을 만들어요.
sessionStorage.setItem("MY_SESSION", "here");

가져오기

// key값으로 쉽게 가져올 수 있어요 :) 
sessionStorage.getItem("MY_SESSION");

삭제하기

// 하나만 삭제하고 싶다면, 이렇게 키를 통해 삭제합니다.
sessionStorage.removeItem("MY_SESSION");

// 몽땅 지우고 싶을 땐 clear()를 쓰면 됩니다. :) 
sessionStorage.clear();

 

로컬 스토리지

HTML5에서 추가된 저장소입니다! 쿠키와 마찬가지로 key:value 형태의 저장소예요.

로컬 스토리지는 따로 지워주지 않으면 계속 브라우저에 남아 있어요.

→ 유저의 아이디, 비밀번호같은 중요한 정보를 넣어두면 아주 위험해요!

 

추가하기

// key는 MY_LOCAL, value는 here인 데이터를 저장해요.
localStorage.setItem("MY_LOCAL", "here");

가져오기

// key값으로 쉽게 가져올 수 있어요 :) 
localStorage.getItem("MY_LOCAL");

삭제하기

// 하나만 삭제하고 싶다면, 이렇게 키를 통해 삭제합니다.
localStorage.removeItem("MY_LOCAL");

// 몽땅 지우고 싶을 땐 clear()를 쓰면 됩니다. :) 
localStorage.clear();

 

토큰은 어디에 저장해야할까?

예전에는 토큰을 저장할만한 곳이 쿠키 밖에 없었어요. 😢

HTML5가 나온 후부터는 LocalStorage 에도 토큰을 저장하는 일이 많아졌습니다.

 

왜 쿠키보다 로컬 스토리지에 저장했을까?

  • 쿠키보다 더 많은 정보를 저장할 수 있다. (쿠키 4KB, 로컬 스토리지 5MB)
  • 쿠키처럼 모든 http 통신에 딸려들어가지 않는다.

그럼 무조건 로컬 스토리지에 토큰을 넣을까?

  • 아니요! 로컬 스토리지는 말그대로 로컬에 데이터가 다 남아있으니 보안상 취약해지기 쉬워요. 프로젝트 성향에 맞춰 저장 장소는 그때그때 달라져야 합니다.
  • 우리는 쿠키에 저장할거예요. 😉

 

헤더 분기하고 리덕스 설치하기

헤더 컴포넌트 분기

클라이언트의 입장에서 로그인 하기는 사실 별 게 없어요.

우리 서버에 로그인 요청을 보내고, 응답을 받아서 토큰을 저장하면 끝입니다.

그럼 토큰을 저장하면서 어떤 걸 더 해야할지 지금부터 함께 봅시다.

 

로그인을 하면? 가장 먼저 생각나는 건 헤더 컴포넌트가 바뀌는 거죠!

 

- 로그인한 상태일 때 헤더 만들기

- 쿠키가 있으면 로그인한 헤더 보여주기

import React from "react";
import {Grid, Text, Button} from "../elements";
import {getCookie, deleteCookie} from "../shared/Cookie";
const Header = (props) => {

    const [is_login, setIsLogin] = React.useState(false);

    React.useEffect(() => {

        // 쿠키를 가져와요!
        let cookie = getCookie('쿠키 이름 넣기!');
        // 확인해봅시다!
        console.log(cookie);
        // 쿠키가 있으면?
        if(cookie){
            setIsLogin(true);
        }else{
            setIsLogin(false);
        }
    });

    if(is_login){
        return (
          <React.Fragment>
            <Grid is_flex padding="4px 16px">
              <Grid>
                <Text margin="0px" size="24px" bold>
                  헬로
                </Text>
              </Grid>

              <Grid is_flex>
                <Button text="내정보"></Button>
                <Button text="알림"></Button>
                <Button text="로그아웃" _onClick={() => {deleteCookie('login');}}></Button>
              </Grid>
            </Grid>
          </React.Fragment>
        );
    }
    return (
        <React.Fragment>
            <Grid is_flex padding="4px 16px">
                <Grid>
                    <Text margin="0px" size="24px" bold>헬로</Text>
                </Grid>
                
                <Grid is_flex>
                    <Button text="로그인"></Button>
                    <Button text="회원가입"></Button>
                </Grid>
            </Grid>
        </React.Fragment>
    )
}

Header.defaultProps = {}

export default Header;

 

리덕스 설치하기

# 이건 리덕스와 리덕스 모듈 내에서 경로 이동까지 하게 해줄 히스토리, 라우터와 히스토리를 엮어줄 모듈까지 한번에 설치해보는 거예요.
yarn add redux react-redux redux-thunk redux-logger history@4.10.1 connected-react-router@6.8.0

프론트엔드의 꽃 리액트 강의에서는 리듀서에서 뭔가를 바꿀 때 불변성 관리를 우리가 신경썼죠.

이번엔 우리가 신경쓰지 말고, 임머라는 패키지 사용해서 해볼거예요.

그리고 액션도 매번 맨 위에서 user/CREATE처럼 리듀서 모듈 명에 어떤 타입 넣어서 만들고,

그 아래에는 액션 생성 함수 만들어서 export 다 해주고 좀 귀찮았죠.

이걸 redux-actions라는 패키지로 좀 더 편하게 쓸거예요.

yarn add immer redux-actions

 

유저 모듈 만들기

user.js 만들기

import하기

import { createAction, handleActions } from "redux-actions";
import { produce } from "immer";

import { setCookie, getCookie, deleteCookie } from "../../shared/Cookie";

액션 타입부터 만들기

const LOG_IN = "LOG_IN";
const LOG_OUT = "LOG_OUT";
const GET_USER = "GET_USER";

액션 생성 함수 만들기

const logIn = createAction(LOG_IN, (user) => ({ user }));
const logOut = createAction(LOG_OUT, (user) => ({ user }));
const getUser = createAction(GET_USER, (user) => ({ user }));

initialState 만들기

const initialState = {
  user: null,
  is_login: false,
};

리듀서 만들기(feat.immer)

https://immerjs.github.io/immer/

 

Introduction to Immer | Immer

Immer

immerjs.github.io

export default handleActions(
  {
    [LOG_IN]: (state, action) =>
      produce(state, (draft) => {
        setCookie("is_login", "success");
        draft.user = action.payload.user;
				draft.is_login = true;
      }),
		[LOG_OUT]: (state, action) =>
      produce(state, (draft) => {
        deleteCookie("is_login");
        draft.user = null;
				draft.is_login = false;
      }),
    [GET_USER]: (state, action) => produce(state, (draft) => {}),
  },
  initialState
);

actionCreators 내보내기

const actionCreators = {
  logIn,
  getUser,
  logOut,
};

export { actionCreators };

 

리덕스 스토어 만들기

스토어 만들 때는 어떻게 하나요?

- combineReducers()를 사용해서 export한 reducer를 모아 root reducer를 만들고,

- 미들웨어를 적용해주고,

- createStore()를 사용해서 root reducer와 미들웨어를 엮어 스토어를 만든다!

 

1. import하기

2. root reducer 만들자!

3. 미들웨어도 준비하고,

4. 크롬 확장 프로그램, redux devTools도 사용 설정하기

5. 이제 미들웨어를 묶자!

6. 미들웨어하고 루트 리듀서를 엮어서 스토어를 만든다!

7. 스토어에 히스토리 넣어주기

 

스토어 주입하기

스토어를 주입할 때 Provider라는 걸 써요. Provider를 index.js에서 주입할거예요.

그 다음에는 App.js에서 원래 BrowserRouter와 Route를 써서 컴포넌트에 주입하던 history를 ConnectedRouter를 써서 리덕스랑 같은 history를 사용하도록 해줄게요. (그래야 히스토리를 공유하겠죠!)

 

1. index.js에서 스토어 주입하기

// index.js
import store from "./redux/configureStore";
import { Provider } from "react-redux";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
// app.js
...
import { ConnectedRouter } from "connected-react-router";
import { history } from "../redux/configureStore";
...
function App() {
  return (
    <React.Fragment>
      <Grid>
        <Header></Header>
        <ConnectedRouter history={history}>
          <Route path="/" exact component={PostList} />
          <Route path="/login" exact component={Login} />
          <Route path="/signup" exact component={Signup}/>
        </ConnectedRouter>
      </Grid>
    </React.Fragment>
  );
}
...

 

2. 로그인 페이지에서 리덕스 훅 사용하여 로그인 액션 실행하기

3. 헤더에서 리덕스 훅 사용하여 스토어 데이터 보기

 

'코딩공부 > React' 카테고리의 다른 글

리액트 심화반 3주차(1)  (0) 2022.02.08
리액트 심화반 2주차(3)  (0) 2022.02.07
리액트 심화반 2주차(1)  (0) 2022.02.05
리액트 심화반 1주차  (0) 2022.02.05
리액트 기초반 4주차  (0) 2022.02.03