이전 편을 참고하면 더욱 좋다!
이전 포스팅에서 로그인 처리를 하고 AccessToken과 RefreshToken을 발급해 유저의 정보를 유지하는 서비스를 구현했었다. 그렇다면 프론트와 백은 해당 토큰들을 어떻게, 어디에 저장해야할까?
이에대해 알아보기전, 우리는 아래의 단어들은 먼저 익혀보자.
공격자가 의도하는 악의적인 js 코드를 피해자 웹 브라우저에서 실행시키는 것
해당 방법을 통해 클라이언트의 브라우저에 저장된 중요 정보들을 탈취 할 수 있다. 만약 클라이언트의 localstorage에 토큰이 저장되어있다면 해커는 아주 간단하게 토큰을 탈취할 수 있다. 토큰 외의 중요한 정보들 까지 해킹당할 위험 또한 높다.
사이트 간 요청 위조, 웹 애플리케이션에 특정 요청을 보내도록 유도하는 공격 행위
위 두가지 조건이 충족되었을때 해커는 클라이언트의 정보들을 탈취 할 수 있다. 우리는 흔히 자동 로그인을 해놓곤 하는데 위의 첫번째 조건이 충족되기 제일 쉽다. 그리고 만약 해커가 만든 피싱 사이트에 접속했다면..? 뒷 일은 감당하기 힘들다.
이전 포스팅에선 로그인시 atk와 rtk를 발급하고 rtk는 Redis에 저장되어 관리되었다. 다만 프론트에게 해당 토큰들을 제공할때 atk와 rtk를 둘다 body에 담아 보냈고 atk를 재발급 요청시 헤더에 REFRESH_TOKEN을 통해 요청 받았었다. 이 방법은 서버에 rtk가 저장되어 있다고 한들, 프론트에서 상당히 취약해진다.
예를들어, 해당 body로 넘겨준 rtk를 프론트가 로컬에 저장했다면? XSS 공격에 상당히 취약할 것 이다. 그렇다고 만약 rtk를 서버에 저장하지 않고 바로 Cookie로 넘겨주었다면? CSRF 공격에 매우 취약해졌을 것 이다. 해커가 requset url을 알았다면 쉽게 rtk를 탈취할 것이다.
우리는 이전 포스팅에서 redis에 rtk를 저장을 해놨기에 서버가 rtk가 무엇인지 알고있으며, 추가적으로 프론트가 Js에서 쿠키에 접근 자체가 불가능하게 만든다면 XSS 공격와 CSRF 공격 두가지에 대해서 모두 보안을 챙길 수 있다.
즉, 발급된 rtk를 HttpOnly를 통해 프론트가 JS에서 쿠키에 접근 자체를 불가능하게 만들어 놓고 해당 rtk 쿠키에서 정보를 요청하면 안전하게 atk를 재발급 할 수 있다.
쿠키에 rtk를 저장하고 HttpOnly는 서버에서만 설정이 가능하기에 아래 코드를 차근차근 따라가자.
@Transactional
public ResponseEntity<UserDto.loginResponse> login(UserDto.login request, HttpServletResponse response) {
LOGIN_VALIDATE(request);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPw());
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
String atk = tokenProvider.createToken(authentication);
String rtk = tokenProvider.createRefreshToken(request.getEmail());
redisDao.setValues(request.getEmail(), rtk, Duration.ofDays(14));
Cookie cookie = new Cookie("refreshToken", rtk);
cookie.setHttpOnly(true);
response.addCookie(cookie);
return new ResponseEntity<>(UserDto.loginResponse.response(
atk,
"httponly"
), HttpStatus.OK);
}
저번 코드와 달라진 점은 rtk를 body에 담지않고 Cookie를 하나 생성하여 그 안에 rtk를 담아준다. 추가적으로 cookie에 httponly를 true로 설정해주면된다.
그리고 다시 포스트맨을 통해 로그인 요청을 하면 Cookie에 refreshToken이 정상적으로 저장된 것을 확인할 수 있고 HttpOnly가 제대로 설정된것 또한 확인할 수 있다.
@GetMapping("auth")
public ResponseEntity<UserDto.loginResponse> reissue(
@CookieValue(value = "refreshToken", required = false) Cookie cookie, HttpServletResponse response
) {
return userService.reissue(cookie.getValue());
}
이전에 REFRESH_TOKEN 헤더에서 rtk를 요청하던것과 달리, 이번엔 쿠키에서 해당 값을 가져오기만 하면 된다. Service 코드는 건드릴 필요없다.
서비스를 개발함에 있어서 사실 보안이 가장 중요하다는 것을 알고 있지만 개발하면서 제일 챙기기 어려운 부분 같다. 그렇지만 이런 부분까지 잘 보완하여 API를 개발하는 것이 좋은 개발자의 숙명이지 않을까 싶다. 그럼 이번 포스팅도 여기서 마무리 지어보도록 하겠다.
최근에 CORS 문제와 이미지 업로드 관련해서 다시 좀 공부했었는데 아마 다음 포스팅은 ,, 아무튼 그럼 이만! 👋