[Spring security] - jwt와 쿠키 그리고 로그인 유지

yeom yaloo·2023년 8월 17일
0

쇼핑몰

목록 보기
18/19
post-thumbnail

[들어가기에 앞서..]

거의 몇주에 걸쳐서 MSA 구조의 인증와 로그인 유지 작업을 진행했다.

사실 jwt filter 적용에 문제가 있었고,spring cloud gateway를 통한 클라이언트 요청 헤더에 token을 어떻게 처리할지에 고민이 많았다.

1. 그럼 어떤 방법들을 대안책으로 사용했나?

  1. 기본적으로 스프링 클라우드 게이트웨이를 사용하지 않고 요청 헤더로 넘어오는 토큰을 인증할 수 있는 방법은 없을까? 라는 고민을 했다.
  • 그래서 선택한 방법이 인증/인가 서버에서 해당 인증을 진행할 때 jwt토큰 발급을 진행하고
  • API 서버를 통해서 해당 회원의 정보를 가져오는 작업을 진행할 때 API Server 내에도 스프링 시큐리티를 적용하고 SecurityMethod를 사용할 수 있게 하여 해당 토큰에 대한 인증을 여기서 하려고 했다.
  • 이때 security context에 해당 인증 회원 객체를 넣어주는 식으로 진행을 하기로 했는데 문제는 클라이언트 서버에서 해당 인증 객체를 받아오기 위해서는 번거로운 작업이 추가적으로 진행이 되어야 했다.
  1. 1번을 충족시키기 위해서는 다른 서버에서 저장된 context를 공유해야했다.
  • 이는 아무리 생각해도 바람직하지 않다는 생각이 들었다.
  • 공유를 위한 여러 작업이 필요했기 때문이다.
  • 또한 클라이언트 서버측에서만 회원의 정보가 필요했기 때문에 해당 작업을 위해서 여러곳에서 이 작업을 진행하여 공유하는 방식은 부적절하다 생각 됐다.

2. 그래서 결국엔 나는 어떤 방식을 택했나?

  1. OnceperRequestFilter를 적용
  • 회원 로그인(인증) 작업의 경우엔 해당 로직이 한 번만 작동하는 특성이 있기 때문에 굳이 다른 필터를 사용할 필요가 없었다.
  • 그래서 OnceperRequestFilter를 도입하기로 했다.
  • 해당 필터는 시큐리터 필터가 아니기 때문에 시큐리터 필터에 적용해서 사용하고 싶다면 위치를 지정해서 추가해주어야 한다 addFilterAfter or addFilterBefore 등..
  1. Cookie에 회원의 정보를 저장하는 방식 대신 회원용 UUID를 랜덤 발급해서 해당 값을 이용한 redis 접근, 데이터 이용을 진행
  • 쿠키에는 민감한 정보를 저장하게 되면 이를 조작하기 쉬워지기 때문에 민감 정보를 넣어두는 등의 방식은 지양해야 한다.
  • 그러나 내가 저장하는 값은 회원이 로그인할 때 랜덤으로 발급한 UUID 값이기 때문에 이를 저장, 이용해서 redis에 접근하고 해당 필터가 작동할 때 security context에 해당 정보를 이용한 UsernamepasswordAuthenticationToken을 넣어주는 식으로 진행하기로 했다.
  1. 요청 헤더로 넘어오는 Authorization: Bearer은 어떻게 매번 유효한지 확인 작업을 진행할 예정인가?

[로그인 관련 흐름]

1. 기존의 로그인 흐름

2. CustomJwtAuthenticationFilter를 적용한 로그인 흐름

3. CustomJwtAuthenticationFilter

import com.yaloostore.front.auth.jwt.meta.AuthInformation;
import com.yaloostore.front.common.utils.CookieUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import static com.yalooStore.security_utils.util.AuthUtil.HEADER_UUID;
import static com.yalooStore.security_utils.util.AuthUtil.JWT;


@RequiredArgsConstructor
@Slf4j
public class CustomJwtAuthenticationFilter extends OncePerRequestFilter {


    private final CookieUtils cookieUtils;

    private final RedisTemplate redisTemplate;




    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String member = cookieUtils.getUuidFromCookie(request.getCookies(), HEADER_UUID.getValue());

        if (Objects.isNull(member)){
            filterChain.doFilter(request,response);
            return;
        }

        if (!(SecurityContextHolder.getContext().getAuthentication() instanceof AnonymousAuthenticationToken)){

            AuthInformation authInformation = (AuthInformation) redisTemplate.opsForHash().get(member, JWT.getValue());

            log.info("auth loginId= ==== == {} ", authInformation.getLoginId());

            Collection<? extends GrantedAuthority> authorities = getGrantAuthority(authInformation.getAuthorities());

            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authInformation.getLoginId(), authInformation.getAccessToken(),authorities);

            SecurityContext context = SecurityContextHolder.getContext();
            context.setAuthentication(authenticationToken);

            filterChain.doFilter(request,response);
        }


    }

    private Collection<? extends GrantedAuthority> getGrantAuthority(List<String> authorities) {

        if (Objects.isNull(authorities)){
            return null;

        }
        return authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());

    }
}
  • 로그인에 성공한 경우라면 HEADER_UUID라는 이름의 쿠키를 생성, Redis에 필요한 정보들을 저장 하도록 로그인 로직에 넣어두었다.
  • 이때 이 HEADER_UUID라는 이름의 쿠키가 있다면 인증에 성공한 것이기 때문에 이를 가지고 redis에 저장한 값들을 가져와 작업을 진행한다.
  • OncePerRequestFilter를 사용해서 지정된 지점에서 한번만 해당 필터가 작동할 수 있게 했다.

3-1. 해당 작업에 쿠키를 사용한 이유는?

  • 쿠키는 민감한 정보를 저장해두면 탈취 당하고 조작 당해서 보안상 지양되는 저장 방식이지만 이 경우엔 로그인 정보가 아닌 redis에 접근할 때만 사용하는 UUID 값을 저장하는 것이기 때문에 쿠키를 사용했다.

4. Security configuration

4-1. 스프링 시큐리티 필터가 아닌 필터를 등록하자


 @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        
		//세션 대신 jwt 사용으로 해당 작업에서 사용하지 않음을 명세
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.addFilterAt(customLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        http.addFilterAfter(customJwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
  • http.addFilterAfter(customJwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
  • 해당 필터의 경우엔 스프링 시큐리티가 제공하는 필터가 아닌 그냥 스프링 필터이다. 그렇기 때문에 해당 필터를 스프링 시큐리티 필터처럼 쓰려면 해당 스프링 시큐리티 필터의 순서에 끼워 넣어주어야 한다.
  • 그래서 사용한 것이 addFilterAfter, addFilterBefore 과 같은 메서드이다.

[결과 화면]

  • 타임리프의 기능을 사용해서 해당 사용자가 존재하면 다시 로그인 하지 못하게 막았다.(기능적으로도 막진 않고 뷰페이지로 넘어가는 버튼을 없앰)
  • 로그인이 된 사용자라면 해당 사용자의 마이페이지, 로그아웃, 카트 목록을 볼 수 있게 header부분을 변경해뒀다
profile
즐겁고 괴로운 개발😎

0개의 댓글