프론트엔드에서 로그인 구현하기

이성훈·2021년 11월 3일
5

3줄 요약

  • JWT를 사용
  • refreshToken은 secure, httpOnly 쿠키로 accessToken은 JSON payload로 전달받는다
  • CSRF 취약점 공격 방어는 되나 XSS 공격으로 API 요청을 보낼때는 위험하니 서버와 클라이언트 모두 추가적으로 방어 수단을 마련해야함

로그인 방식

세션 id를 이용하는 방식

서버는 특정 유저의 정보를 담은 세션을 생성한다
1. 유저가 로그인할 때
2. 서버는 세션을 생성한 후
3. 그 세션의 id를 클라이언트에 보내주고
4. 클라이언트는 이 id를 클라이언트에 저장해두었다가
5. 인증이 필요한 데이터를 가져올때 서버에 id값을 보내면
6. 서버는 그 id를 통해 세션을 불러와 유효한지 확인한다

JWT를 이용하는 방식

  1. 유저가 로그인할 때
  2. 서버가 인증 정보를 암호화된 데이터 패키지(JWT)안에 담아 보내준다
  3. 그 정보중 accessToken과 refreshToken이 이후 유저 인증에 사용되므로 클라이언트에 저장해둔다
  4. accessToken을 유저에게만 보여줄 수 있는 정보에 접근할때 서버에 보내면
  5. 서버는 그 토큰이 유효한지 확인한다

실질적인 인증 정보는 accessToken이다 이는 일정 시간이 지나면 만료되는 특징을 가지는데

  • refreshToken을 이용해 로그인을 지속적으로 유지할 수 있다
  • refreshToken을 서버에 보내주면 새로운 accessToken을 받아올 수 있다
  • refreshToken 사용 여부는 옵션이다
  • 보통 localStorage나 쿠키에 저장하게 되는데 리프레쉬 되도 정보가 남도록 저장하는 방식이지만 밑에서 얘기할 XSS, CSRF에 취약하다

보안

로그인하면 빠질 수 없는 것이 보안문제이다
보안 공격의 종류와 특징에 대해 살펴보자

XSS 공격

해커가 클라이언트 브라우저에 JavaScript를 삽입해 실행하는 공격으로써 url에 javascript를 적어 사이트에 스크립트를 삽입하거나 input 태그를 통해 javascript를 서버로 전송해 스크립트를 실행하는 식으로 마치 그 사이트인것마냥 API 요청을 한다

CSRF 공격

해커가 다른 사이트에서 우리 사이트의 API를 요청하는 공격
API를 요청할 수 있는 클라이언트 도메인을 서버에서 통제하고 있지 않다면 가능한 일인데
해커가 클라이언트에 저장된 유저 인증정보를 서버에 보낼 수 있다면 제대로 로그인한 것처럼 유저의 정보를 변경하거나 유저만 가능한 일들을 수행할 수 있다


브라우저 저장 방식

localStorage

브라우저 저장소에 저장하며 Javascript내 글로벌 변수로 읽기 / 쓰기 접근이 가능하다

브라우저에 쿠키로 저장하며 클라이언트가 HTTP 요청을 보낼 때마다 자동으로 쿠키가 서버에 전송된다
Javascript내 글로벌 변수로 읽기 / 쓰기 접근이 가능하다

secure, httpOnly 쿠키

브라우저에 쿠키로 저장되는 건 같지만 Javascript내에서 접근이 불가능하다
secure을 적용하면 https 접속에서만 동작한다

사실 어떠한 저장 방식을 택해도 XSS 취약점이 있다면 보안 이슈가 존재하므로 추가적으로 XSS 방어 처리가 필수다
React는 해커가 string에 html/Javascript를 담아 JSX에 삽입할 경우 자동으로 escape 처리한다고 한다


실제 적용

  • secure 쿠키 전달을 위해서 프론트와 백엔드는 같은 도메인을 공유해야한다 (ex: client: https://shop.kaen.com, server: https://api.kaen.com)
  • 백엔드는 HTTP 응답 set-cookie 헤더에 refreshToken 값을 설정하고 accessToken을 JSON payload에 담아 보내줘야 한다

클라이언트

최상단 index.js에 다음과 같이 설정해주면 refreshToken cookie를 주고 받을 수 있다

axios.defaults.baseURL = 'https://www.kaen.com';
axios.defaults.withCredntials = true;

아래는 로그인 로직입니다

onLogin = (email, password) => {
	const data = {
		email,
		password,
	};
	axios.post('/login', data).then(response => {
		const { accessToken } = response.data;

		// API 요청하는 콜마다 헤더에 accessToken 담아 보내도록 설정
		axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;

		// accessToken을 localStorage, cookie 등에 저장하지 않는다!

	}).catch(error => {
		// ... 에러 처리
	});
}

// 개인적으로 사용했던 로그인 코드
import React, { useState } from "react";
import { Link } from "react-router-dom";
import axios from "axios";
import config from "../../config";

const ClickSignIn = ({ loginHandler }) => {
	const [loginInfo, setLoginInfo] = useState({
		email: "",
		password: "",
	});
	const [errorMessage, setErrorMessage] = useState("");
	const onSignIn = () => {
		axios
			.post(`${config.serverUrl}/users/signin`, loginInfo, {
				withCredentials: true,
			})
			.then((res) => loginHandler(res.data));
		if (!loginInfo.email || !loginInfo.password) {
			setErrorMessage("이메일과 비밀번호를 입력하세요");
			return;
		}
	};
	return (
		<>
			<div>
				<button className="btn" onClick={onSignIn}>
					LogIn
				</button>
				<button className="btn signup-btn">
					<Link to="/signup" className="signup-link">
						SignUp
					</Link>
				</button>
				<div className="alert-box">{errorMessage}</div>
			</div>
		</>
	);
};
export default ClickSignIn;

로그인 유지 부분

아직 이해하지 못한 부분이므로 추후에 수정해서 포스팅할 예정


참조

yaytomato

profile
블로그 이전중입니다 => https://kusdsuna.tistory.com/

0개의 댓글