Day6. React <-> SpringBoot: 인증 처리

2ㅣ2ㅣ·2024년 10월 31일

Project

목록 보기
5/13

개요

Spring Boot에서는 /signup, /login인증 정보 없이 접근 가능한 경로WhiteListUri로 설정했다.
Spring Security의 인증이 로직이 적용되도록 React 클라이언트에서 사용자 인증 상태를 확인하는 로직을 구현하려고 한다.



As-Is

  • React 클라이언트에서 인증이 필요한 경로 접근 시 인증 상태를 확인하는 로직이 없다.
  • 인증되지 않은 사용자가 인증이 필요한 경로에 접근 가능한 보안 취약점 존재한다.

간단하게 생각하면 인증이 필요한 경로로 리디렉션 될때마다 인증 검증 API를 호출시키면 되는거 아닌가?

💡 /member/verify 로 인증 검증 API 분리

  1. 모듈화된 인증 처리
  • 인증 로직을 /member/verify API로 별도 분리하여 재사용성을 높이고, 코드 유지보수를 간소화했다.
  • 다양한 컴포넌트에서 일관된 인증 검증이 가능하도록 구현했다.
  1. 일관된 인증 처리
  • 모든 비허용 API 호출에서 일관된 인증 처리가 가능하도록 설계했다.
  • 추가적인 인증 확인 요청이 필요할 때도 /member/verify를 일관된 진입점으로 활용할 수 있다.


To-Be

React 클라이언트

const useAuth = () => {
  // useNavigate를 통한 리다이렉션 처리
  const navigate = useNavigate();

  useEffect(() => {
    const verifyAuth = async () => {
      try {
        const response = await fetch('http://localhost:8080/member/verify', {
          method: 'GET',
          credentials: 'include', // 쿠키 포함
        });

        if (!response.ok) {
          navigate('/');
        }
      } catch (error) {
        console.error('인증 확인 실패:', error);
        navigate('/');
      }
    };

    verifyAuth();
  }, [navigate]);
};
  • 재사용 가능성: React의 커스텀 훅을 활용해 인증 상태 검증 로직을 추상화.
  • 간단한 인증: credentials: 'include'로 쿠키 기반 인증
  • 컴포넌트 로드 시 자동 인증 확인: 마운트 순간 /member/verify 호출로 인증 상태를 즉시 확인하고 검증 결과 반환
    • 검증 실패시: 즉시 홈화면/으로 리다이렉션시킴

Springboot

@RequestMapping("/member")
public class MemberController {
    @GetMapping("/verify")
    public BaseResponse<MemberResponseDto> verifyMember(
            Principal principal
    ){
        String email = principal.getName();
        return memberService.verifyMember(email);
    }
}
  • 인증 검증 : Principal 객체로 인증된 사용자 정보 확인 후 상태를 반환함

React - Springboot 간 /member/verify 경로 인증 검증 흐름

  1. 사용자가 로그인 후 보호된 경로(Whitelist 이외의 경로 - ex. dashboard)로 접근.
  2. React의 useAuth 훅이 /member/verify API 호출을 통해 인증 상태 확인.
  3. Spring Security는 SecurityContext를 통해 인증 객체를 확인하고, 검증 결과를 반환.
  4. React는 검증 실패 시 즉시 홈 화면(/)으로 리다이렉션.



결과 확인


로그인 성공 후, dashboard(whiteListUri로 지정하지 않은 경로)로 리디렉션 되는 즉시 /member/verify API가 호출되는 것을 확인할 수 있다.



최선이었을까?

/member/verify API로 인증 검증 로직을 분리한 이유는 단순하고 직관적인 기능 구현신속한 개발이 주요 목적이었다. 마감기한을 준수하면서도 동작하는 코드를 작성하기 위한, 당시에는 최선의 선택이었다.

하지만, 조금 더
sexy
할 순 없을까?

클라이언트와 서버간 인증 검증 로직 플로우를 추가적으로 학습하다가 다음과 같은 방법론들을 알게 되었다. 추후 배포까지 완료된 후, 리팩토링 시기에 적용해보려고 한다.

대안1. SpringAOP를 활용한 인가 처리

AOP(Aspect-Oriented Programming)를 통해 인증 및 인가 로직을 메서드 호출 전에 자동으로 수행하면 검증 API 호출이 필요 없어지고, 인증 로직이 컨트롤러에 노출되지 않는다.

  • 메서드 실행 전, 인증 상태를 검증하는 @Before 로직을 삽입.
  • 인증 검증 로직이 모든 API에 일괄적으로 적용됨.
  • 컨트롤러와 인증 로직을 분리해 관심사를 명확히 함.
@Aspect
@Component
public class AuthorizationAspect {
    @Before("@annotation(com.diary.musicinmydiaryspring.common.annotation.Authorized)")
    public void authorizeUser(JoinPoint joinPoint) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new CustomRuntimeException(BaseResponseStatus.UNAUTHORIZED);
        }
    }
}

대안2. Filter 또는 Interceptor 활용

Filter나 Interceptor는 HTTP 요청 단계에서 전역적인 인증 검증을 처리할 수 있다. 이를 통해, 클라이언트가 /member/verify를 호출하지 않아도 인증 상태를 확인할 수 있다.

  • 요청이 서버에 도달하기 전에 Jwt를 검증.
  • (Filter의 경우) Spring Security의 OncePerRequestFilter를 확장해 구현.
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
        if (isWhiteListed(request.getRequestURI())) {
            filterChain.doFilter(request, response);
            return;
        }

        String jwtToken = getCookieValueFromToken(request, "accessToken");
        if (jwtToken != null && jwtProvider.validateToken(jwtToken)) {
            Authentication authentication = jwtProvider.getAuthentication(jwtToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            String email = jwtProvider.getClaims(jwtToken).getSubject();
            request.setAttribute("email", email);
        }
        filterChain.doFilter(request, response);
    }

    private String getCookieValueFromToken(HttpServletRequest request, String cookieName) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookieName.equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

0개의 댓글