프론트에서의 로그인/회원가입 진행과정

제론·2022년 8월 16일
2

로그인과 회원가입은 어떻게 이루어질까?!

백과 프론트의 협업이 잘 이루어져야 하는 부분이 로그인/회원가입입니다.
사용자의 개인정보를 다루어야 하기 때문에 보안이 매우 중요하고 사용자에 따른 접근 권한과 특정 리소스에만 접근 가능하게 하는 과정이 하나의 서비스에서 필수적인 부분이라 그렇습니다.

프론트에서는 로그인/회원가입을 어떻게 진행하는지 살펴보도록 하겠습니다!

  • 필요성
    • 보해야할 정보(content)가 있기 때문에
      • 모든 사용자가 모든 정보 접근 권한을 가져서는 안됨
      • 예시
      1. 특정 페이지 접근 제한
        • content → 특정 페이지(프로필페이지) → 비밀번호 변경
        • 로그인한 사람만 프로플 페이지에 들어갈 수 있게 접근 제한을 걸어야함
      2. 데이터베이스 저장된 데이터
        • 미인증 사용자의 접근 제한 - API 엔드포인트를 통해서
  • 인증 작동 원리
    1. 사용자가 접근 허가를 받음 - 로그인을 통해서
      1. 로그인을 통해 자격증명 → 데이터 서버 → 서버에서 유효성 검증 → 접근 허가
        ⇒ 사용자는 특정 페이지 접근 가능, 유저 정보를 통해 특정 엔드포인트 요청 가능
    2. 서버가 검증하는 방식
      1. Server-side Sessions
        1. 서버가 특정 클라이언트의 고유 ID를 저장
        2. 클라이언트도 이 ID를 서버로부터 받고 요청시 같이 보냄
      2. Authentication Tokens
        1. 자격 증명시 서버가 사용자의 모든 정보를 담은 하나의 문자열인 인코딩된 토큰을 생성함
        2. 서버만 아는 비밀기로 해싱해서 토큰을 생성(비밀키는 서버만 알고 있음)
        3. 만들어진 토큰은 서버에 저장되지 않고 사용자에게 전달됨(서버는 토큰생성 방법만 안다)
        4. 해당 토큰을 첨부해 후속 요청을 보낸다
        5. 서버는 토큰에 대한 검증을 진행함
  • 인증을 전역 상태관리 툴로 관리하는 이유!
    • 로그인을 하고 받은 token은 모든 컴포넌트에서 사용될 수 있기 때문에!
      • 로그인 되었을 경우에만 조건부 렌더링을 걸어줄 수 있다.
        • 예시 - 로그인 되었을 때만, logout, profile 버튼이 렌더링 됨!
          import { Link } from 'react-router-dom'
          import { useContext } from 'react'
          import { AuthContext } from '../../store/auth-context'
          import classes from './MainNavigation.module.css'
          
          const MainNavigation = () => {
            const authCtx = useContext(AuthContext)
          
            const isLoggedIn = authCtx.isLoggedIn
            return (
              <header className={classes.header}>
                <Link to="/">
                  <div className={classes.logo}>React Auth</div>
                </Link>
                <nav>
                  <ul>
                    {!isLoggedIn && (
                      <li>
                        <Link to="/auth">Login</Link>
                      </li>
                    )}
                    {isLoggedIn && (
                      <li>
                        <Link to="/profile">Profile</Link>
                      </li>
                    )}
          
                    {isLoggedIn && (
                      <li>
                        <button>Logout</button>
                      </li>
                    )}
                  </ul>
                </nav>
              </header>
            )
          }
          
          export default MainNavigation
      • 보호된 리소스 요청에서 토큰 활용
        • 예시 - 비밀번호 변경 요청
          import classes from './ProfileForm.module.css'
          import { useContext, useRef } from 'react'
          import AuthContext from '../../store/auth-context'
          
          const ProfileForm = () => {
            const newPasswordInputRef = useRef()
            const authCtx = useContext(AuthContext)
          
            const submitHandler = (event) => {
              event.preventDefault()
              const entredNewPassword = newPasswordInputRef.current.value
          
              // add validation
          
              fetch(
                'https://identitytoolkit.googleapis.com/v1/accounts:update?key=AIzaSyCDF7hXMozn3Eq-mRlgmUH8It12KIFGh9Y',
                {
                  method: 'POST',
                  body: JSON.stringify({
                    idToken: authCtx.token,
                    password: entredNewPassword,
                    returnSecureToken: false
                  }),
                  headers: {
                    'Content-Type': 'application/json'
                  }
                }
              ).then((res) => {
                // assumption: Always succeeds!
              })
            }
          
            return (
              <form className={classes.form} onSubmit={submitHandler}>
                <div className={classes.control}>
                  <label htmlFor="new-password">New Password</label>
                  <input type="password" id="new-password" minLength="6" ref={newPasswordInputRef} />
                </div>
                <div className={classes.action}>
                  <button>Change Password</button>
                </div>
              </form>
            )
          }
          
          export default ProfileForm
      • 리다이렉팅
        • 예시 - 로그인 되었을 때 메인페이지로 이동
          • loginHandler 함수에 useNaviation을 사용해서 메인페이지로 넘기기
      • 로그아웃
        • JWT 방식은 서버에 유저 정보가 저장되지 않으므로 프론트쪽 state만 바꿔주면됨!
        • 즉, 전역상태로 관리하는 토큰의 상태 값을 비워주면된다.
        • 예시
          const logoutHandler = () => {
              setToken(null)
            }
      • 네비게이션 가드
        • 문제: 직접 url에 프로필 페이지 입력시 접근할 수 있음
        • 해결: 로그인 여부에 따라 라우트 설정을 동적으로 변경
        • 예시 - state에 따라 조건부 걸어주기
          import { useContext } from 'react'
          import { Switch, Route, Redirect } from 'react-router-dom'
          
          import Layout from './components/Layout/Layout'
          import UserProfile from './components/Profile/UserProfile'
          import AuthPage from './pages/AuthPage'
          import HomePage from './pages/HomePage'
          import AuthContext from './store/auth-context'
          
          function App() {
            const authCtx = useContext(AuthContext)
          
            return (
              <Layout>
                <Switch>
                  <Route path="/" exact>
                    <HomePage />
                  </Route>
                  {!authCtx.isLoggedIn && (
                    <Route path="/auth">
                      <AuthPage />
                    </Route>
                  )}
                  <Route path="/profile">
                    {authCtx.isLoggedIn && <UserProfile />}
                    {!authCtx.isLoggedIn && <Redirect to="/auth" />}
                  </Route>
                  <Route path="*">
                    <Redirect to="/" />
                  </Route>
                </Switch>
              </Layout>
            )
          }
          
          export default App
      • 로그인이 만료시간 만큼 저장되도록
        • 로컬스토리지 - 브라우저에 있는 저장소(XSS 공격에 취약)
        • 예시 - 로컬스토리지에 토큰 저장, 토큰 초기화
          import React, { useState } from 'react'
          
          export const AuthContext = React.createContext({
            token: '',
            isLoggedIn: false,
            login: (token) => {},
            logout: () => {}
          })
          
          export const AuthContextProvider = (props) => {
            const initialToekn = localStorage.getItem('token')
            const [token, setToken] = useState(initialToekn)
            // !! ->  truthy falsy 값을 ture나 false인 불리언 값으로 바꿔줌
            // token이 빈 문자열이 아니라면 -> true로 바꿔줌, 빈 문자열이라면 -> false로 바꿔줌
            console.log(token)
          
            const userIsLoggedIn = !!token
          
            const loginHandelr = (token) => {
              setToken(token)
              localStorage.setItem('token', token)
            }
          
            const logoutHandler = () => {
              setToken(null)
              localStorage.removeItem('token')
            }
          
            const contextValue = {
              token: token,
              isLoggedIn: userIsLoggedIn,
              login: loginHandelr,
              logout: logoutHandler
            }
          
            return <AuthContext.Provider value={contextValue}>{props.children}</AuthContext.Provider>
          }
          
          export default AuthContext
        • 쿠키
      • 자동 로그아웃
        • expirationTime을 통해 토큰의 만료시간 계산
        • 예시 - 만료시간 계산 helper 함수
          const calculateRemainingTime = (expirationTime) => {
            // 현재 시간
            const currentTime = new Date().getTime()
            // 만료 시간
            const adjExpirationTime = new Date(expirationTime).getTime()
            // 남은 시간
            const remainingDuration = adjExpirationTime - currentTime
          
            return remainingDuration
          }
        • 로그인 했을 때 현재시간 기준 만료시간 계산
          .then((data) => {
                  // 현재 시간 기준 만료시간 구하기(Date 객체로 만둘어줌)
                  const expirationTime = new Date(new Date().getTime() + +data.expiresIn * 1000)
                  authCtx.login(data.idToken, expirationTime.toISOString())
          
                  // 사용자를 다른 페이지로 리다이렉션, replace -> 뒤로가기 버튼X
                  history.replace('/')
                })
        • 사용자가 새로고침을 했을때 시간 다시 계산
          • cf) setTimeout은 해당 타이머의 식별자를 리턴한다. 그 식별자를 통해서 cleatTimeout 함수로 타이머를 정지시킬 수 있다.
        • 직접 로그아웃 했을 때 clearTimeout
        • 줄친 건 나중에 적용.. 너무 어렵
profile
Software Developer

0개의 댓글