일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 30 |
- Django
- 선택정렬
- 자바
- 문자열압축
- codingtest
- 협업도구
- testcode
- 문자열함수
- 코딩테스트
- git
- 알고리즘
- 코테
- 자료구조
- 트러블슈팅
- github
- 기록
- kafkaconsumer
- 기술블로그
- c
- 시스템프로그래밍
- IT
- jwt
- kafka
- SpringSecurity
- 한이음
- 공부기록
- java
- 백준
- 회고록
- AWS
- Today
- Total
신뇽이 되어보자
[Spring Security + JWT] 세션 기반 인증에서 JWT로 전환 회고 본문
학교 수업 시간에 SpringSecurity를 사용해서 프로젝트를 진행했었다.
왜 SpringSecurity를 사용했는가?
Spring 공식 문서에 따르면, Spring Security는 Java 애플리케이션에서 인증과 권한 부여를 모두 제공하는 데 중점을 둔 프레임워크라고 기술되어 있다. 그 핵심은 사용자 정의 요구 사항을 충족하도록 쉽게 확장할 수 있는 점에 있다.
Spring Security의 주요 특징은 다음과 같다:
- 인증 및 권한 부여에 대한 포괄적이고 확장 가능한 지원
- 세션 고정, 클릭재킹, 크로스 사이트 요청 위조(CSRF) 등 다양한 보안 공격으로부터 보호
- 서블릿 API와의 통합 지원
- Spring Web MVC와의 선택적 통합
진행 중이었던 프로젝트에서 1대1 문의 채팅방을 구현하는 상황이었고, 유저를 관리자와 사용자로 구분하여 권한을 관리해야 했다. 이를 위해 Spring Security를 사용하여 권한 부여를 통해 관리하는 것이 편리할 것이라 판단했다.
또한 CSRF와 CORS와 같은 보안 취약점을 자동으로 처리해주는 고급 프레임워크라는 점, 그리고 무료로 제공되는 장점이 있어 경험해볼 가치가 충분하다고 생각했다.
처음 Spring Security를 공부할 때는 과정이 복잡하고 길어 머리가 아팠다.
과정들이 너무 복잡했고, 길었기 때문에 처음 사용했을 때는 잘 모르고 사용했었던 것 같다
계속 관련 문서들을 보고 사용자 인증 방법을 jwt로 전환하는 중에 이제는 좀 알것 같다라는 느낌을 받았다.
왜 Session기반 인증에서 JWT로 전환하는가?
우선 Session은 서버에 사용자의 세션 정보를 데이터베이스에 저장하기 때문에 매번 세션을 읽고 쓰는 작업이 필요하다. 만약 많은 사용자가 동시에 접속한다면 서버 부하가 일어날 수 있다. 만약에 프로젝트를 모바일로도 만들어야한다면? Session은 웹에서는 세션이 유효하지만 모바일 앱에서도 동일한 세션을 공유하려면 세션 스토리지나 캐시 시스템을 추가로 설정해줘야하는 번거로움이 생긴다는 점이다.
반면, JWT는 무상태이므로 클라이언트가 JWT를 매 요청에 포함시켜 보내기만 하면, 서버는 이를 검증하고 필요한 정보만 추출하여 처리한다. 고로 서버가 상태를 관리할 필요가 없어진다. 또한, JWT는 클라이언트 측에서 관리되므로, 서버는 매번 세션을 초기화할 필요가 없고, 서버 측 부담이 크게 줄어들게 되어 성능이 개선될 것으로 판단했다.
어떻게 공부했고, jwt로 전환하면서 가장 크게 느낀 변화는 무엇인지?
jwt를 사용했던 깃허브 레포들을 많이 보면서 남들의 코드를 분석해보고 적용해보는 식으로 공부를 했다.
가장 크게 느낀점은 기존 프로젝트에서는 session으로 인증을 거치고 사용자의 userId를 요청하는 api를 호출하고 모든 api 호출에
사용자 userId를 파라미터로 넣어줘야하는 번거로움이 있었다.
api호출을 줄이기 위해서 클라이언트 단에서 userId를 localStorage에 저장을 하여 필요할 때마다 gettem하여 서버에 보내곤 했는데 보안상 그리 좋을 것 같지 않았다.
근데 jwt를 사용하고 나서는 claims에 userId와 roles를 함께 포함시켜 토큰을 생성한 뒤, 클라이언트 측에서 토큰을 서버로 보내면 jwt를 검증하면서 userId를 얻을 수 있어서 굳이 userId를 별도로 요청하는 api가 필요없어졌고, 인증과정이 더 효율적이고 안전해졌다,
채팅방 조회는 관리자는 모든 채팅방을 조회하고, 사용자는 자신의 채팅방만 조회해야하는 기능이었다.
기존 채팅방 조회 메서드 : userId와 관리자인지 아닌지 boolean값으로 파라미터를 받아왔다.
public ResponseEntity<List<ChatRoom>> getChatRooms(@RequestParam Long userId, @RequestParam boolean isAdmin) {
이제와서 보면 왜 저렇게 했을까 싶을 정도로 ... 이상한 코드였다.. 작동만 되라 마인드였나보다
바꾼 후 채팅방 조회 메서드
@GetMapping
public ResponseEntity<List<ChatRoom>> getChatRooms(@RequestHeader("Authorization") String token) {
// JWT 토큰에서 Bearer 제거
String jwt = token.substring(7);
String userNameFromToken = jwtUtil.extractUsername(jwt); // jwtUtil은 JWT 유틸리티 클래스
// 토큰 유효성 검사
if (!jwtUtil.validateToken(jwt, customUserDetailsService.loadUserByUsername(userNameFromToken))) {
throw new RuntimeException("Invalid or expired token");
}
// userId 및 roles 추출
Long userId = jwtUtil.extractUserId(jwt);
String roles = jwtUtil.extractRoles(jwt);
token만 받아와서 jwt 안의 정보를 추출하는 방식으로 바꿨다.
고민거리는 없었나?
고민거리는 이제 accessToken과 refreshToken의 저장 장소에 대한 고민이 있었다.
accessToken은 기한을 짧게 두었기 때문에 응답으로 보내줬다.
그러나 refreshToken은 기간이 길기 때문에 탈취를 당했을 때 위험하다고 판단이 되었다.
하지만, 데이터베이스에 저장하면 JWT의 장점인 무상태성(stateless)을 깨뜨리는 문제가 발생한다.
따라서, 무상태성을 유지할 것인지, 보안을 강화할 것인지의 선택이 필요했다.
고민한 방법
1. 데이터베이스에 저장하는 방식
jwt의 무상태성은 일부 포기하지만 보안성을 강화할 수 있다.
사용자가 로그아웃하거나 refreshToken이 탈취되었을 경우, 이를 무효화할 수 있는 장점이 있다.
2. RefreshToken을 HttpOnly Secure Cookie에 저장하는 방식
브라우저 기반 웹 애플리케이션에서는 refresh token을 HttpyOnly 및 Secure 옵션이 활성화 된 쿠키에 저장할 수 있다.
JavaScript에서 접근이 불가능하기 때문에 XSS(크로스 사이트 스크립팅) 공격에 강하다는 장점이 있다.
나는 위의 두 방법을 모두 사용했다.
쿠키에만 저장하면 클라이언트가 refreshToken을 탈취당했을 때 무효화할 방법이 없다.
그러나 DB에도 저장하면 사용자가 로그아웃하거나, refreshToken이 탈취되었을 때 서버에서 한번더 검증이 들어가기 때문에 무효화가 가능하다.
그러나 이에도 단점은 존재하는데
무상태원칙이 깨지고, refreshToken을 관리하는 추가적인 테이블이 필요하고, accessToken을 검증할 때는 db조회가 필요없지만, refreshToken을 검증할 때는 db조회가 필요하므로 약간의 성능 부담이 증가한다는 점이다.
많은 글을 찾아본 결과 사용자 인증에는 정답은 없는 것 같다.
그때 그때 상황에 맞게 활용하면 될 것 같다는 생각을 했다.
사용자 인증에 관한 부분은 너무나도 방대하고 어려운 것 같다. 계속 공부하면서 지식을 쌓아가야겠다고 느꼈다.
'프로젝트' 카테고리의 다른 글
[Nginx,Django,React] 배포 - 이슈 정리 (0) | 2024.11.09 |
---|---|
[Refact] Enum 확장 (0) | 2024.10.30 |
[Refact] 코드 개선 (0) | 2024.10.30 |
[Error] ExcelFile & RDS 동기화 실시간 변경 데이터 적용 시 나타난 이슈s (0) | 2024.10.30 |
[Kafka] Producer로부터 받은 데이터를 DB에 적재하는 Consumer 구축 (0) | 2024.10.30 |