기존 프로젝트에서도 JWT랑 시큐리티, Oauth를 사용했지만 이번에는 jwt 저장 방법을 변경해보려고 한다.
기존 방법
액세스 토큰 -> json으로 넘겨서 클라이언트가 로컬, 세션 스토리지에 저장해서 관리.
리프레시 토큰 -> 쿠키에 저장해서 secure, httpOnly, sameSite 옵션을 걸음.
리프레시 토큰은 쿠키에 저장해서 보안성을 챙겼지만 액세스 토큰을 스토리지에 저장한다는 게 마음에 걸렸다.
그래서 액세스 토큰도 쿠키에 같이 저장하기로 하였다.
변경 방법
아래 설명은 스프링부트 Oauth 라이브러를 사용해서 successHandler까지 도달했다는 가정 하에 진행한다.(즉 인증코드를 액세스 토큰으로 바꿔서 접근 권한을 획득한 상태)
@Override
@Transactional
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("oauth 성공 핸들러 동작");
OAuth2User principal = (OAuth2User) authentication.getPrincipal();
// user의 이메일 추출
Map<String, Object> attributes = principal.getAttributes();
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
String email = String.valueOf(kakaoAccount.get("email"));
// 유저 없으면 예외처리
Optional<User> byEmail = Optional.ofNullable(userRepository.findByEmail(email)
.orElseThrow(EntityNotFoundException::new));
User user = byEmail.get();
// 액세스 토큰 생성
String accessToken = jwtUtil.createAccessToken(user.getId());
// 리프레시 토큰 생성
String refreshToken = jwtUtil.createRefreshToken(user.getId());
// 액세스 토큰을 위한 쿠키 생성
Cookie accessTokenCookie = new Cookie("accessToken", accessToken);
accessTokenCookie.setHttpOnly(true);
accessTokenCookie.setSecure(true); // HTTPS를 사용하는 경우에만 true로 설정
accessTokenCookie.setPath("/");
// 리프레시 토큰을 위한 쿠키 생성
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setSecure(true); // HTTPS를 사용하는 경우에만 true로 설정
refreshTokenCookie.setPath("/");
// 쿠키에 토큰 저장
response.addCookie(refreshTokenCookie);
response.addCookie(accessTokenCookie);
// url 생성
String url = makeRedirectUrl();
redirectStrategy.sendRedirect(request, response, url);
}
// 리다이렉트 주소
private String makeRedirectUrl() {
return UriComponentsBuilder.fromUriString("http://localhost:3000/success")
.encode(StandardCharsets.UTF_8)
.build().toUriString();
}
위 코드에서 액세스 토큰을 생성하고 cookie에 같이 담았다.(리프레시 또한 마찬가지)
그리고 로그인 성공 처리를 담당하는 프론트 페이지로 리다이렉션.
기존에는 해당 처리가 끝나면 모든 API 요청마다 헤더에 (Bearer 액세스 토큰) 이걸 담아서 보내줘야 했다.
그래야 JwtFilter에서 파싱해서 토큰을 검증하게 되었다.
근데 이제는 쿠키에 담아서 자동으로 모든 API 요청마다 보내게 된다.
즉 클라이언트는 액세스 토큰의 값을 모른다는 장점이자 단점(?)이 생긴다.
그래서 기존에 헤더에서 추출했던 액세스 토큰을 쿠키에서 추출하도록 로직을 변경해야한다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 헤더에서 액세스 토큰 추출
Cookie[] cookies = request.getCookies();
String accessToken = null;
for (Cookie c : cookies) {
if (c.getName().equals("accessToken")) {
accessToken = c.getValue();
break;
}
}
log.info("accessToken : {}", accessToken);
// 토큰이 null인 경우(없는 경우)
if (accessToken == null) {
log.error("액세스 토큰이 null입니다.");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "액세스 토큰 null 발생");
return;
}
간단하게 로직의 흐름을 살펴보자.
request에서 먼저 쿠키들을 꺼낸다.
그러나 만약에 쿠키 자체가 없으면 접근 자체가 불가능하기에 예외 처리를 해준다.
반면에 쿠키에 accessToken이라는 키 값을 가진 쿠키가 있다면 쿠키가 존재하기에 꺼내서 기존 JwtFilter 로직을 처리해주면 된다.
이제는 클라이언트에서 accessToken을 스토리지에 저장하지 않아도 돼서 보안성을 챙길 수 있게 되었다.
단 쿠키를 사용할 때 secure, httpOnly 옵션을 꼭 사용해야 보안성을 챙긴다는 것을 잊지 말자.
'기타' 카테고리의 다른 글
어떻게 HTTP는 로그인된 상태를 판단할까? (0) | 2024.02.07 |
---|---|
분명 JPA에서는 쿼리를 모았다가 날리는 걸로 알았는데? (0) | 2024.02.06 |
FETCH JOIN 사용하면서 깨달은 1차 캐시 (0) | 2024.01.18 |
인텔리제이에서 Lombok을 사용해보자.(IntelliJ) (0) | 2023.06.14 |