1st - 회원가입, 로그인, 인증 (JWT 토큰 및 쿠키 사용)

Jobmania·2023년 6월 12일
0
post-thumbnail

이전까지는 API로 프로젝트를 해봤다면 JSP나 Thymeleaf를 통한 템플릿 엔진으로 서버사이드 렌더링을 활용해보기로 했다.

그 과정에서 당연히 서비스의 기본이 되는 로그인 기능부터 구현해보기로 하였다.

당연히 인증 방식에 대해선 세션을 통한 인증, 토큰을 통한 인증 둘 중 하나를 고려해야한다.

인증 방식 고려

토큰 기반 인증 방식과 세션 기반 인증 방식은 각각 장단점을 가지고 있다. ChatGPT 및 여러 블로그를 참조한 자료이다!

1. 토큰 기반 인증 방식의 장점:

  • 확장성: 토큰은 서버의 인증 상태를 저장하지 않고, 클라이언트 측에 저장하기 때문에 서버의 확장성이 용이. 다중 서버 환경에서도 각 서버가 독립적으로 토큰을 검증을 할 수 있다. 즉 수평적 확장에 유리하다는 말이다!
  • 분리된 인증과 인가: 토큰 기반 인증은 인증과 인가를 분리할 수 있습니다. 토큰은 클라이언트가 인증된 사용자임을 증명하고, 서버는 해당 토큰의 권한을 확인하여 인가를 처리할 수 있따.
  • 클라이언트와 서버 간의 상태 비저장: 토큰은 서버에 상태를 저장하지 않고, 클라이언트의 요청에 응답하는 데 필요한 모든 정보를 포함하고 있기 때문에 서버에 부담을 주지 않는다.

하지만 😑

토큰 중 하나인 JWT는 사용자 인증 정보와 토큰의 발급시각, 만료시각, 토큰의 ID등 담겨있는 정보가 세션 ID에 비해 비대하므로 세션 방식보다 훨씬 더 많은 네트워크 트래픽을 사용한다.

  • 네이버 세션
  • JWT 토큰값

    그럴수 밖에 없는게 세션 ID는 단순히 랜덤값이지만 토큰값은 암호화된 정보이기 때문
    또한!
    토큰은 서버가 트래킹하지 않고, 클라이언트가 모든 인증정보를 가지고 있다.
    따라서 토큰이 한번 해커에게 탈취되면 해당 토큰이 만료되기 전까지는 속수무책으로 피해를 입을 수 밖에 없다. -> 그래서 인증토큰 만료시간을 짧게 잡는다.

2.세션 기반 인증 방식의 장점:

  • 간편한 구현: 세션 기반 인증은 일반적으로 세션을 사용하여 인증을 처리하기 때문에 개발 및 구현이 비교적 간단하다. Spring Security를 통해 간편히 기능 구현이 가능하다.
  • 서버 측에서 세션 관리: 서버가 세션 데이터를 관리하고 유효성을 검사하기 때문에 클라이언트는 세션에 대한 걱정을 할 필요가 없다.
  • 세션 데이터의 변경 관리: 세션은 서버 측에서 관리되므로 세션 데이터의 변경이 필요할 경우 서버 측에서 처리할 수 있습니다. -> 그래서 세션 값이 탈취되더라도 서버에서 해당 세션을 종료시키면 된다

하지만 😑

일반적으로 웹 어플리케이션의 서버 확장 방식은 수평 확장을 사용한다. 수평적 vs 수직적
즉, 한대가 아닌 여러대의 서버가 요청을 처리하게 된다. 이때 별도의 작업을 해주지 않는다면, 세션 기반 인증 방식은 세션 불일치 문제를 겪게 된다.
만약 세션 정보를 공유하지 않으면 사용자가 한 서버에서 로그인한 후에 다른 서버로 이동했을 때 로그인 상태가 유지되지 않게된다.
다중 서버 환경에서 세션 기반 인증을 사용할 때는 세션 정보를 공유하기 위해서 세션 공유방법이 필요하다!


이러한 고려 과정에서 나는 수평적 확장에 유리, 서버부담완화를 고려해 JWT 토큰 인증 방식으로 로그인을 구현할 것이다.

프로젝트 코드


모든 요청이 들어올 때 Filter에서 인증 유무를 판단하여 이후 로직을 실행할 것이다.

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러

또한 Spring Security에서는 인증과 접근 제어 기능이 Filter로 구현되어진다.


@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final TokenProvider tokenProvider;

    // h2 database 테스트가 원활하도록 관련 API 들은 전부 무시
    // local 관련 파일 접속들 허용
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .antMatchers("/h2-console/**", "/favicon.ico", "/js/**", "/image/**", "/css/**", "/scss/**");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /** security filter 설정 변경
     * https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
     * */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        JwtFilter customFilter = new JwtFilter(tokenProvider);

        // CSRF 설정 Disable, JWT는 CSRF 공격 걱정 ㄴㄴ
        http.csrf().disable()

                // h2-console 을 위한 설정을 추가
                .headers()
                .frameOptions()
                .sameOrigin()

                // 세션 관련 설정 : 사용하지 않기때문에 stateless
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                // 모든 url 인증  
                .and()
                .authorizeRequests()
                // 허용 url 설정
                .antMatchers("/member/auth/**").permitAll() // 로그인, 회원가입 관련
                .antMatchers("/").permitAll() // 홈화면
                .anyRequest().authenticated()


                
                .and() // 지정된 필터 앞에 커스텀 필터를 추가 (UsernamePasswordAuthenticationFilter 보다 먼저 실행된다)
                .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

}

SecurityConfig에서 인증 설정을 구성하고 허용할 범위를 설정한다.

나는 여기서 마지막 부분의 addFilterBefore를 통해 Customfilter인 jwtfilter를 적용할 것이다.


@Slf4j
public class JwtFilter extends OncePerRequestFilter {

  
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private final TokenProvider tokenProvider;


    public JwtFilter(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }



    private String resolveToken(HttpServletRequest request){ // 토큰정보 획득, 쿠키값 이슈로 'Bearer ' 변경
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer_")){
            return bearerToken.substring(7);
        }
        return null;
    }


    private String resolveCookie(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();

        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                    log.info("쿠키 값 찾기 ={}",cookie.getValue().substring(7));
                    return cookie.getValue().substring(7);
                }
            }
        }

        log.info("쿠키 못꺼내옴");
        return null;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {

        // Request Header 에서 토큰을 꺼냄
//        String jwt = resolveToken(servletRequest);

        // 1.  쿠키에서 값 꺼내야 됨
        String jwt = resolveCookie(servletRequest);
        log.info("jwt 값={}",jwt);

        String requestURI = servletRequest.getRequestURI();

        // 2. validateToken 으로 토큰 유효성 검사
        // 정상 토큰이면 해당 토큰으로 Authentication 을 가져와서 SecurityContext 에 저장

        if(StringUtils.hasText(jwt)&& tokenProvider.validateToken(jwt)){
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            // 유저 저장.
            SecurityContextHolder.getContext().setAuthentication(authentication);
              log.debug("Security Context, Member_ID ='{}' 인증정보 저장 및 조회 , uri= {} ",authentication.getName(),requestURI);
        }else {
            log.debug("통과 url : {} ",requestURI);
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

OncePerRequestFilter는 그 이름에서도 알 수 있듯이 모든 서블릿에 일관된 요청을 처리하기 위해 만들어진 필터이다.

이 추상 클래스를 구현한 필터는 사용자의 한번에 요청 당 딱 한번만 실행되는 필터를 만들 수 있다.

이 필터가 하는일은 JWT 토큰값에 대한 검증을 할 것이다. 인증이 필요한 URL 접속시 쿠키값에 들어있는 토큰값을 꺼내고 TokenProvider에게 전달한다. 그리고 SecurityContextHolder 저장하는데 우선 유저 정보를 저장한다고 생각하면 된다.

이 이후에는 어떻게 JWT 토큰을 복호화 하고 인증을 처리하는지 알아보자.

profile
HelloWorld에서 RealWorld로

0개의 댓글