개발놀이터

JWT 필터 리팩토링 본문

리팩토링

JWT 필터 리팩토링

마늘냄새폴폴 2023. 7. 23. 19:07

이번 포스팅에선 제 프로젝트의 JWT 필터를 리팩토링 해보도록 하겠습니다. 우선 제 코드를 보여드릴텐데요. 정말 그지같지않을 수 없습니다. 

 

if else if else 몇번을 진행한건지 참...

 

한번 리팩토링 진행해보도록 하겠습니다. 클린 코드를 읽은 개념들을 바탕으로 리팩토링 해보겠습니다. 

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 1. Request Header 에서 JWT 토큰 추출
        String token = resolveToken((HttpServletRequest) request);
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // 2. validateToken 으로 토큰 유효성 검사
        if (token != null  && isValidJwtFormat(token) && jwtTokenProvider.validateToken(token)) {
            // Redis 에 해당 accessToken logout 여부 확인
            String isLogout = (String) redisTemplate.opsForValue().get(token);

            if (ObjectUtils.isEmpty(isLogout)) {
                // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
                Authentication getAuthentication = jwtTokenProvider.getAuthentication(token);
                HttpSession session = httpRequest.getSession();
                session.setAttribute("loginId", getAuthentication.getName());
                SecurityContextHolder.getContext().setAuthentication(getAuthentication);
            }
        }
        else if (authentication != null && authentication.isAuthenticated()) {  // 3. access token 이 만료된 상황
            // token 은 만료 되었으나 인증이 되어있는 사용자 = 아직 페이지를 안벗어났지만 token 의 유효시간은 끝난 사용자
            UserResponseDto tokenInfo = (UserResponseDto) redisTemplate.opsForValue().get("RT:" + authentication.getName());
            if (tokenInfo != null) {
                setNewAuthentication((HttpServletResponse) response, authentication);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        else { // 4. access token 만료되고 Authentication 객체도 null 인 상황
            String sessionId = getSessionId((HttpServletRequest) request);
            Authentication authenticationFromSessionId = null;
            RedisAndSession redisAndSession = null;
            if (sessionId != null) {
                redisAndSession = redisTokenRepository.findBySessionId(sessionId).orElse(null);
            }

            if (redisAndSession != null) {  // refresh token 이 존재하는 경우 존재하지 않으면 로그인이 풀린다.
                Member findMember = memberRepository.findBySessionId(sessionId).orElse(null);
                User findUser = userRepository.findBySessionId(sessionId).orElse(null);

                if (findMember == null && findUser != null) {   // User 객체만 존재할 경우
                    Collection<? extends GrantedAuthority> authorities =
                            Arrays.stream(findUser.getRole().toString().split(","))
                                    .map(SimpleGrantedAuthority::new)
                                    .collect(Collectors.toList());
                    UserDetails principal = new org.springframework.security.core.userdetails.User(findUser.getEmail(), "", authorities);
                    authenticationFromSessionId = new UsernamePasswordAuthenticationToken(principal, "", authorities);
                    setNewAuthentication((HttpServletResponse) response, authenticationFromSessionId);
                    SecurityContextHolder.getContext().setAuthentication(authenticationFromSessionId);
                }
                else if (findUser == null && findMember != null) {  // Member 객체만 존재할 경우
                    Collection<? extends GrantedAuthority> authorities =
                            Arrays.stream(findMember.getRole().toString().split(","))
                                    .map(SimpleGrantedAuthority::new)
                                    .collect(Collectors.toList());
                    UserDetails principal = new org.springframework.security.core.userdetails.User(findMember.getUsername(), "", authorities);
                    authenticationFromSessionId = new UsernamePasswordAuthenticationToken(principal, "", authorities);
                    setNewAuthentication((HttpServletResponse) response, authenticationFromSessionId);
                    SecurityContextHolder.getContext().setAuthentication(authenticationFromSessionId);
                }
                else if (findMember != null && findUser != null) {  // 둘 다 존재하는 경우 우선순위는 Member
                    Collection<? extends GrantedAuthority> authorities =
                            Arrays.stream(findMember.getRole().toString().split(","))
                                    .map(SimpleGrantedAuthority::new)
                                    .collect(Collectors.toList());
                    UserDetails principal = new org.springframework.security.core.userdetails.User(findMember.getUsername(), "", authorities);
                    authenticationFromSessionId = new UsernamePasswordAuthenticationToken(principal, "", authorities);
                    setNewAuthentication((HttpServletResponse) response, authenticationFromSessionId);
                    SecurityContextHolder.getContext().setAuthentication(authenticationFromSessionId);
                }
                else {  // 둘 다 null 인 경우 에는 로그인이 풀린다.
                    chain.doFilter(request, response);
                }
            }
        }
        chain.doFilter(request, response);
    }

 

1. 난잡한 If문

우선 If 문이 굉장히 난잡합니다. 읽기 쉬운 함수로 따로 빼버리겠습니다. 

    if (token != null && isValidJwtFormat(token) && jwtTokenProvider.validateToken(token) {
    	//...
    }
    
    if (authentication != null && authentication.isAuthenticated()) {
    	//...
    }

이렇게 되어있던것을

    if (isTokenValidate(token)) {
    	//...
    }
    
    if (isAuthenticated(authentication)) {
    	//...
    }
    
    private boolean isAuthenticated(Authentication authentication) {
        return authentication != null && authentication.isAuthenticated();
    }

    private boolean isTokenValidate(String token) {
        return token != null && isValidJwtFormat(token) && jwtTokenProvider.validateToken(token);
    }

이렇게 뺐습니다. 클린코드 책에서 말한 것 처럼 조건문을 딱 봤을 때 어떤 일을 할지 짐작할 수 있어야하고 짐작한대로 작동해야 한다고 했습니다. 

 

짐작할 수 있는 동작이라고 인식될 수 있게 고민해서 이름을 지었습니다. 

 

2. 비대한 If문

        if (token != null  && isValidJwtFormat(token) && jwtTokenProvider.validateToken(token)) {
            // Redis 에 해당 accessToken logout 여부 확인
            String isLogout = (String) redisTemplate.opsForValue().get(token);

            if (ObjectUtils.isEmpty(isLogout)) {
                // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
                Authentication getAuthentication = jwtTokenProvider.getAuthentication(token);
                HttpSession session = httpRequest.getSession();
                session.setAttribute("loginId", getAuthentication.getName());
                SecurityContextHolder.getContext().setAuthentication(getAuthentication);
            }
        }

이 부분 If문이 조금 무거운 것 같습니다. 

 

        if (isTokenValidate(token)) {
            // Redis 에 해당 accessToken logout 여부 확인
            String isLogout = (String) redisTemplate.opsForValue().get(token);

            Authentication validatedAuthentication = validAuthentication(isLogout, token);
            setSession(httpRequest, validatedAuthentication);
        }
        
     private Authentication validAuthentication(String isLogout, String token) {
        if (ObjectUtils.isEmpty(isLogout)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
            Authentication getAuthentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(getAuthentication);
            return getAuthentication;
        }
        return null;
    }

    private void setSession(HttpServletRequest request, Authentication getAuthentication) {
        HttpSession session = request.getSession();
        session.setAttribute("loginId", getAuthentication.getName());
    }

이렇게 validAuthenticationsetSession 이렇게 두 함수로 꺼냈습니다. 

 

클린코드 책에서 함수의 인자를 결정할 때 0개가 베스트, 1개가 그다음, 2개가 그다음, 3개는 하지마라 라고 말한대로 최대한 인자를 2개로 줄이려고 노력했습니다.

 

코드만 봤을 때 어떤 로직일지 짐작함과 동시에 인자값에서도 그 행동을 인지할 수 있게 설계했습니다. 

            Authentication validatedAuthentication = validAuthentication(isLogout, token);
            setSession(httpRequest, validatedAuthentication);

위에서 보면 isLogout은 캐싱값에서 가져온 토큰이기 때문에 이 토큰 값과 http 에서 가져온 토큰을 서로 비교한다는 의미에서 인자를 두개로 설정했습니다. 

 

또한, requestsession을 꺼내서 Authentication 객체를 저장한다는 의미에서 setSession 이라는 함수로 꺼냈습니다. 

 

3. 코드의 중복

                if (findMember == null && findUser != null) {   // User 객체만 존재할 경우
                    Collection<? extends GrantedAuthority> authorities =
                            Arrays.stream(findUser.getRole().toString().split(","))
                                    .map(SimpleGrantedAuthority::new)
                                    .collect(Collectors.toList());
                    UserDetails principal = new org.springframework.security.core.userdetails.User(findUser.getEmail(), "", authorities);
                    authenticationFromSessionId = new UsernamePasswordAuthenticationToken(principal, "", authorities);
                    setNewAuthentication((HttpServletResponse) response, authenticationFromSessionId);
                    SecurityContextHolder.getContext().setAuthentication(authenticationFromSessionId);
                }
                else if (findUser == null && findMember != null) {  // Member 객체만 존재할 경우
                    Collection<? extends GrantedAuthority> authorities =
                            Arrays.stream(findMember.getRole().toString().split(","))
                                    .map(SimpleGrantedAuthority::new)
                                    .collect(Collectors.toList());
                    UserDetails principal = new org.springframework.security.core.userdetails.User(findMember.getUsername(), "", authorities);
                    authenticationFromSessionId = new UsernamePasswordAuthenticationToken(principal, "", authorities);
                    setNewAuthentication((HttpServletResponse) response, authenticationFromSessionId);
                    SecurityContextHolder.getContext().setAuthentication(authenticationFromSessionId);
                }
                else if (findMember != null && findUser != null) {  // 둘 다 존재하는 경우 우선순위는 Member
                    Collection<? extends GrantedAuthority> authorities =
                            Arrays.stream(findMember.getRole().toString().split(","))
                                    .map(SimpleGrantedAuthority::new)
                                    .collect(Collectors.toList());
                    UserDetails principal = new org.springframework.security.core.userdetails.User(findMember.getUsername(), "", authorities);
                    authenticationFromSessionId = new UsernamePasswordAuthenticationToken(principal, "", authorities);
                    setNewAuthentication((HttpServletResponse) response, authenticationFromSessionId);
                    SecurityContextHolder.getContext().setAuthentication(authenticationFromSessionId);
                }

보면 Collection 객체인 authorities 변수와 principal을 가져오는 부분, Authentication 객체를 설정하는 부분, Authentication으로 쿠키 설정하는 부분, SecurityContextAuthenticaiton 객체를 저장하는 부분까지 세개의 if 문에서 중복이 일어났습니다. 

 

이를 함수로 빼서 중복을 최소화했습니다. 

 

            if (redisAndSession != null) {  // refresh token 이 존재하는 경우 존재하지 않으면 로그인이 풀린다.
                Member findMember = memberRepository.findBySessionId(sessionId).orElse(null);
                User findUser = userRepository.findBySessionId(sessionId).orElse(null);

                if (findMember == null && findUser != null) {   // User 객체만 존재할 경우
                    setAuthenticationWithUser(httpResponse, findUser);
                }
                else if (findMember != null && findUser == null) {  // Member 객체만 존재할 경우
                    setAuthenticationWithMember(httpResponse, findMember);
                }
                else if (findMember != null && findUser != null) {  // 둘 다 존재하는 경우 우선순위는 Member
                    setAuthenticationWithMember(httpResponse, findMember);
                }
                else {  // 둘 다 null 인 경우 에는 로그인이 풀린다.
                    chain.doFilter(request, response);
                }
                
    private void setAuthenticationWithMember(HttpServletResponse response, Member findMember) {
        Authentication authenticationFromSessionId;
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(findMember.getRole().toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());
        UserDetails principal = new org.springframework.security.core.userdetails.User(findMember.getUsername(), "", authorities);
        authenticationFromSessionId = new UsernamePasswordAuthenticationToken(principal, "", authorities);
        setNewAuthentication(response, authenticationFromSessionId);
        SecurityContextHolder.getContext().setAuthentication(authenticationFromSessionId);
    }

    private void setAuthenticationWithUser(HttpServletResponse response, User findUser) {
        Authentication authenticationFromSessionId;
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(findUser.getRole().toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());
        UserDetails principal = new org.springframework.security.core.userdetails.User(findUser.getEmail(), "", authorities);
        authenticationFromSessionId = new UsernamePasswordAuthenticationToken(principal, "", authorities);
        setNewAuthentication(response, authenticationFromSessionId);
        SecurityContextHolder.getContext().setAuthentication(authenticationFromSessionId);
    }

Member 객체와 User 객체로 Authentication 객체를 만드는 것이기 때문에 두개로 함수를 나눴습니다. 

 

4. 최종본

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 1. Request Header 에서 JWT 토큰 추출
        String token = resolveToken((HttpServletRequest) request);
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 2. validateToken 으로 토큰 유효성 검사
        if (isTokenValidate(token)) {
            // Redis 에 해당 accessToken logout 여부 확인
            String isLogout = (String) redisTemplate.opsForValue().get(token);

            Authentication validatedAuthentication = validAuthentication(isLogout, token);
            setSession(httpRequest, validatedAuthentication);
        }
        else if (isAuthenticated(authentication)) {  // 3. access token 이 만료된 상황
            // token 은 만료 되었으나 인증이 되어있는 사용자 = 아직 페이지를 안벗어났지만 token 의 유효시간은 끝난 사용자
            UserResponseDto tokenInfo = (UserResponseDto) redisTemplate.opsForValue().get("RT:" + authentication.getName());
            if (tokenInfo != null) {
                setNewAuthentication((HttpServletResponse) response, authentication);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        else { // 4. access token 만료되고 Authentication 객체도 null 인 상황
            String sessionId = getSessionId(httpRequest);
            RedisAndSession redisAndSession = null;
            if (sessionId != null) {
                redisAndSession = redisTokenRepository.findBySessionId(sessionId).orElse(null);
            }

            if (redisAndSession != null) {  // refresh token 이 존재하는 경우 존재하지 않으면 로그인이 풀린다.
                Member findMember = memberRepository.findBySessionId(sessionId).orElse(null);
                User findUser = userRepository.findBySessionId(sessionId).orElse(null);

                if (findMember == null && findUser != null) {   // User 객체만 존재할 경우
                    setAuthenticationWithUser(httpResponse, findUser);
                }
                else if (findMember != null && findUser == null) {  // Member 객체만 존재할 경우
                    setAuthenticationWithMember(httpResponse, findMember);
                }
                else if (findMember != null && findUser != null) {  // 둘 다 존재하는 경우 우선순위는 Member
                    setAuthenticationWithMember(httpResponse, findMember);
                }
                else {  // 둘 다 null 인 경우 에는 로그인이 풀린다.
                    chain.doFilter(request, response);
                }
            }
        }
        chain.doFilter(request, response);
    }

    private boolean isAuthenticated(Authentication authentication) {
        return authentication != null && authentication.isAuthenticated();
    }

    private boolean isTokenValidate(String token) {
        return token != null && isValidJwtFormat(token) && jwtTokenProvider.validateToken(token);
    }

    private void setAuthenticationWithMember(HttpServletResponse response, Member findMember) {
        Authentication authenticationFromSessionId;
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(findMember.getRole().toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());
        UserDetails principal = new org.springframework.security.core.userdetails.User(findMember.getUsername(), "", authorities);
        authenticationFromSessionId = new UsernamePasswordAuthenticationToken(principal, "", authorities);
        setNewAuthentication(response, authenticationFromSessionId);
        SecurityContextHolder.getContext().setAuthentication(authenticationFromSessionId);
    }

    private void setAuthenticationWithUser(HttpServletResponse response, User findUser) {
        Authentication authenticationFromSessionId;
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(findUser.getRole().toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());
        UserDetails principal = new org.springframework.security.core.userdetails.User(findUser.getEmail(), "", authorities);
        authenticationFromSessionId = new UsernamePasswordAuthenticationToken(principal, "", authorities);
        setNewAuthentication(response, authenticationFromSessionId);
        SecurityContextHolder.getContext().setAuthentication(authenticationFromSessionId);
    }

    private Authentication validAuthentication(String isLogout, String token) {
        if (ObjectUtils.isEmpty(isLogout)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
            Authentication getAuthentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(getAuthentication);
            return getAuthentication;
        }
        return null;
    }

    private void setSession(HttpServletRequest request, Authentication getAuthentication) {
        HttpSession session = request.getSession();
        session.setAttribute("loginId", getAuthentication.getName());
    }

 

마치며

확실히 리팩토링을 하니까 전보다는 읽기 더 쉬워진 느낌이 있습니다. 아직 책을 정독하는 중이기 때문에 다른 리팩토링 기법을 보고 다시 한번 손을 대볼까 합니다. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~