⭐ 로그인의 역사 ➡ 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) 한 묶음이라 봐야 덜 헷갈린다
배열(array) (0) | 2022.03.26 |
---|---|
아규먼트(Argument)와 파라미터(Parameter)의 차이점 (0) | 2022.03.26 |
[TIL] 로그인 (withAuth) // 220215 (0) | 2022.02.17 |