일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- 자바
- mongoDB
- 자료구조
- 파이썬
- IT
- Kafka
- OS
- 컴퓨터구조
- redis
- JavaScript
- Java
- Proxy
- 네트워크
- react
- MySQL
- Heap
- MSA
- 백준
- Data Structure
- JPA
- spring webflux
- 운영체제
- c언어
- Spring
- 알고리즘
- Galera Cluster
- Algorithm
- 디자인 패턴
- design pattern
- C
- Today
- Total
시냅스
JWT 대신 Session 을 쓰는 이유 (Redis Session Clustering, Spring Security) 본문
JWT 대신 Session 을 쓰는 이유 (Redis Session Clustering, Spring Security)
ted k 2023. 7. 17. 20:11Session vs JWT
현재 프로젝트에서는 Redis Session Clustering 을 사용하고 있습니다.
사용자 정보에 대해 JWT 대신 Session 을 사용하기로 하였고 이유는 다음과 같습니다.
- JWT 를 사용하려는 이유는 세션에서 감당하는 부하를 줄이기 위함이다. (혹은 Stateless 하게 유지하기 위해)
- 다만, JWT 는 header, payload, signature 로 이루어진 구조로 모든 정보들을 노출시키고 있다. (HTTPS 를 사용한다고 하더라도...) 또한 대칭키를 알고 있고 토큰이 탈취당한 상황이라면 너무나도 쉽게 보안에 취약해진다.
- 이러한 문제를 최소화시키기 위해 Refresh Token 을 사용한다.
- Access Token, Refresh Token 쌍으로 Access Token 의 만료시간을 짧게 설정하고 만료됐을 때 Access Token 을 Refresh Token 을 확인하여 새로 발급하겠다는 것이다.
- 그렇다면 서버에서는 Refresh Token 을 개별 사용자마다 갖고 있어야 한다.
- 대체로 Access Token 보다 만료 시간이 길다.
- 또한 Refresh Token 이 탈취되고 서버에서 Refresh Token 을 갱신하지 않았다면 다시 보안에 취약한 상태가 된다.
- 또한 JWT 는 모든 정보를 토큰 안에 담고 있으므로 세션 ID 보다 큰 크기를 갖고 있으므로 매 통신마다 주고 받아야 한다.
- Signature 를 통해 토큰이 유효한지 확인하려면 서버에서 다시 hashing 하고 decoding 해줘야한다.
- 종합하자면 Refresh Token 을 갖고 있어야 하기 때문에 똑같이 정보를 서버에서 저장하고 있어야 하고 덕분에 딱히 stateless 하지 않게 된다, 또한 JWT 특성상 정보를 지속적으로 노출시키고 있고 네트워크 통신에 cost 가 크다.
위와 같은 이유들로 JWT가 Session 에 비해 갖는 이점이 없는 것 같아 Session 을 사용하기로 하였습니다.
다만 분산 환경에서 Session 을 사용하려면 다시 고민이 필요하였습니다.
Sticky Session
Sticky Session 은 내가 접근한 서버로만 가겠다는 것입니다.
만약 A 서버에 로그인 했다면 나의 Session 정보는 A 세션에 있을 것이므로 A로만 접근하겠다는 것입니다.
다만 Sticky Session 은 Hotspot 문제를 일으킬 수 있고, Load Balancing 이 되지 않을 수 있습니다.
당연하게도, least connection 이나 static RR 알고리즘을 사용한다고 하더라도
내가 접근한 서버가 아니라면 요청하지 않을 것이기 때문입니다.
Tomcat Session Clustering
이러한 문제들 때문에 Tomcat 에서도 Session Clustering 을 제공합니다.
- all-to-all
- 세션의 모든 정보들을 다른 노드들에게 공유하는 것
- 모든 정보를 다시 다른 서버에 복사해서 넘겨주기 때문에 부하가 크다.
- primary-secondary
- source-replica 와 비슷하고, source 나 replica 가 아니라면 다시 source 에 접근해서 정보를 가져오게 된다.
- 따라서 세션 정보가 없는 서버에 접근했다면 세션 정보를 얻기 위해 source 와 통신해야 한다.
톰캣에서 제공하는 해법의 주된 것은 session 정보를 다른 서버에도 복제하겠다는 것입니다.
분산 환경을 쓰는 것은 많은 사용자에 대응하기 위함인데,
다수의 사용자 정보를 개별 서버에 다시 복제한다면 의미가 퇴색되는 것 같았습니다.
세션 정보에 대한 개별 서버에서의 저장과 이를 위한 네트워크 IO 가 발생할 수 있기 때문입니다.
따라서 분산환경에서 여러 애플리케이션에서 session 에 접근할 수 있는 제품이 필요했고,
DB 는 Disk IO 를 유발하므로 In-Memory Cache 를 사용하는 제품을 고려하였고
그 중 싱글스레드로 동시성 제어가 간편한 Redis 를 사용하기로 하였습니다.
자세한 설정법과 Spring Security와 함께 사용하며 문제가 됐던 사항은 아래에서 설명하겠습니다.
Spring Security
https://liltdevs.tistory.com/157
사용자 인증/인가 관련된 작업은 Spring Security 에 위임하고 있습니다.
물론 Spring Security 를 사용하지 않아도 가능하지만 제공해주는 이점이 더 크다고 판단하였습니다.
(권한 관련, 접근 관련, 인증/인가 관련 등등)
Spring Security 를 Session 방식으로 사용할 때 Spring Security 는 SPRING_SECURITY_CONTEXT 라는 것을 저장합니다.
SPRING_SECURITY_CONTEXT 는 말 그대로 사용자의 security context 입니다.
Spring Security 가 보안 관련 기능을 수행할 때 context 를 기준으로 하게 됩니다.
또한 로그인 할 때에는 인증 정보를 거치게 되면 자동으로 context 가 session 에 저장되기 때문에
(UserDetailsService 를 거칠 때 혹은 SecurityContextHolder.setContext하게 될 때)
redis 를 session 으로 사용할 때에도 마찬가지로 context 를 저장하는 것입니다.
context 내부에는 authentication 이
다시 authentication 내부에는 principal, credentials, authorities 가 들어있게 됩니다.
이때 principal 에는 다시 UserDetails 들어있습니다.
들어있는 UserDetails 는 AuthenticationManager 가 처리한 객체입니다.
하지만 이상하게도, 프로젝트를 진행하며 직렬화 관련 에러가 발생하여 확인해보니 UserDetails 가 아닌
UserDetailsService 를 커스텀한 CustomUserDetailsService 가 들어있었습니다.
Spring Security 에서 제공하는 객체들은 다른 애플리케이션(redis, mongo...) 에 대응하며 직렬화가 가능하지만
CustomUserDetailsService 에서 사용하는 JPA 관련 객체(Proxy, Interceptor...)들은 직렬화가 불가능하기 때문에
에러가 발생한 것으로 판단하고 원인을 찾아보았습니다.
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService, Serializable {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findUserByEmail(email)
.orElseThrow(() -> new ImheroApplicationException(ErrorCode.EMAIL_NOT_FOUND));
return new CustomUserDetails(user);
}
public class CustomUserDetails extends org.springframework.security.core.userdetails.User {
private User user;
public CustomUserDetails(User user) {
super(user.getEmail(), user.getPassword(), new ArrayList<>());
this.user = user;
}
public User getUser() {
return user;
}
}
}
- CustomUserDetailsService
CustomUserDetailsService 내부에 CustomUserDetails 를 갖고 있는 구조였습니다.
당연히 내부 객체에 대한 참조를 위해 외부 객체를 만들어야 하고 이를 Security 는 외부객체로 context 에 담았던 것입니다...
따라서 당연히 CustomUserDetails 를 외부로 떼내어 해결하였습니다.
아래에서 redis와 session 을 설정하는 방법을 알아보겠습니다.
Redis + Spring Security
session:
store-type: redis
redis:
host: localhost
port: 6379
- application.yaml
redis 의 설정은 놀랍게도 위가 전부입니다.
위의 설정을 통해 향후 HttpServletSession 이나, request 를 통해 session 을 얻게 되면
이는 redis 에서 관리하는 session 이 됩니다.
@PostMapping("/api/v1/users/login")
public Response<LoginResponse> login(@RequestBody LoginRequest loginRequest, HttpServletRequest request) {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
return Response.success(userService.login(loginRequest));
}
위의 코드는 session 을 사용하는 예시 코드입니다.
단순히 setAuthentication 을 하게 되면, session 에 저장하는 것과 같은 효과입니다.
또한 차후에 session 을 통해 context를 찾을 때에는
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY
위의 상수를 사용하면 context 를 찾을 수 있습니다.
session 은 말씀드렸던 것처럼 Redis Session 이 됩니다.
따라서 user 를 그저 session 에 넣는 코드처럼 보이지만 실제로는 Redis Session 에 넣어주고 있는 것입니다.
Redis 에서 확인해본 결과입니다.
session ID 와 SPRING_SECURITY_CONTEXT 를 사용하면 context 가 저장되어 있는 것을 확인할 수 있습니다.
끝!
'Java, Spring' 카테고리의 다른 글
Spring Boot 3++ 를 위한 Spring Batch migration 요약 (1) | 2023.11.18 |
---|---|
Spring 동시성 문제 해결 (비관적 락, Redis 분산락을 적용한 Annotation AOP) (0) | 2023.07.23 |
HikariCP 설정과 유의사항 (0) | 2023.05.03 |
구현하며 이해하는 Spring MVC (0) | 2023.04.16 |
Java 소켓으로 HTTP 요청 구현하기 (0) | 2023.04.16 |