외부 Session Storage 사용 이유
다수의 서버에서 세션을 관리하는 방법은 여러가지가 있는데, 최종적으로 외부 Session Storage를 사용하도록 결정했습니다.
외부에서 로그인 정보를 따로 관리한다면, 다음과 같은 장점이 있습니다.
- 톰캣 서버에 문제가 있더라도 영향을 받지 않을 수 있다.
- 배포하는 과정에서 생기는 세션 관리에도 전혀 영향을 받지 않는다
- 여러 서버 세션 정보의 정합성을 유지할 수 있다
세션을 다루는 부분을 Redis에서 관리하도록 개선할 예정입니다. Redis는 In-memory 저장소로, key-value 방식을 이용해 고속 쓰기 및 읽기 처리가 가능합니다. 세션 자체가 복잡한 연산과 계산을 필요로 하지 않으므로 적절합니다.
Redis 코드에 추가하기
gradle에 의존성 추가하기
gradle 의존성에 다음을 추가합니다
implementation 'org.springframework.session:spring-session-data-redis:2.7.0'
해당 의존성은 spring의 session을 redis에서 관리하도록 하는 의존성입니다. 이에 따라서, spring에서 각 tomcat에 관리하는 session을 설정된 redis에 연결하게 됩니다.
application.yml에 redis 정보 추가하기
host는 IP주소인데 로컬에 설치했기 때문에 localhost, port는 deafult인 6379로 했습니다.
@EnabledRedisHttpSession
@EnabledRedisHttpSession을 통해 Redis 세션 사용을 설정합니다. 아래 주석에 따르면, @Configuration으로 빈을 등록하는 곳에 @EnableRedisHttpSession을 사용합니다. springSessionRepositoryFilter라고 이름붙여진 SessionRepositoryFilter를 구현하여 빈을 생성하며 Spring Session을 Redis에서 이용할 수 있습니다.
RedisConnectionFactory를 제공하기 위해서, Lettuce를 이용합니다.
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 30)
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration
= new RedisStandaloneConfiguration();
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
}
또한, maxInactiveIntervalInSeconds를 사용해, 세션 유지 기간을 설정할 수 있습니다. 여기서는 30초로 제한합니다.
왜 Spring은 Jedis 대신에 Lettuce를 사용할까?
Lettuce, Jedis 모두 유명한 Redis Client 오픈소스 입니다. 현재 Spring에서는 Redis Client default로 Lettuce를 사용하고 있습니다. 이유는 다음과 같습니다.
Jedis의 경우, 멀티 쓰레드 환경에서 하나의 Jedis 인스턴스를 공유하고 싶을 때, 쓰레드 안정성을 보장하지 않습니다. Jedis는 멀티 쓰레드 환경에서 pooling 연결 방식입니다. Jedis를 사용하는 각 동시성을 지닌 스레드는 Jedis가 상호 작용하는 동안 자체 Jedis 인스턴스를 가져옵니다. 이는, Redis 연결이 증가하여 Jedis 인스턴스가 늘어날 때마다 물리적인 연결 비용이 발생합니다.
Lettuce는 netty 기반으로 멀티 쓰레드 환경에서 상태를 가지고 공유될 수 있습니다. 따라서, 멀티 쓰레드 어플리케이션이 Letuce와 상호 작용하는 동시성을 가진 쓰레드의 개수와 상관없이 하나의 연결만을 사용하면 됩니다.
따라서, Spring Boot 2.0 부터는 Jedis 대신에 Lettuce를 사용한다고 합니다.
로그인 방식 결정하기
로그인 구현을 위한 다양한 방법이 있습니다. Cookie, Session을 이용할 수도 있지만, Spring을 사용하는 경우, 세션 관리 뿐 아니라 다양한 인증과 권한을 처리할 수 있는 Spring Security 라이브러리가 있습니다. 이 안에서 다시 front를 어떤 방식으로 구현하느냐에 따라서 사용 방식이 달라집니다. mustache라는 템플릿을 이용했기 때문에, JWT까지는 사용하지 않아도 되겠다고 판단하였고, form-login 기능을 사용하였습니다.
* form-login 로그인 구현 코드에 추가하기
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityJavaConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.formLogin(login ->
login
.loginPage("/login")
.loginProcessingUrl("/loginprocess")
.permitAll()
.defaultSuccessUrl("/", false)
.failureUrl("/login-error")
)
.logout(logout ->
logout
.logoutSuccessUrl("/")
)
.sessionManagement(s->
s
.sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::changeSessionId)
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredUrl("/")
.sessionRegistry(sessionRegistry())
);
...
}
WebSecurityConfigurerAdapter : 이 추상 클래스를 구현하여 WebSecurityConfigurer 객체를 손쉽게 생성할 수 있도록 합니다. 특히, configure() 메서드를 재정의하여 HttpSecurity를 이용해 다양한 설정을 할 수 있습니다.
@EnableGlobalMethodSecurity(prePostEnabled = true) : preAuthorize 메서드를 활성화시킵니다. 특정 메서드를 실행하기 전에, Security 검사를 할 수 있습니다.
configure(HttpSecurtiy http) : HttpSecurity를 설정합니다. 다양한 요청을 막을 수도 있고, 허용할 수도 있습니다. CSRF, XSS 관련 설정도 가능하며, 공통 취약점으로부터 보호하기 위한 설정을 합니다.
로그인 관련 설정을 먼저 살펴보겠습니다.
loginPage : 로그인을 시도하는 url 주소입니다. Spring Security 라이브러리에서 기본 페이지를 만들어 줍니다.
loginProcessingUrl : 로그인을 시도하는 url입니다. 등록된 id, 비밀번호에 따라서 검증합니다.
permitAll : 어떠한 인증정보 없이도 url을 허용합니다.
defaultSuccessUrl : 로그인이 성공하면 redirect되는 url입니다.
failureUrl : 로그인이 실패하면 redirect되는 url입니다.
로그아웃은 /logout 을 POST로 요청하면 로그아웃이 되며, logoutSuccessUrl은 로그아웃 성공 시 redirect는 url입니다.
세션 관련 설정을 살펴보겠습니다.
sessionFixation : 세션 고정관련 정책을 정합니다. changeSessionId를 통해, 재로그인시마다 세션 아이디를 바꾸도록 합니다. none을 설정하면 세션 아이디가 고정되는데, 이는 탈취당했을 때, 취약합니다.
maximumSessions : 세션의 최대 갯수를 설정합니다.
maxSessionPreventsLogin : 최대 세션 갯수를 초과했을 때 로그인을 허용하는지 유무를 설정합니다.
sessionRegistry : 세션을 관리하는 저장소를 설정합니다. Spring Security에서 제공하는 저장소를 사용합니다.
* 로그인하고 Redis 내부 살펴보기
redis는 다음과 같은 명령어로 클라이언트에 접속 가능합니다.
redis-cli
로그인을 하고 레디스 세션을 조회하니 총 4개의 정보가 생성되어 있습니다.
1) "spring:session:sessions:expires:$sesssionId (string)
스프링 세션의 만료시간을 관리하는 key입니다.
2) "spring:session:expirations:$expireTime" (set)
스프링 세션의 만료시간입니다.
3) "spring:session:sessions:$sessionId" (hash)
생성된 스프링 세션 데이터입니다.
4) "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:$username (set)
username으로 세션을 가져올 수 있도록 저장되는 인덱스입니다.
세션 정보에 담겨진 정보를 확인해보겠습니다.
해당 데이터는 hash 저장소이므로 hgetall을 이용해 조회하면, 각 hash key에 따른 값들을 조회할 수 있습니다.
로그아웃을 하고 다시 조회해보겠습니다.
로그아웃을 했는데, 2개의 정보가 남아있습니다.
먼저 2번 만료시간의 경우, 세션 만료 시간인 millisecond를 우리가 아는 일반적인 시간으로 변환하면 다음과 같습니다.
to UTC time으로 10:29:00 이 기록되어있습니다. 이는, 로그아웃을 했지만, 세션이 10:29:00에 만료된다는 것을 알려주는 정보입니다. 로그아웃이 되어도, 세션의 정보가 남아있고, 만료시간이 관리됨을 알 수 있습니다. 왜냐하면 로그인 정보와 세션의 생명주기가 완전히 같지 않기 때문입니다. 단지 세션에 로그인 정보를 담고 있을 뿐입니다.
세션 만료시간이 지나면 2번이 지워지고 1번 정보만 남습니다.
1번 정보는 그렇다면 언제 제거될까요? 바로 2번에서 설정된 만료시간의 5분 후입니다. 세션이 만료 될 때, 세션에 어떤 값을 사용하기 위해 요청과 접근이 올 수 있습니다. 따라서, 자체적으로 만료시간에 5분을 더해서 수명이 연장되고 5분 후 삭제됩니다.
참고
https://www.javadevjournal.com/spring/spring-session-with-redis/
https://jojoldu.tistory.com/418
https://github.com/spring-projects/spring-session/issues/789
https://escapefromcoding.tistory.com/487
https://hyperconnect.github.io/2018/10/21/spring-session-migration.html
https://redis.com/blog/jedis-vs-lettuce-an-exploration/
https://docs.spring.io/spring-data/data-redis/docs/2.3.4.RELEASE/reference/html/#why-spring-redis
'문제 해결, 기술 비교 > 개인프로젝트(북클럽)' 카테고리의 다른 글
In-memory Redis vs Memcached 비교하기 (0) | 2022.06.21 |
---|---|
cache란? Redis를 cache로 조회 성능 개선하기 (0) | 2022.06.20 |
분산 서버에서 Session 관리하기 (0) | 2022.06.17 |
sentry.io로 에러 로그 관리하기 (0) | 2022.06.01 |
Elastic Search 연동 및 테스트하기 (0) | 2022.05.27 |