Auth 적용하기

jh_leitmotif·2021년 8월 11일
0

이것저것..TIL

목록 보기
5/9
post-thumbnail

🧐 개요

웹 서비스에는 로그인이 필요한 기능이 있습니다.

따라서 로그인하지 않은 비사용자의 경우 접근할 수 없도록 막아

의도치않은 상황을 방지해야합니다.

DB에 이상한 값이 들어간다든가..

이 포스트는 React의 hoc(고차 컴포넌트)와 Node의 Session을 이용해

Auth를 적용하는 방법을 정리합니다.

📋 Session 생성하기 - Node

세션은 서버 측에서 관리하는 쿠키라고 비유할 수 있을 것 같습니다.
클라이언트의 구분을 위해 세션 ID를 부여하며, 브라우저 종료 전까지 세션을 유지합니다.

세션과 쿠키에 대한 정리는 다른 포스트에서 진행하고, 우선 Node에서의 Session 생성을 후술합니다.

npm install express-session       // 세션 관리용 미들웨어
npm install express-mysql-session // MySQL에 세션 저장
npm install session-file-store    // 파일에 세션 저장
npm install fortune-session       // MongoDB, Redis, Postgres, NeDB
npm install connect-mongo         // MongoDB

세션은 서버 메모리에 저장됩니다. 즉, 서버가 종료되면 초기화된다는 뜻이기에 그것을 방지하기 위해 DB와 연동하곤 합니다.

npm install express-session  

기본적으로 위의 모듈을 설치해야 세션을 다룰 수 있습니다.
그 뒤에는 개발자가 연동한 DB에 따라 설치하는 모듈이 달라질 것입니다.

저는 현재 연동된 MySQL을 기준으로 서술합니다.

const session = require('express-session');
	// 세션 미들웨어 선언
const MySQLStore = require('express-mysql-session')(session);
	// 세션값 저장소 선언 ( MySQL )

app.use(session({
    secret              : configF.secret, 
    resave              : false,
    saveUninitialized   : false,
    secure              : true,
    HttpOnly            : true,
    store               : new MySQLStore({
        host    : configL.host,
        port    : 3306,
        user    : configL.user,
        password: configL.password,
        database: configL.database
    })
}));

사용법은 위와 같습니다.
각 값들은 따로 Config 파일에서 꺼내오는 것으로 해두었습니다.

아래는 옵션에 대한 설명입니다.

  1. secret : 쿠키의 변조를 막는 암호화 값. 세션을 암호화함
  2. resave : 매 Request마다 세션을 재저장.
  3. saveUninitialized : 초기화되지 않은 세션을 미리 저장함.
  4. secure : https 환경에서만 세션을 통신함
  5. HttpOnly : 클라이언트에서 쿠키 확인 가능 여부 지정.
  6. store : 세션이 저장될 저장소를 지정함

이 외에도 여러가지 옵션들이 있습니다. 아래의 링크에서 확인 가능합니다.

아무튼, 이렇게 세션을 생성하면 필요할 때 세션 변수를 설정하여 관리하면 됩니다.

crypto.pbkdf2(userpw,results[0].salt,108326,64,'sha512',(err,key)=>{
          const realPW = key.toString('base64');
          if (realPW==results[0].pw){
              req.session.displayName=userid;
              console.log('Auth complete');
                       

로그인 라우터의 한 부분입니다.

클라이언트가 입력한 값을 암호화했을 때, DB에 저장된 값과 일치하는 경우

세션의 diplayName 변수에 사용자의 ID를 저장합니다.

이와 같이 세션 변수를 지정할 수 있습니다.

req.session.[변수 이름] = 값

세션 변수를 전역 변수처럼 활용할 수 있습니다.

마지막으로 세션을 삭제하는 것은 다음과 같습니다.

req.session.destroy(); // 로그아웃 라우터에 선언합니다.

📋 프론트엔드에 전달

app.get('/api/getSession',(req,res)=>{
    if (typeof req.session.displayName!=='undefined'){
        res.send({isAuth:true,ID:req.session.displayName});
    }else{
        res.send({isAuth:false});
    }

선언되지 않은 세션 변수의 타입은 undefined 입니다.

그러므로 타입 값에 따라 분기하여 isAuth값과 ID값을 전달합니다.

📋 로그인 검증 - React

먼저 시도해본 것은 받아온 isAuth 값에 따라 렌더링될 요소를 다르게하는 것입니다.

const [Session,setSession] = useState("")

useEffect(()=>{
        axios.get('/api/getSession')
        .then(response=>{
            setSession(response.data);
        })
    },[])
    
const sessionValue = (Session) =>{
    if (Session.length>0){
        return true;
    }else{
        return false;
    }
}

return (
 {sessionValue(Session) ?
 	<div>
    	로그인된 유저입니다.
	</div>
    :
	<Redirect to='forbidden'/>
 
)

위와 같은 방법으로 검증할 수는 있지만
세션을 가져오는 데에 아주 약간의 딜레이가 있습니다.

따라서 Session이 false인 경우의 페이지가 잠깐 렌더링되었다가, 다시 true인 경우의 페이지로 렌더링됩니다.

물론 Redirect 시키지 않고 빈 div 태그로 감싸놓아도 되겠지만
완벽한 방식이 아니며, 불필요하게 세션을 가져오는 코드를 작성해야합니다.

따라서 고차 컴포넌트, HOC 방식을 채용했습니다.

📋 HOC - React

컴포넌트를 작성하다보면 중복되는 공통기능이 있습니다.

웹소켓, Navbar.. 등등이 있을 것 같습니다.

그러한 기능들을 최상위 컴포넌트로 두고, 해당 기능이 필요한 컴포넌트로 뿌려주기만 하면 되는 것입니다.

이 포스트에서는 로그인 검증과 관련된 hoc을 생성합니다.

📎 HOC Action / Reducer

export function auth(){
    const request=axios.get('/api/getSession')
    .then(response=>response.data);

    return {
        type:AUTH_USER,
        payload: request
    }
// user_action.js

먼저 auth action을 선언합니다.
axios.get을 통해 미리 서버에 만들어둔 /api/getSession에 접근하여 값을 얻는 동작을 작성합니다.

case AUTH_USER:
            return {...state, authData: action.payload}
            break;
            
// user_reducer.js

마찬가지로 reducer를 작성합니다.
여러가지 action이 있으므로 switch 문으로 작성되어 있습니다.


📎 HOC function

export default function (SpecificComponent, option, adminRoute = null){
    function AuthenticationCheck(props){
      const dispatch = useDispatch();
      useEffect(()=>{
        dispatch(auth())
        .then(response=>{
            if (response.payload.isAuth){
                if (option==false){
                    props.history.push('/authError');
                }
            }else{
                if (option==true && SpecificComponent.name!=='Logout'){
                    props.history.push('/authError');
                }
            }
        })
    },[])

    return <SpecificComponent idx={props.match.params.idx} filter={props.match.params.filter} text={props.match.params.text} name={props.match.params.name}/>
    }
  return AuthenticationCheck
}

// auth.js

먼저 Parameter에 대해 설명합니다.

  1. SpecificComponent : 적용될 컴포넌트
  2. option : 로그인 여부에 따른 접속 가능 여부
    2-1. true : 로그인 유저만 접속 가능
    2-2. false : 로그인하지 않은 유저만 접속 가능
    2-3. null : 누구나 접속 가능
  3. adminRoute : 관리자 접속 여부
dispatch(auth())
.then(response=>{
      if (response.payload.isAuth){
          if (option==false){
              props.history.push('/authError');
          }
      }else{
          if (option==true && SpecificComponent.name!=='Logout'){
              props.history.push('/authError');
          }
       }

auth Action의 isAuth값을 통해 로그인 여부를 확인합니다.

또한 option값을 통해 접근 가능 여부를 결정합니다.

3번째 인자인 adminRoute값을 통해 관리자 페이지 등의 접근 가능 여부를 제어할 수 있습니다.

저는 따로 만든 공통 forbidden 페이지로 이동시키도록 해두었습니다.

로그아웃의 경우 landingPage로 Redirect 시키도록 되어있는데, 
Redirect 직전에 isAuth값이 false가 되어 authError가 발생합니다.
임시조치로 logout의 경우 authError가 발생하지 않도록 막아두었습니다.
아예 logout 라우터의 옵션을 null로 두어도 되지만
req.session.destroy()의 의미없는 호출은 비효율적이라고 판단했습니다.

📋 App.js

<Route exact path="/" component={Auth(LandingPage)}/>
<Route path="/authError" component={Forbidden}/>
<Route path="/login" component={Auth(LoginPage,false)}/>
<Route path="/register" component={Auth(RegisterPage,false)}/>
<Route path="/logout" component={Auth(Logout,true)}/>
<Route path="/board/list/:idx" component={Auth(Board)}/>

// App.js

Route 부분에는 위와 같이 Auth로 컴포넌트를 감싸놓으면 됩니다.

여기서 주의해야될 것은 match params로 넘긴 값의 처리입니다.

<Route path="/board/list/:idx" component={Auth(Board)}/>

예를 들어, idx params는 Board가 아닌 Auth 컴포넌트로 전달됩니다.

return <SpecificComponent idx={props.match.params.idx} filter={props.match.params.filter} text={props.match.params.text} name={props.match.params.name}/>

따라서 위와 같이 컴포넌트에 props로 넘겨주어야 합니다.

무분별하게 props가 많아지는 현상을 방지하기 위해

경우에 따라 Auth를 감싸지 않는 것을 고려하고 있습니다.


profile
Define the undefined.

0개의 댓글