Next 9장 - login 페이지
포스트
취소

Next 9장 - login 페이지

Next

[https://choigirang.github.io/react/01-React-Token/]

  • 로그인 블로그 글이 있지만 기억을 못 하고 이해를 못 하니 다시 적는 로그인 구현하기

로그인이 이루어지는 방식

세션 id

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

JWT

  1. 유저가 로그인할 때 서버가 인증 정보를 보내고 암호화나 시그니처 추가가 가능한 데이터 패키지(jwt) 안에 인증 정보를 담아 보낸다.
  2. 담기는 정보 중 accessTokenrefreshToken이 이후 유저 인증에 사용된다.
    • accessToken : 실직적인 인증 정보이며 일정 시간이 지나면 만료되는 구조를 가진다.
    • refreshToken : 일정 시간이 지나면 다시 서버에 인증 정보를 요청하여 그 때마다 새로운 accessToken을 발급해 돌려준다.
  3. 이 정보를 클라이언트에 저장한다.
  4. 받은 accessToken으로 유저에게만 보여줄 수 있는 정보에 접근할 때 서버가 그 토큰이 유효한지 확인한다.

보안

XSS 공격
  • 해커가 클라이언트 브라우저에 Javascript를 삽입해 실행하는 공격이다.
  • 해커가 input을 통해 Javascript를 서버로 전송해 서버에서 스크립트를 실행하거나, url에 Javascript를 적어 클라이언트에서 스크립트 실행이 가능하다면 해커가 사이트에 스크립트를 삽입해 XSS 공격을 할 수 있다.
  • 해커는 Javascript를 통해 사이트의 글로벌 변숫값을 가져오가나 그 값을 이용해 사이트인 척 API 요청을 할 수도 있다.
CSRF 공격
  • 해커가 다른 사이트에서 내 사이트의 API 요청을 보내 실행하는 공격이다.
  • API를 요청할 수 있는 클라이언트 도메인이 누구인지 서버에서 통제하고 있지 않다면 CSRF가 가능한데, 이 때 해커가 클라이언트에 저장된 유저 인증정보를 서버에 보낼 수 있다면 로그인한 것처럼 유저의 정보를 변경하거나 유저만 가능한 액션들을 수행할 수 있다.
  • 예를 들어 CSRF에 취약한 은행 사이트가 있다면 로그인한 척 계좌 비밀번호를 바꾸거나 송금을 할 수 있다.

브라우저 저장소 종류와 보안

localStorage
  • 브라우저 저장소에 저장하는 방식으로 Javascript 내 글로벌 변수로 읽기,쓰기 접근이 가능하다.
  • id, refreshToken, accessToken을 저장해두면 XSS 취약점을 통해 그 안에 담긴 값을 불러오거나 불러온 값을 이용해 API 콜을 위조할 수 있다.
쿠키 저장 방식
  • 브라우저에 쿠키로 저장되는데, 클라이언트가 HTTP 요청을 보낼 때마다 자동으로 쿠키가 서버에 전송된다.
  • Javascript 내 글로벌 변수로 읽기,쓰기 접근이 가능하다.
  • 쿠키 저장 방식 또한 안에 인증정보를 저장해두면 XSS취약점이 있을 때 담긴 값들을 불러오거나, API 요청을 보내면 쿠키에 담긴 값들이 함께 전송되어 로그인한 척 공격할 수 있다.
  • 쿠키에 인증정보를 저장하는 구조에 CSRF가 취약하다면 인증 정보가 쿠키에 담겨 서버로 보내지고, 공격자는 유저 권한으로 정보를 가져오거나 액션을 수행할 수 있다.
  • 쿠키에 refreshToken만 저장하고 새로운 accessToken을 받아와 인증에 이용하는 구조에서는 CSRF취약점 공격을 방어할 수 있다.
    • refreshToken으로 accessToken을 받아도 스크립트에 삽입할 수 없다면 accessToken을 사용해 유저 정보를 가져올 수 없기 때문이다.
secure, httpOnly 방식
  • 브라우저에 쿠키로 저장되는 것 같지만, Javascript 내에서 접근이 불가능하다.
  • secure 을 적용하면 https에서만 동작한다.
  • httpOnly 쿠키 방식으로 저장된 정보는 XSS 취약점 공격으로 담긴 값을 불러올 수 없으며, refreshToken만 저장하고 accessToken을 받아와 인증에 이용하는 구조로 CSRF 공격 방어가 가능하다.
  • 쿠키 저장과 같은 이유로 세션 id, accessToken은 저장하면 안 된다.
  • httpOnly 쿠키에 담긴 값에 접근할 수는 없지만 XSS 취약점을 노려 API 요청하면 httpOnly 쿠키에 담긴 값들도 보내져 유저인 척 정보를 빼오거나 액션을 수행할 수 있다.
결론
  • 어떤 방식을 선택해도 XSS 취약점이 있다면 보안 이슈가 존재한다.
  • 그러므로 추가적인 XSS 방어 처리가 필수이다.

    • input에 입력된 값이 html/javascript로 인식되지 않도록 서버에서 escape처리를 해준다.
    • url을 통해 javascript를 수행할 수 없도록 라우팅을 꼼꼼하게 관리한다.
    • React에서는 공격자가 html/javscript를 담아 JSX에 삽입할 경우 자동으로 escape처리한다.
  • 세션 id를 브라우저에 저장하는 방식은 어떤 방식이던지 보안 위험요소가 있으므로 JWT인증 방식을 사용한다.
  • refreshToken만을 secure,httpOnly쿠키에 저장해 CSRF공격을 방어하고 accessToken은 웹 애플리케이션 내 로컬 변수에 저장해 사용하며, API 요청을 할 때 Authorization헤더에 넣어 보낸다.
  • XSS 취약점을 이용한 API 요청 공격은 클라이언트와 서버에서 추가적으로 방어해야 한다.

적용해보기

  • secure 쿠키 전달 하려면 클라이언트와 서버가 같은 도메인을 공유해야 한다.
    • 클라이언트 : http://example.abc.com 서버 : https://api.abc.com
  • 서버는 HTTP 응답 Set-Cookie 헤더에 refreshToken 값읋 설정하고 accessToken을 JSON payload에 담아보낸다.

client

  • axios의 옵션값 중 withCredentialstrue로 설정해줘야 refreshToken을 주고받을 수 있다.
1
2
3
4
5
6
const api = axios.create({
  baseUrl: "",
  timeout: 3000, // axios 요청 시간 제한
  headers: { "Content-Type": "application/json" }, // header 설정
  withCredentials: true, // 인증, cors 해제해도 여기에 걸릴 시 무조건 에러
});
  • 서버에서 인증 정보를 받아와 헤더에 토큰을 담아 보내도록 설정한다.
    • accessTokenlocalStorage, cookie등에 저장하지 않는다.
1
2
3
4
5
6
7
8
api.post("/login", data).then((res) => {
  const { accessToken } = res.data;

  // 모든 요청 헤더에 accessToken을 담도록 설정
  // Bearer... 부분은 accessToken 토큰의 타입에 따라 조절하며
  // JWT는 "Bearer" 다음에 공백을 넣어주어야 한다.
  api.defaults.headers.common["Authorization"] = `Bearer {accessToken}`;
});

로그인 연장

  • JWT 방식에서 실질적으로 인증되었나를 결정하는 것은 accessToken이다.
  • 브라우저에 저장된 값은 쿠키에 저장된 refreshToken뿐이고 로컬에 저장된 accessToken은 브라우저 창이 꺼지거나 페이지가 리로드되면 사라진다.
  • accessToken은 일정시간이 지나면 만료되는데 새로운 accessToken을 받아와 로그인이 연장되도록 처리해야 한다.
    1. 다시 로그인 페이지로 이동시킨다.
    2. 유저가 모르게 서버에서 새로운 토큰을 받아와 연장한다.
  1. 로그인을 했을 때 post/login 으로 아이디와 비밀번호를 보내고 refreshTokenaccessToken을 받는다.
  2. 토큰이 만료됐을 때 post/refresh 등으로 refreshToken을 보내고 새로운 refreshTokenaccessToken을 받는다. 두 API 모두 HTTP 응답 Set-Cookie헤더에 refreshToken 값을 설정하고 accessToken을 JSON payload에 담아 보내줘야 한다.

이어서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const JWT_TIME = 24 * 3600 * 1000; // 만료시간

onLogin = (email, password) => {
  const data = {
    email,
    password,
  };

  axios
    .post("/login", data)
    .then(onLoginSuccess)
    .catch((err) => "에러처리");
};

onSilentRefresh = () => {
  axios
    .post("/silent-refresh", data)
    .then(onLoginSuccess)
    .catch((err) => "에러처리");
};

onLoginSuccess = (res) => {
  const { accessToken } = res.data;

  axios.defaults.headers["Access-Token"] = `Bearer ${accessToken}`;

  // 만료되기 1분 전 로그인 연장
  setTimeout(onSilentRefresh, (JWT_TIME = 60000));
};
  • 페이지 리로드 시 로그인 자동 연장
1
2
3
4
5
class App extends Component {
  componentDidMount() {
    onSlientRefresh();
  }
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.