상세 컨텐츠

본문 제목

[TIL] 로그인의 역사 // 220214

TIL

by KBstar⭐ 2022. 2. 15. 19:06

본문

⭐ 로그인의 역사 ➡ Login

⭐ JWT 토큰 ➡JWT Token

⭐ 암호화 방식 2가지 ➡Encrypt / Hash

⭐ 로그인 인증 토큰은 어디에 저장을 할까? ➡ Context-API

 

기존 로그인 방식

1) 브라우저에서 BE에 로그인을 시도한다(email, pwd)

2) BE에서 login-api를 통해 회원 테이블에서 해당하는 데이터를 찾는다.

3) BE 메모리(Session)에 유저가 로그인한 데이터를 저장한다. ➡ Session : BE의 변수

4) 데이터에 대한 Token(ID: ajkdj-12)를 생성해서 브라우저에 되돌려준다.

5) 유저가 로그인한 후 createPayment를 요청하게 될 땐 ID값 즉, Token(ajkdj-12) 값을 다시 BE로 보내준다.

6) BE에선 ID(ajkdj-12)를 통해 유저가 누구인지를 인지하게 된다.

 

하지만 기존 로그인 방식에는 많은 문제점이 발생하였다.

BE에 많은 사용자가 몰릴 경우❗

1) BE의 cpu와 mem 스펙을 올려줌으로써 해결 - scale up이라고 한다

* cpu : 사용자의 접속 및 요청을 빠르게 처리하게끔 해준다

* mem : cpu를 도와 계산하거나 저장하기 위한 공간

2) scale up을 해도 문제가 발생 -> scale out으로 해결해준다

* scale out : 스펙이 똑같은 BE들을 수평으로 복사 후 확장시켜준다

3) scale out을 해도 문제가 발생

- 기존 BE엔 로그인한 유저들의 데이터가 저장된 Session이 있다

- 즉, BE를 그대로 복사를 한다고 하더라도 API를 나누기가 쉽지가 않다

- 이러한 상태를 stateful라고 하며 상태를 가지고 있다고 표현한다

4) 이후 stateful을 해결하기 위해 DB에 데이터를 저장하기 시작했다 

- DB에 데이터를 저장함으로써 scale out이 가능해짐

- stateful이 없어졌기 때문에 가능!

- stateless라고 표현한다(상태가 없음)

5) 하지만 DB에 저장을 해줘도 여전히 문제점이 발생하였다

- 파티셔닝을 통해 해결

6) 테이블 파티셔닝

- 수직 파티셔닝 : 하나의 테이블에 유저의 모든 정보가 담겨 있고 그 정보를 수직 형태로

반으로 나누고 나눈 데이터를 관리하는 방식(테이블을 수직으로 잘라냄)

- 수평 파티셔닝(샤딩) : 1~100번까지는 테이블 1, 101번~200번까지는 테이블 2 이런 식으로 

데이터를 나눈다(테이블을 수평으로 잘라냄)

 

현대 로그인 방식

1. 토큰을 DB에 저장해 두고 Redis와 같이 사용해준다

- disk에 저장될 경우 안전하지만 속도가 느린 단점이 있었다(DB를 긁는다라고 표현)

- 속도를 개선하기 위해 Redis에 저장해줌으로써 해결

- DB에서 받은 토큰(유저의 정보)을 state, cookie, localStorage, sessionStorage에 저장해둔다.

 

2. 토큰을 암호화(DB가 필요하지 않다)

1) JWT(Jason Web Token)

- 브라우저에서 BE에 로그인 요청을 하고 BE에선 DB에서 요청한 데이터를 확인한 후 로그인 만료시간을

포함한 객체를 만들어 낸다

- 객체를 암호화

- 암호화한 토큰을 사용자에게(브라우저) 넘겨주고 저장

- 이때 넘겨준 토큰을 AccessToken이라 부른다

2) JWT의 암호화 방식

- Encoded : 암호화

- Decoded : 복호화

- header : 암호화에 사용된 알고리즘과 토큰(Token)형식을 보여줌

- payload : 토큰의 내용을 보여줌

 

💥JWT 문제점 및 해결

- 내용을 알 수 있기 때문에 중요한 데이터를 저장해두어선 안된다

- 토큰이 탈취당할 가능성이 있기때문에 만료시간을 30분~2시간정도 설정해준다(만료시간이 초과되면 토큰은 사라짐)

- 서명(Verify Signature)을 통해 키를 얻고 조작이 불가능하게끔 만들어 놓음

 

❗비밀번호 찾기 했을 때❗

- 비밀번호를 보여주는 서비스는 문제가 있는 서비스이다(ex => 당신의 비밀번호는 '1234'입니다)

- 비밀번호 조회가 되면 안된다

✔ 정상적인 서비스

- 비밀번호를 fetch 할 수가 없음

- 비밀번호를 다시 만들어주고 페이지를 이동시켜줌

 

암호화 2가지 방식

1. 양방향 암호화

- 암호화 및 복호화 둘다 가능

2. 단방향 암호화 

- 암호화만 가능하며 복호화는 불가능

 

다대일관계

1) 단방향 암호화 : Hash(Password Hashing)

- 273719 : 2자리씩 끊어서 10으로 나눈 몫으로 비밀번호를 설정 ⏩ 779로 설정됨

⏩ 779가 나올 경우의 수가 무수히 많음

2) 레인보우 테이블 : 무수히 많은 경우의 수를 테이블로 정리해둔 것 ⏩ 해킹에 쓰임

 

로그인 프로세스

1. 인증(Authentication)

- 로그인해서 토큰을 얻는 과정

2. 인가(Authorization)

- 🔼 playground에서 HTTP HEADERS 부분에 토큰을 첨부해 줄 수 있다

- 관례상 Bearer를 accessToken(JWT)앞에  붙여주며 저장한다.

- 토큰을 가지고 권한을 얻는 과정

- 토큰을 가지고서 createPayment, updateProfile, createProduct api를 요청할 때 유저의 정보를 가진 JWT token을 보내주고 BE에선 이 토큰을 가지고 복호화를 통해 해당 유저임을 인식하게 됨

💥 accessToken이면 JWT이다? NOPE❗ JWT를 accessToken으로 사용할 뿐이다

3.vscode에서 accessToken을 어떻게 적용할까❓

- accessToken을 모든 페이지에 적용시켜주기 위해 _app.tsx파일에서 global state를 적용시켜준다.

 

- _app.tsx 파일에서 uploadLing함수에 headers: {Authorization: `Bearer ${accessToken}`}을 추가하고

- useEffect 함수를 이용해 localStorage에 토큰을 저장하면 된다.

 

Login Code

1. 로그인 입력창

// 로그인 입력창
import { gql, useMutation } from "@apollo/client"
import { Modal } from "antd"
import { useRouter } from "next/router"
import { ChangeEvent, useContext, useState } from "react"
import { IMutation, IMutationLoginUserArgs } from "../../src/commons/types/generated/types"
import { GlobalContext } from "../_app"

const LOGIN_USER = gql`
    mutation loginUser($email: String!, $password: String!) {
        loginUser(email: $email, password: $password){
            accessToken
        }
    }
`

export default function LoginPage(){
    const {setAccessToken} = useContext(GlobalContext)
    const router = useRouter()

    const [email, setEmail] = useState("") // 타입추론이 자동으로 된다.
    const [password, setPassword] = useState("")
    
    const [loginUser] = useMutation< // Mutation 같은 애들은 타입추론이 안돼서 타입을 입력해줘야 한다.
    Pick<IMutation, "loginUser">, // Omit => 특정 데이터 빼고 나머지 다 가져와줘! Partial => 부분적으로 필요하고 필요 없을 수도 있다! ':' 앞에 ?를 붙여서 가져와줘!(유틸리티 타입)
    IMutationLoginUserArgs
    >(LOGIN_USER)


    const onChangeEmail = (event: ChangeEvent<HTMLInputElement>) => {
        setEmail(event.target.value)
    }

    const onChangePassword = (event: ChangeEvent<HTMLInputElement>) => {
        setPassword(event.target.value)
    }

    // LoginUser Api 요청하는 함수
    const onClickLogin = async () => {
        try {
            const result = await loginUser({
                variables: { 
                    email,
                    password
                }
            })
            const accessToken = result.data?.loginUser.accessToken || ""
            // result 안에는 data가 있을거고 
            // data안에 loginUser가 있고 그 안에 accessToken이 있다.
            console.log(result.data?.loginUser.accessToken) 
            // setAccessToken이 있으면 보여줘! token이 없으면 ""에 넣어주세요!
            if(setAccessToken) { 
                setAccessToken(accessToken) // 새로고침하게 되면 로그인 데이터가 사라짐! 유지방법은 추후에 배울 예정!
                // localStorage.setItem("aaa","철수")
                // localStorage.getItem("aaa") // key만 작성 => 로컬 스토리지에서 뽑아오는 것!
                localStorage.setItem("accessToken",accessToken || "")

                console.log("==========================")
                console.log(localStorage.getItem("accessToken"))
                console.log("==========================")
            }    
            // 로그인 성공 페이지로 이동하기!!
            router.push("/23-05-login-check-success")
        } catch (error) {
            // 타입스크립트 버젼에 따라 error부분에 밑줄이 그어져서 아래처럼 작성
            if(error instanceof Error) Modal.error({content: error.message})
        }
        
    }

    return(
        <div>
            {/* 이메일과 비밀번호를 state에 담아주고 이메일과 비밀번호를 변경했을때 onChange 함수를 만들어줘야함 */}
            이메일: <input onChange={onChangeEmail} type="text" /> 
            비밀번호: <input onChange={onChangePassword} type="password" />
            <button onClick={onClickLogin}>로그인하기!!!!</button>
        </div>
    )
}

2. withAuth 권한분기 코드

import { Modal } from "antd"
import { useRouter } from "next/router"
import { useEffect } from "react"

// @ts-ignore => 타입스크립트 무시해주는 명령어. 주석처리해줘야 적용됨
export const withAuth = (Component) => (props) => { // 타입스크립트는 generic 배우고 적용해줄 예정!
    const router = useRouter()

    useEffect(() => {
        if(!localStorage.getItem("accessToken")){
            Modal.warning({content: "회원전용 페이지입니다"})
            router.push('/23-04-login-check')
        }
    },[])

    return <Component {...props} />
}

3. 로그인 성공 후 메인페이지 코드

import { gql, useQuery } from "@apollo/client"
import { IQuery } from "../../src/commons/types/generated/types"
import { withAuth } from "../../src/components/commons/hocs/withAuth"

const FETCH_USER_LOGGED_IN = gql`
    query fetchUserLoggedIn {
        fetchUserLoggedIn{
            email
            name
        }
    }
`
const LoginSuccessPage = () => {
    // const router = useRouter()
    const {data} = useQuery<
    Pick<IQuery, 'fetchUserLoggedIn'>
    >(FETCH_USER_LOGGED_IN)
    
    // useEffect(() => {
    //     if(!localStorage.getItem("accessToken")){
    //         alert("로그인을 먼저 해주세요!")
    //         router.push('/23-04-login-check')
    //     }
    // },[])
    
    return(
        <div>
            {data?.fetchUserLoggedIn.name}님 환영합니다!!!
        </div>
    )
}

export default withAuth(LoginSuccessPage) // withAuth(LoginSuccessPage) 한 묶음이라 봐야 덜 헷갈린다

'TIL' 카테고리의 다른 글

배열(array)  (0) 2022.03.26
아규먼트(Argument)와 파라미터(Parameter)의 차이점  (0) 2022.03.26
[TIL] 로그인 (withAuth) // 220215  (0) 2022.02.17

관련글 더보기