개발놀이터

온라인 쇼핑몰 ver.2 (3) : JWT 토큰으로 인증 레이어 추가하기 본문

사이드 프로젝트/온라인 쇼핑몰 ver.2

온라인 쇼핑몰 ver.2 (3) : JWT 토큰으로 인증 레이어 추가하기

마늘냄새폴폴 2023. 5. 19. 04:57

기존 인증 방식

  • 스프링 시큐리티에 100퍼센트 의존하는 방식이었습니다. 
  • 스프링 시큐리티는 웹 세션으로 동작합니다. 

 

기존 인증 방식의 문제점

  • 추후에 MSA로 변경하는 과정에서 필요한 stateless한 인증 방식이 필요했습니다. 

 

ver.2에서 개선한 인증 방식

  • 스프링 시큐리티의 Authentication 객체를 이용해 JWT를 만들어 회원 인증에 사용했습니다. 
  • 인증 레이어를 기존 스프링 시큐리티 하나에서 JWT를 추가해 두개의 인증 레이어를 사용할 수 있었습니다. 
  • 스프링 시큐리티에 100퍼센트 의존했으면 구현할 수 없었던 OAuth 의 Remember-me 기능을 JWT를 이용해 구현할 수 있었습니다. 

 

 

JWT로 인증 추가

0. 고민

사실 이 프로젝트를 진행할까 말까 수많은 고민이 오갔습니다. 

 

이미 인증으로 스프링 시큐리티를 사용하고 있는데 새로운 인증이 필요할까가 우선 걱정되었습니다. 

 

어떤 포스팅의 댓글에서 JWT인증 정말 좋지만 구현하기 까다롭고 유지보수에도 스프링 시큐리티를 단독으로 사용하는 것보다 떨어질 수 있다. 라는 글을 보고 더 의욕이 꺾였습니다. 

 

또한 JWT를 인증에 사용한다는 것은 세션을 기반으로한 인증을 사용하지 않겠다는 것을 의미하는데 스프링 시큐리티가 HTTP Session을 기반으로 작동한다는 사실을 알고 더 고민했습니다. 

 

1. Chat GPT

이 고민을 챗지피티한테 물어봤습니다. 역시라면 역시일까 GPT는 대단했습니다. 이런 제 고민을 한방에 뒤집어줬습니다. 

 

GPT가 스프링 시큐리티와 JWT를 섞었을 때의 장점을 몇가지 말해주는데 하나하나가 묵직했습니다. 

 

  1. stateless 한 인증 : JWT는 인증 정보를 그 안에 가지고 있는 내장된 토큰이기 때문에 서버가 유저의 인증을 위해 server-side에서 기록들을 유지하거나 세션을 유지할 필요가 없습니다. 동시에 시스템을 무상태로 유지할 수 있죠. 이 상황은 특히 애플리케이션을 확장할 때 굉장히 유용한데 그 이유는 공유하고 있는 세션 스토리지를 제거할 수 있기 때문입니다. 
  2. 마이크로서비스 준비 : 마이크로서비스 아키텍처에서 유저는 다양한 서비스에서 인증할 필요가 있습니다. JWT를 사용하면 유저가 한번 인증하고 토큰을 생성하면 이 토큰은 다음 요청을 인증하기위해 사용될 수 있습니다. 이것은 인증을 구현하기위해 각각의 마이크로서비스들을 만들 때 필요성이 완화될 것입니다. 
  3. 성능 : 서버가 세션을 유지할 필요가 없어진다면, 각각의 요청에서 세션 정보를 찾을 필요가 없습니다. 이는 단순히 토큰이 유효한지 검증하기 위해 토큰의 신호를 받기만 하면 된다는 의미입니다. 
  4. 모바일 친화적 : JWt는 다른 도메인에서 같은 방식으로 동작합니다. 그래서 다른 디바이스나 다른 도메인으로부터의 요청을 핸들링하기 더 쉽습니다. 이는 특히 모바일 애플리케이션을 구현할 때 더 두드러집니다. 
  5. 세분화된 Access Control : JWT는 유저의 특정한 정보나 허가를 가지고 있을 수 있습니다. 이는 세분화된 접근 컨트롤을 할 수 있게 해줍니다. 이러한 방식은 유저가 접근할 수 있는 실행이나 자원을 인가하는데 유용하게 사용될 수 있습니다. 
  6. CORS 지원 : JWT는 RESTful API에 잘 맞게 만들면서 CORS 정책을 잘 준수합니다. 그리고 그곳에서 AJAX요청이 다른 도메인으로부터 만들어질 때 유용합니다. 
  7. 스프링 시큐리티 통합 : 스프링 시큐리티는 자바 애플리케이션을 위한 포괄적인 보안 방법들을 제공합니다. 이는 유연하고 커스텀하기 쉬운데 우리가 스프링 시큐리티에서 JWT를 사용하게 된다면 우리는 session fixation이나 clickjacking이나 XSS공격과 같은 것을 처리할 때 이점이 있습니다. 

 

이 중 마이크로 서비스 준비가 제 맘에 와닿았습니다. 저는 이 프로젝트를 단순한 성능 개선을 넘어 CI/CD와 이후 마이크로서비스까지 도전해볼 생각이기 때문입니다. 

 

2. 험난한 과정

저는 JWT를 말로만 들었지 직접 구현할 것이라고는 생각도 못했습니다. 그래서 그랬던 것일까요 정말 힘들었습니다. 

 

제가 참고한 코드의 성질이 저와 완전히 달라 그대로 작성하면 땡치는 그런 구조가 아니었습니다. 

 

그나마 최근에 Redis를 이용해 캐싱을 구현해본 것이 큰 도움이 되었습니다. 

 

거의 모든 것을 제가 손수 커스터마이징 해야 했습니다. 뼈대는 다른 사람이 만든 코드를 기반으로 했지만 제 프로젝트에 알맞은 기술로 정착시키기 위해서는 뼈를 깎는 노력이 필요했습니다. 

 

2-1. 저는 프론트는 모릅니다만...

제가 참고한 코드는 프론트와의 통신을 기본으로 깔고 들어갔습니다. 때문에 제 프로젝트와 결이 많이 달랐습니다. 저는 프론트의 ㅍ자도 모르는 사람이었기 때문에 특히 리턴타입을 제 프로젝트에 맞게 고치는 일이 허다했습니다. 

 

2-2. header로 토큰 검증 그거 어떻게 하는건데

참고한 코드는 Authorization 헤더에 토큰을 유지하는 방식을 사용하고 있었습니다. 하지만 프론트에선 가능할지 몰라도 server-side에선 Authorization 헤더를 추가할 수 없다는 충격적인 사실을 알아버렸습니다. 

 

때문에 저는 기존 헤더에 토큰을 유지하는 방식에서 쿠키에 유지하는 방식을 선택했습니다. 

 

2-3. 쿠키가 안보입니다?

제 개인적인 생각이지만 쿠키와 세션은 기본적으로 갈취된다고 가정하고 코딩을 하는 것이 속편했습니다. JWT토큰같은 경우는 갈취당했을 때 답이 없기 때문에 단순히 쿠키에 토큰을 넣으면 정말 위험합니다. 

 

그래서 제가 참고한 코드에선 헤더에 담아서 보관한 것이겠지요. 

 

그래서 저는 HttpOnly 내부에 쿠키를 두기로 결심했습니다. 하지만 쿠키가 안보이더군요. 

 

몇시간을 삽질 끝에 ResponseCookie라는 객체를 스프링에서 지원해주고 거기서 path(), sameSite(), httpOnly(), secure()과 같은 설정을 할 수 있었습니다. 

 

2-4. 저기요 OAuth는요..?

제가 참고한 코드는 로그인 폼 기반 인증을 기반으로 두고 있습니다. 때문에 OAuth도 같이 사용하는 제 프로젝트에선 OAuth에 대한 추가적인 인증이 필요했습니다. 

 

정말 막막했습니다. 기존 문제들은 뼈대에서 조금씩 변형하면 해결됐지만 이젠 진짜 없는걸 만들어야 했습니다. 

 

기존에 남들이 만든 코드를 복붙하고 제가 원하는 방향대로 조금씩 수정하던 방식과는 전혀 달랐습니다. 

 

하지만 이 때 스프링 시큐리티의 동작 원리에 대한 전반적인 이해가 엄청난 도움이 됐습니다.시큐리티 코드를 하나씩 디버깅해보며 어떤 클래스를 상속하거나 구현할지 고민했습니다. 

 

처음엔 Filter를 커스텀하게 만들까 고민했습니다. 때문에 OAuth2LoginAuthenticationFilter와 그 부모인 AbstractAuthenticationProcessingFilter가 후보대상이었습니다. 

 

하지만 필터는 인증이 되기 전에 작동하는건데 과연 인증된 값으로 쿠키를 만들 수 있을지 의문이 들었습니다. 

 

우선 인증과정이 모두 끝나야 쿠키를 만들던 Redis에 RefreshToken을 만들건 할 것 같았습니다. 

 

때문에 모든 인증이 끝나고 작동하는 SuccessHandler를 이용하기로 결정했습니다. 다행히 제가 찾고있던 인터페이스가 있었습니다. 바로 AuthenticationSuccessHandler였습니다. 

 

이 핸들러를 구현하고나니 OAuth에대한 인증을 끝마칠 수 있었습니다. 

 

 

3. 결론 (느낀점)

이번 경험은 저에게 정말 뜻깊은 경험이었습니다. 제가 드디어 제가 생각하던 개발자와 조금 가까워진 기분이 들었기 때문입니다. 

 

제가 개발 공부를 막 시작했을 때는 나만의 코드에 대한 집착이 강했습니다. 때문에 구글링해서 코드를 복붙하는 것을 부끄러워했습니다. 

 

주변 지인들은 복붙해서 나만의 것으로 커스터마이징 하는 것도 실력이 없으면 할 수 없는 것이라고 위로해줬지만 저는 코드를 직접 만들고 싶었습니다. 

 

마치 그림을 그리는 사람들이 모작을 하는 것이 아니라 나만의 그림, 나만의 그림체, 나만의 캐릭터를 갖는 것을 원하듯이 말이죠. 

 

이번 프로젝트에선 나만의 코드를 만들 수 있었습니다. 상속, 인터페이스 구현을 적극 사용해서 드디어 제가 생각하던 개발자에 조금 더 다가간 기분이었습니다. 

 

JWT를 이용한 인증 레이어 추가 프로젝트가 모두 끝났을 때 정말 날아갈 듯이 기뻤습니다. 이번 프로젝트에선 느리던 것이 빨라진 것도 아니고 DB 부하를 줄여준 것도 아니었습니다. 

 

사실 기존에도 잘 돌아가던 인증 체계를 커스텀하게 만든 것 뿐이었습니다. 

 

하지만 나만의 코드를 만들었다는 그 기분은 제 개발공부 인생 최고의 자랑거리이지 않을까 싶습니다. 

 

 

4. 이후 개선해야할 점

이후에 개선되어야할 점은 동시성문제입니다. 

 

기존 프로젝트에 결제 로직쪽에서 동시성 문제가 확인되었습니다. 저는 이 부분을 synchronized 키워드를 가지고 해결했습니다. 

 

이 프로젝트를 마치고 한참 뒤에 프로세스 동기화에 대해서 공부할 때쯤 동기화에는 뮤텍스와 세마포어가 있다는 것을 공부하게 되었습니다.

 

제가 구현한 것은 뮤텍스였죠. 

 

하지만 이용자가 많아지면 뮤텍스 방식은 한계가 있을 것이라고 판단했습니다. 때문에 프로세스 동기화를 공부하자마자 저는 기존 뮤텍스방식의 제 프로젝트를 세마포어 방식으로 바꿔봐야겠다는 생각이 들었습니다. 

 

다음 포스팅은 아마도 뮤텍스에서 세마포어로 바꾸는 작업을 진행할 것 같습니다. 긴 글 읽어주셔서 감사합니다.