jwt Filter 가독성 리팩토링 진행

2024. 4. 13. 23:26· 프로젝트/RESTAPI 추천 서비스
목차
  1. findCookieToken
  2. reGenerateToken
  3. setCookie
  4. authenticateUser
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("jwt 필터 동작");

        System.out.println(request.getRequestURI());
        // 헤더에서 액세스 토큰 추출
        Cookie[] cookies = request.getCookies();

        // 쿠키 자체가 없으면 401 에러 발생
        if (cookies == null) {
            log.error("쿠키 없음.");
            sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "쿠키가 존재하지 않습니다.");
            return;
        }

        // 액세스 토큰 추출
        String accessToken = Arrays.stream(cookies)
                .filter(c -> "accessToken".equals(c.getName()))
                .findFirst()
                .map(Cookie::getValue)
                .orElse(null);

        // 액세스 토큰이 없는 경우
        if (accessToken == null) {
            log.error("액세스 토큰이 없습니다.");
            sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "액세스 토큰이 존재하지 않습니다.");
            return; // 여기서 처리 종료
        }

        // userId 토큰에서 꺼냄.
        try {
            TokenInfo tokenInfo = jwtService.getUserId(accessToken);
            log.info("userId:{}", tokenInfo.getUserId());

            // 토큰이 만료됐으면
            if (tokenInfo.isExpired()) {
                // 리프레시 토큰 탐색
                String refreshToken = Arrays.stream(cookies)
                        .filter(c -> "refreshToken".equals(c.getName()))
                        .findFirst()
                        .map(Cookie::getValue)
                        .orElse(null);

                // 만약에 null이면
                if (refreshToken == null) {
                    log.error("리프레시 토큰이 없습니다.");
                    sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "리프레시 토큰이 존재하지 않습니다.");
                    return;
                }

                String result = jwtService.checkRefreshToken(refreshToken);

                if ("사용 불가".equals(result)) {
                    log.error("리프레시 토큰 문제 발생");
                    sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "리프레시 토큰 사용 불가(로그아웃 진행)");
                    return; // 여기서 처리 종료
                } else {
                    String reAccessToken = jwtService.createAccessToken(tokenInfo.getUserId());
                    String reRefreshToken = jwtService.createRefreshToken(tokenInfo.getUserId(), true);

                    if ("유저 없음".equals(reRefreshToken)) {
                        log.error("유저 없음");
                        sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "유저 정보 조회 실패");
                        return; // 여기서 처리 종료
                    }

                    // 액세스 토큰을 위한 쿠키 생성
                    Cookie accessTokenCookie = new Cookie("accessToken", reAccessToken);
                    accessTokenCookie.setHttpOnly(true);
                    accessTokenCookie.setSecure(true); // HTTPS를 사용하는 경우에만 true로 설정
                    accessTokenCookie.setPath("/");

                    // 리프레시 토큰을 위한 쿠키 생성
                    Cookie refreshTokenCookie = new Cookie("refreshToken", reRefreshToken);
                    refreshTokenCookie.setHttpOnly(true);
                    refreshTokenCookie.setSecure(true); // HTTPS를 사용하는 경우에만 true로 설정
                    refreshTokenCookie.setPath("/");

                    // 쿠키에 토큰 저장
                    response.addCookie(refreshTokenCookie);
                    response.addCookie(accessTokenCookie);

                    sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "JWT 재발급 완료");
                    return; // 여기서 처리 종료
                }
            }

            Optional<Member> userOptional = memberRepository.findByIdLogin(tokenInfo.getUserId());
            if(userOptional.isEmpty()) {
                log.info("유저 데이터 없음");
                sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "유저 데이터 없음");
                return;
            }

            Member member = userOptional.get();

            List<MemberRole> memberRoles = memberRoleRepository.findByMember(member);

            List<GrantedAuthority> authorities = memberRoles.stream()
                    .map(memberRole -> new SimpleGrantedAuthority(memberRole.getRole().name()))
                    .collect(Collectors.toList());

            // 권한 부여
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(member.getId(), null, authorities);

            // Detail을 넣어준다.
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);

            filterChain.doFilter(request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

현재 위에 보이는 코드가 JWT 필터에 만들어놓은 전체 코드.

 

위에서 사용하는 기능은 다음과 같다.

  1. 쿠키에서 JWT 토큰 확인
  2. 액세스 토큰 추출
  3. 토큰 만료 검증
  4. 만료되면 리프레시 토큰 추출 및 액세스 재발급
  5. 만료되지 않았으면 시큐리티컨텍스트 홀더에 유저 인증 정보 저장.

 

그런데 현재 코드는 많이 길고 알아보기 어렵다는 느낌을 받아서 이번에 코드를 좀 간결하고, 가독성 좋게 변경해보려고 한다.

 

우선 메서드를 분리하기로 결정했다.

아래 4개의 메서드를 추가해서 로직을 묶었다.

 

 

findCookieToken

(기존 로직)

// 헤더에서 액세스 토큰 추출
        Cookie[] cookies = request.getCookies();

        // 쿠키 자체가 없으면 401 에러 발생
        if (cookies == null) {
            log.error("쿠키 없음.");
            sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "쿠키가 존재하지 않습니다.");
            return;
        }

        // 액세스 토큰 추출
        String accessToken = Arrays.stream(cookies)
                .filter(c -> "accessToken".equals(c.getName()))
                .findFirst()
                .map(Cookie::getValue)
                .orElse(null);

기존에 있던 로직은 액세스 토큰 쿠키 파싱, 리프레시 토큰 쿠키 파싱이 따로 진행됐는데 거의 동일한 로직을 사용하기에 하나의 메서드로 묶었다.

 

(변경 로직)

// 쿠키에서 토큰 찾기                                                                       
private Optional<String> findCookieToken(HttpServletRequest request, String name) { 
    Cookie[] cookies = request.getCookies();                                        
                                                                                    
    return cookies == null ? Optional.empty() : Arrays.stream(cookies)               
            .filter(cookie -> name.equals(cookie.getName()))                        
            .findFirst()                                                             
            .map(Cookie::getValue);                                                 
}

request와 name을 파라미터로 넘겨서 accessToken인지 refreshToken인지 구분시켜서 파싱하도록 변경했다.

 

 

reGenerateToken

(기존 로직)

String reAccessToken = jwtService.createAccessToken(tokenInfo.getUserId());
String reRefreshToken = jwtService.createRefreshToken(tokenInfo.getUserId(), true);

if ("유저 없음".equals(reRefreshToken)) {
   log.error("유저 없음");
   sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "유저 정보 조회 실패");
   return; // 여기서 처리 종료
}

 

(수정 로직)

// 토큰 재생성                                                                                        
private boolean reGenerateToken(HttpServletResponse response, Long userId) throws IOException {  
    // 액세스 토큰을 위한 쿠키 생성                                                                          
    String accessToken = jwtService.createAccessToken(userId);                                   
    String refreshToken = jwtService.createRefreshToken(userId, true);                           
                                                                                                 
    if ("유저 없음".equals(refreshToken)) {                                                          
        log.error("유저 없음");                                                                      
        sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "유저 정보 조회 실패");                     
        return false;                                                                            
    }                                                                                            
                                                                                                 
    setCookie(response, "accessToken", accessToken);                                             
    setCookie(response, "refreshToken", refreshToken);                                           
                                                                                                 
    return true;                                                                                 
}

사실 여기는 별로 바뀐 건 없다.

아무래도 필터이고 스프링 스펙이 아니기에 글로벌 예외처리가 불가능했다. 그래서 response를 전달하기 위해 boolean 타입으로 true, false로 구분시켰다. 메인 비즈니스 로직에서 false가 나오면 바로 필터 빠져나오도록

 

 

setCookie

(기존 로직)

 // 액세스 토큰을 위한 쿠키 생성
Cookie accessTokenCookie = new Cookie("accessToken", reAccessToken);
accessTokenCookie.setHttpOnly(true);
accessTokenCookie.setSecure(true); // HTTPS를 사용하는 경우에만 true로 설정
accessTokenCookie.setPath("/");

// 리프레시 토큰을 위한 쿠키 생성
Cookie refreshTokenCookie = new Cookie("refreshToken", reRefreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setSecure(true); // HTTPS를 사용하는 경우에만 true로 설정
refreshTokenCookie.setPath("/");

// 쿠키에 토큰 저장
response.addCookie(refreshTokenCookie);
response.addCookie(accessTokenCookie);

 

(수정 로직)

// 쿠키에 토큰 저장                                                                      
private void setCookie(HttpServletResponse response, String name, String value) { 
    Cookie cookie = new Cookie(name, value);                                      
    cookie.setSecure(true);                                                       
    cookie.setHttpOnly(true);                                                     
    cookie.setPath("/");                                                          
    response.addCookie(cookie);                                                   
                                                                                  
    log.info("쿠키 생성 및 설정 완료");                                                    
}

사실 이 부분이 제일 지저분해보였다. 쿠키 저장 또한 동일한 로직이 반복되었기에 하나의 메서드로 묶었다.

 

 

authenticateUser

(기존 로직)

Optional<Member> userOptional = memberRepository.findByIdLogin(tokenInfo.getUserId());
if(userOptional.isEmpty()) {
	log.info("유저 데이터 없음");
	sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "유저 데이터 없음");
	return;
}

Member member = userOptional.get();

List<MemberRole> memberRoles = memberRoleRepository.findByMember(member);

List<GrantedAuthority> authorities = memberRoles.stream()
	.map(memberRole -> new SimpleGrantedAuthority(memberRole.getRole().name()))
	.collect(Collectors.toList());

// 권한 부여
UsernamePasswordAuthenticationToken authenticationToken =
	new UsernamePasswordAuthenticationToken(member.getId(), null, authorities);

// Detail을 넣어준다.
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

 

(수정 로직)

// 유저 인증                                                                                                                    
private boolean authenticateUser(HttpServletRequest request, HttpServletResponse response, Long userId) throws IOException {
    Optional<Member> byIdLogin = memberRepository.findByIdLogin(userId);                                                    
    if (byIdLogin.isEmpty()) {                                                                                              
        log.info("유저 데이터 없음");                                                                                              
        sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "유저 데이터 없음");                                                  
        return false;                                                                                                       
    }                                                                                                                       
                                                                                                                            
    Member member = byIdLogin.get();                                                                                        
    List<MemberRole> memberRoles = member.getMemberRoles();                                                                 
    List<SimpleGrantedAuthority> authorities = memberRoles.stream()                                                          
            .map(memberRole -> new SimpleGrantedAuthority(memberRole.getRole().name()))                                      
            .collect(Collectors.toList());                                                                                  
                                                                                                                            
    // 유저 인증 객체                                                                                                             
    UsernamePasswordAuthenticationToken authenticationToken =                                                               
            new UsernamePasswordAuthenticationToken(member.getId(), null, authorities);                                     
    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));                             
                                                                                                                            
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);                                              
                                                                                                                            
    return true;                                                                                                            
}

 

일단 memberRole을 찾기 위해 쿼리를 한 번 날렸는데, fetchJoin을 통해 user에서 한번의 쿼리로 수행하기 위해 fetchJoin을 적용시켜 N+1 문제를 방지했다.(왜 애초에 이렇게 냅뒀었는지??)

그리고 모든 로직을 메서드 하나에 몰아서 정리했다.

 

 

전체 로직

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("jwt 필터 동작");

        Optional<String> accessTokenOptional = findCookieToken(request, "accessToken");
        // 쿠키 자체가 없으면 401 에러 발생
        if (!accessTokenOptional.isPresent()) {
            log.error("쿠키 없음.");
            sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "쿠키가 존재하지 않습니다.");
            return;
        }

        // 액세스 토큰 추출
        String accessToken = accessTokenOptional.get();

        // userId 토큰에서 꺼냄.
        try {
            TokenInfo tokenInfo = jwtService.getUserId(accessToken);
            log.info("userId:{}번 유저 토큰 추출 완료", tokenInfo.getUserId());

            // 토큰이 만료됐으면
            if (tokenInfo.isExpired()) {
                // 리프레시 토큰 추출
                Optional<String> refreshTokenOptional = findCookieToken(request, "refreshToken");
                if (!refreshTokenOptional.isPresent()) {
                    log.error("리프레시 토큰 없음");
                    sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "리프레시 토큰이 존재하지 않습니다.");
                    return;
                }

                // 리프레시 토큰 만료 확인
                if (!jwtService.checkRefreshToken(refreshTokenOptional.get())) {
                    log.error("리프레시 토큰 문제 발생");
                    sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "리프레시 토큰 사용 불가(로그아웃 진행)");
                    return; // 여기서 처리 종료
                }

                // 토큰 재생성 및 쿠키 설정
                if (!reGenerateToken(response, tokenInfo.getUserId())) return;

                sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "JWT 재발급 완료");
                return; // 여기서 처리 종료
            }

            if (!authenticateUser(request, response, tokenInfo.getUserId())) return;

            filterChain.doFilter(request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 공통 응답 메시지
    private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message) throws
            IOException {
        response.setStatus(status.value());
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(String.format("{\"error\": \"%s\"}", message));
    }

    // 쿠키에서 토큰 찾기
    private Optional<String> findCookieToken(HttpServletRequest request, String name) {
        Cookie[] cookies = request.getCookies();

        return cookies == null ? Optional.empty() : Arrays.stream(cookies)
                .filter(cookie -> name.equals(cookie.getName()))
                .findFirst()
                .map(Cookie::getValue);
    }

    // 토큰 재생성
    private boolean reGenerateToken(HttpServletResponse response, Long userId) throws IOException {
        // 액세스 토큰을 위한 쿠키 생성
        String accessToken = jwtService.createAccessToken(userId);
        String refreshToken = jwtService.createRefreshToken(userId, true);

        if ("유저 없음".equals(refreshToken)) {
            log.error("유저 없음");
            sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "유저 정보 조회 실패");
            return false;
        }

        setCookie(response, "accessToken", accessToken);
        setCookie(response, "refreshToken", refreshToken);

        return true;
    }

    // 쿠키에 토큰 저장
    private void setCookie(HttpServletResponse response, String name, String value) {
        Cookie cookie = new Cookie(name, value);
        cookie.setSecure(true);
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        response.addCookie(cookie);

        log.info("쿠키 생성 및 설정 완료");
    }

    // 유저 인증
    private boolean authenticateUser(HttpServletRequest request, HttpServletResponse response, Long userId) throws IOException {
        Optional<Member> byIdLogin = memberRepository.findByIdLogin(userId);
        if (byIdLogin.isEmpty()) {
            log.info("유저 데이터 없음");
            sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "유저 데이터 없음");
            return false;
        }

        Member member = byIdLogin.get();
        List<MemberRole> memberRoles = member.getMemberRoles();
        List<SimpleGrantedAuthority> authorities = memberRoles.stream()
                .map(memberRole -> new SimpleGrantedAuthority(memberRole.getRole().name()))
                .collect(Collectors.toList());

        // 유저 인증 객체
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(member.getId(), null, authorities);
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        return true;
    }

이렇게 코드를 정리하고 로직을 변경했는데 가독성 측면에서는 확실히 좋아졌다고 생각이 든다.

 

근데 아직도 이게 잘 정리를 한 건지 모르겠다. 애초에 보기 좋게 만들었으면 됐을 텐데

반응형
저작자표시 (새창열림)

'프로젝트 > RESTAPI 추천 서비스' 카테고리의 다른 글

Navbar를 어떻게 페이지마다 유동적으로 보여줄 수 있을까?  (0) 2024.05.15
코드 리팩토링(1)  (0) 2024.05.04
웹소켓을 사용해서 간단하게 전체 채팅을 구현하자(1)  (0) 2024.04.11
어김없이 또 발생한 N+1 문제(요청 기록 조회 API)  (0) 2024.04.09
  1. findCookieToken
  2. reGenerateToken
  3. setCookie
  4. authenticateUser
'프로젝트/RESTAPI 추천 서비스' 카테고리의 다른 글
  • Navbar를 어떻게 페이지마다 유동적으로 보여줄 수 있을까?
  • 코드 리팩토링(1)
  • 웹소켓을 사용해서 간단하게 전체 채팅을 구현하자(1)
  • 어김없이 또 발생한 N+1 문제(요청 기록 조회 API)
indeep
indeep
백준 - https://www.acmicpc.net/user/esu08259 깃허브 - https://github.com/qkrrlgus114
indeep
불편한 게 싫어
indeep
글쓰기방명록관리자
전체
오늘
어제
  • 분류 전체보기 (180)
    • 알고리즘문제 (11)
      • 백준 (10)
      • swea (1)
    • CS지식 (27)
      • HTTP 웹 지식 (4)
    • 일상 (1)
    • 스프링 강의(인프런) (4)
    • JAVA 강의(인프런) (11)
    • JAVA (8)
    • 오류해결 (31)
    • Vue (5)
    • 싸피 (4)
    • 스프링 개념 (2)
    • git 관련 (1)
    • 면접 (4)
    • 기타 (14)
    • 프로젝트 (45)
      • RESTAPI 추천 서비스 (34)
      • 씈크럼 프로젝트 (11)
    • 독서 (3)
    • 행사 || 컨퍼런스 (4)
    • 회고 (3)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

최근 댓글

hELLO · Designed By 정상우.v4.2.2
indeep
jwt Filter 가독성 리팩토링 진행
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.