Next
[https://choigirang.github.io/react/01-React-Token/]
- 로그인 블로그 글이 있지만 기억을 못 하고 이해를 못 하니 다시 적는 로그인 구현하기
로그인이 이루어지는 방식
세션 id
- 서버에서 특정 유저의 정보를 담은 세션을 생성한다.
- 유저가 로그인할 때 세션을 생성하고
- 세션의
id
를 클라이언트에게 보내 - 클라이언트 측에서
id
를 저장해두었다가 - 인증이 필요한 데이터를 가져올 때 서버에
id
를 보내면 - 서버에서
id
를 통해 세션을 불러와 유효한 방식인지 검증한다.
JWT
- 유저가 로그인할 때 서버가 인증 정보를 보내고 암호화나 시그니처 추가가 가능한 데이터 패키지
(jwt)
안에 인증 정보를 담아 보낸다. - 담기는 정보 중
accessToken
과refreshToken
이 이후 유저 인증에 사용된다.accessToken
: 실직적인 인증 정보이며 일정 시간이 지나면 만료되는 구조를 가진다.refreshToken
: 일정 시간이 지나면 다시 서버에 인증 정보를 요청하여 그 때마다 새로운accessToken
을 발급해 돌려준다.
- 이 정보를 클라이언트에 저장한다.
- 받은
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
의 옵션값 중withCredentials
를true
로 설정해줘야refreshToken
을 주고받을 수 있다.
1
2
3
4
5
6
const api = axios.create({
baseUrl: "",
timeout: 3000, // axios 요청 시간 제한
headers: { "Content-Type": "application/json" }, // header 설정
withCredentials: true, // 인증, cors 해제해도 여기에 걸릴 시 무조건 에러
});
- 서버에서 인증 정보를 받아와 헤더에 토큰을 담아 보내도록 설정한다.
accessToken
을localStorage, 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
을 받아와 로그인이 연장되도록 처리해야 한다.- 다시 로그인 페이지로 이동시킨다.
- 유저가 모르게 서버에서 새로운 토큰을 받아와 연장한다.
- 로그인을 했을 때
post/login
으로 아이디와 비밀번호를 보내고refreshToken
과accessToken
을 받는다. - 토큰이 만료됐을 때
post/refresh
등으로refreshToken
을 보내고 새로운refreshToken
과accessToken
을 받는다. 두 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();
}
}