Redis를 활용한 중복로그인

유정현·2024년 3월 20일

Redis는

저장소이다. 메모리 저장소인데 DB는 물리저장소에 실제 저장이 되는데 redis는 그런게 아니다. 서버를 끄면 사라지는 휘발성이 있는 저장소이다. 아래는 gpt 답변이다.

redis를 사용할땐 두 가지 고려할 지점이 있다. 두 가지의 차이점은 key노트에 정리중이라 차후에 올릴 예정이다.
  • LettuceConnector vs JedisConnector
  • rdb 저장 vs AOF 저장 (redis persistence)

기능 요구사항 설계

  1. 로그인 요청시
    1. Interceptor의 postHandle 메소드에서 로그인 성공했으면 redis에 저장한다.
    2. 이미 저장된 key인지 확인을 한다.
    3. 저장된 상태면 session에 중복로그인이라 이전 로그인한 유저는 로그아웃 처리한다고 Attribute로 담아 보내준다.
  2. 로그인을 해야만 접근할 수 있는 기능
    1. redis에 있는 sessionId와 요청하는 sessionId가 같은 값인지 확인해준다.
    2. 다르다면 로그인 페이지를 전송한다.

환경설정

Redis를 써본적이 없지만 블로그 글을 보면서 하나씩 따라했다. 이렇게 하면 금방 머리에서 사라지지만, 구현에 있어서는 가장 빠르며, 대략적인 흐름을 파악할 수 있다.

  1. 의존성 설정 gradle에 추가해준다.
	implementation 'org.springframework.boot:spring-boot-starter-data-redis
  1. Redis Configuration을 만든다.
    redisConnectionFactory는 내가 yml파일에 설정해둔 값을 읽어서 커넥션을 가져온다. 이것을 이용하여 RedisTemplate을 만들어주는데 RedisTemplate이 redis에 읽기, 쓰기를 다 해준다. 그리고 주석으로도 작성했는데 데이터를 저장하거나 불러올 때 문자 그대로 되는 것이 아니라서 StringRedisSerializer를 통해 설정해준다.
@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisRepositoryConfig {

    private final RedisProperties redisProperties;


    @Bean
    public RedisConnectionFactory redisConFactory() {
        return new LettuceConnectionFactory(redisProperties.getHost(),
                redisProperties.getPort());
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConFactory());
        /**
         * setKeySerializer, setValueSerializer 설정해주는 이유는
         * RedisTemplate를 사용할 때 Spring - Redis 간 데이터 직렬화, 역직렬화 시
         * 사용하는 방식이 Jdk 직렬화 방식이기 때문입니다.
         *
         * 동작에는 문제가 없지만 redis-cli을 통해 직접 데이터를 보려고 할 때
         * 알아볼 수 없는 형태로 출력되기 때문에 적용한 설정입니다.
         * */
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

}
  1. 인터셉터 설정1
    로그인을 성공하고 돌아온 유저의 아이디가 redis 저장소에 있는지 확인후 있었다면, session에 메세지를 담아준다.
@Slf4j
@RequiredArgsConstructor
public class UserDupleLoginInterceptor implements HandlerInterceptor {


    private final RedisTemplate redisTemplate;

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                            ModelAndView modelAndView) throws Exception {
		// 로그인 요청 id와 session
        String loginId = request.getParameter("id");
        HttpSession session = request.getSession();
		
        // get은 loginForm이고, post가 로그인 요청이다. 
        // 로그인 성공 했을 때만 session에 로그인 id를 담아주기 때문에 두 아이디를 비교후 같으면 
        // 로그인 성공으로 처리한다.
        if(request.getMethod().toLowerCase().equals("post")
                && loginId.equals(session.getAttribute("loginId"))){
            ValueOperations valueOperations = redisTemplate.opsForValue();

			// 해당 아이디가 이미 redis 저장소에 있는지 확인한다.
            if(redisTemplate.hasKey(loginId)){
                log.info(loginId + " : 중복로그인 입니다. 기존 로그인은 로그아웃 됩니다.");
                request.getSession().setAttribute("message", "중복로그인 입니다. 기존 로그인은 로그아웃 됩니다.");
            }

            valueOperations.set(loginId, session.getId());
        }

        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
}
  1. 인터셉터 설정2
    만약 로그인을 해야만 사용할 수 있는 요청을 했을 땐 로그인을 했는지 확인하고, 로그인을 했다면 redis에 있는 세션 id와 현재 요청중인 sessionId가 같은지 확인한다. 다르면 바로 로그아웃 처리한다.
@Slf4j
@RequiredArgsConstructor
public class UserCertificationInterceptor implements HandlerInterceptor {

    private final RedisTemplate redisTemplate;

    public boolean preHandle(HttpServletRequest httpServletRequest, 
    	HttpServletResponse httpServletResponse, Object handler ) throws  Exception {

        HttpSession session = httpServletRequest.getSession();
        String loginId = (String) session.getAttribute("loginId");
        String sessionId = session.getId();

        ValueOperations valueOperations = redisTemplate.opsForValue();

        String redisSessionId = (String) valueOperations.get(loginId);

        // 로그인을 하지 않았거나 || 레디스에 저장된 세션아이디와 현재 요청중인 세션 아이디가 다르다면
        if (ObjectUtils.isEmpty(redisSessionId) || !sessionId.equals(redisSessionId)){
            httpServletResponse.sendRedirect("/user/login");
            return false;
        } else {
            return true;
        }
    }

}

마무리

이렇게 중복로그인이 방지가 되긴 했는데 일단 이렇게 해서 테스트를 해보니 되긴 했는데 아쉬움이 몇가지 남는다.

  1. redis를 중복로그인을 위해서만 사용해서 그렇지 여러 용도로 사용하게 된다면, 단순히 id, session id를 key-value로 저장하면 안 될 것 같다. 다른 특정 List를 만들고 사용해야 될 것 같다.
  2. 1번과 연결된 문제인데 확장하는 방향으로 RedisConfig 클래스를 좀 더 기능별로 나누거나, 저장 리스트별로 나누는 등 좀 더 객체지향적인 코드를 작성할 여지가 있을 것 같다.
  3. Redis 설정에 대한 지식이 부족하다. 아까 지나가며 보기로는 LettuceConnectionFactory로 커넥션을 만드는 방식이 있고, 다른 방식이 하나 더 있는 것으로 알고있는데 뭐가 다른지 아직은 모른다.

redis를 아예 처음 써보고 설치도 3시간 전에 해본거라 아쉬움을 느끼는 부분들이 실제로 아쉬운 부분인지는 좀 더 공부해봐야 알 것 같다.

profile
코딩하는 감자입니다.

0개의 댓글