본문 바로가기

3. JWT + oAuth2 카카오 로그인 삽질일기

by 흐리멍텅 2024. 6. 29.

3. WhiteList 관련 삽질

코드까지 전부 기록해 놓은 내용은 노션에서 확인할 수 있다.

해당 코드를 사용한 프로젝트는 깃허브에서 확인할 수 있다.

 

WhiteList << 별거 아닌 줄 알았는데 마지막까지 내 발목잡은 놈

시큐리티 특성(?)상 에러 원인을 찾기가 참 힘든 것 같다.
(공부 안하고 바로 도입했으니 시큐리티 특성이 아니라 내 특성인가)

타 API를 다루듯이 에러의 위치를 "정확히" 찝어내는데 까지 굉장히 번거롭고

놀랍게도 시큐리티 처음부터, 마지막까지, 정말 꾸준하게 애먹인놈은 저놈이다 저놈.

 

이 글에는 whitelist가 어떻게 다채롭게 나를 엿먹였는지 나열하며 정리해보겠다.


1. 카카오 로그인 성공...? 정말로?

 

맨 처음 카카오 로그인 api를 연결했었을 때 "localhost:8080/api/login" 경로로 진입했었고 카카오 로그인 페이지가 나와서 성공한 줄 알았다.

"localhost:8080/" 루트 경로로 진입했었을 때도 카카오 로그인 페이지가 나오기 전까진.

 

debug level에서 security 부분을 설정하고 로그를 봤을 때 [ "/" -> "/error" -> 카카오로그인 ] 순서로 카카오 로그인이 동작하고 있었다.

 

원인은 이러했다

 

"api/**" 는 물론이거니와 "/" 루트경로 또한 화이트리스트에 들어있었으나, 이 경로에 맞는 html 정적파일이 존재하지 않았다. 일반적으로 찾아보고 따라한 블로그는 대체로 백엔드/프론트엔드 나눠서 하는 프로젝트 형의 게시글이 아닌 "카카오 로그인 사용해보기" 등의 게시글이었고, 스프링 MVC를 따르는 그 게시글들은 html파일이 당연히 존재했으나 나는 존재하지 않았다.

 

따라서 "/" 루트경로에 대한 접근을 테스트 했을때, index.html이 없기때문에 404가 리턴됐을 것이고, 404 에러페이지는 화이트리스트에 존재하지 않기 때문에 로그인이 필요하다는 시큐리티 에러페이지로 리다이렉트 됐을 것이고, "/error"경로 또한 당연히 화이트리스트에 존재하지 않기 때문에 카카오 로그인페이지로 리다이렉트됐고, 결과적으로 나에게는 카카오 로그인 페이지가 보여졌다. 대충 1~2초 뒤에

 

결과적으로 원인을 분석하고 나니 해결할 필요는 없었다. 나는 api만 제공하면 되고, html 파일이 없어서 생긴 문제는 상관없기때문

 

다 된줄알았다.

 

2. Whitelist는 "두 번" 써야하더라.

 

로그인 할 때는 잘 만 나오던 "내 유저정보"가 타 api에서 "현재 로그인된 유저 (이 글에서 getCurrentUser 메소드)" 정보만 가져왔다 하면 전부 Anonymous가 나왔다. 로그를 수십개를 찍어보고 나서 찾은 원인은 별게 아니었다.

// SecurityConfig.java의 SecurityFilterChain 일부

// HTTP 요청에 대한 보안 규칙을 설정
.authorizeHttpRequests(auth -> auth
	.requestMatchers(JwtConstants.WHITELIST).permitAll()  // 지정된 경로들은 인증 없이 접근 허용
	.requestMatchers("/admin/**").hasRole("ADMIN")  // "/admin/**" 경로는 'ADMIN' 역할을 가진 사용자만 접근 가능
	.anyRequest().authenticated())  // 그 외 모든 요청은 인증을 요구
public class JwtConstants {
    public static final String key = "DG3K2NG9lK3T2FLfnO283HO1NFLAy9FGJ23UM9Rv923YRV923HT";
    public static final int ACCESS_EXP_TIME = 10;   // 10분
    public static final int REFRESH_EXP_TIME = 60 * 24;   // 24시간

    public static final String JWT_HEADER = "Authorization";
    public static final String JWT_TYPE = "Bearer ";
    public static final String[] WHITELIST = {
            "/api/comments",
            "/api/comments/**",
            "/api/images",
            "/api/images/**",
            "/api/posts",
            "/api/posts/**",
            "/api/reviews",
            "/api/reviews/**",
            "/api/auth",
            "/api/auth/**",
            "/api/places",
            "/api/places/**",
            "/signUp",
            "/login",
            "/refresh",
            "/",
            "/index.html",
            "/swagger-ui.html",
            "/api-docs/**",
            "/v3/api-docs/**",
            "/swagger-ui/**",
            "/webjars/**"
    };
}

내가 화이트리스트를 사용했던 방식은 위와 같다. 나 뿐만 아니라 다들 비슷할 것이다.

하지만 이곳에만 넣어서는 "절대" 안된다.

// JwtVerifyFilter 클래스 에서 shouldNotFilter 메소드 내용만 추출

@Component
public class JwtVerifyFilter extends OncePerRequestFilter {

    // 필터를 거치지 않을 URL 및 메서드를 설정하고, true 를 return 하면 현재 필터를 건너뛰고 다음 필터로 이동
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String requestURI = request.getRequestURI();
        String httpMethod = request.getMethod();

        // 특정 경로에 대해 GET 요청과 POST, PUT, DELETE 요청을 모두 필터링하도록 설정
        if (PatternMatchUtils.simpleMatch("/api/users/**", requestURI)) {
            return false;
        }

        // 인증 code 로직은 필터링 하지 않음
        if (PatternMatchUtils.simpleMatch("/api/auth/**", requestURI)) {
            return true;
        }

        // POST, PUT, DELETE 요청이 아닌 경우 = GET 요청인 경우
        if (!httpMethod.equalsIgnoreCase("POST") && !httpMethod.equalsIgnoreCase("PUT") && !httpMethod.equalsIgnoreCase("DELETE")) {
            for (String pattern : JwtConstants.WHITELIST) {
                if (PatternMatchUtils.simpleMatch(pattern, requestURI)) {
                    return true; // 화이트리스트에 포함된 경우 필터를 거치지 않음 : Get & 화이트리스트
                }
            }
            return false; // 화이트리스트에 포함되지 않은 경우 필터를 거침 : Get이지만 화이트리스트 아님
        }

        // POST, PUT, DELETE 요청인 경우
        for (String pattern : JwtConstants.WHITELIST) {
            if (PatternMatchUtils.simpleMatch(pattern, requestURI)) {
                return false; // 화이트리스트에 포함되더라도 필터를 거침
            }
        }

        return false; // POST, PUT, DELETE 요청이면서 화이트리스트에 포함되지 않은 경우
    }

}

여기도 넣어줘야 한다.

jwtVerifyFilter는 에서 말하는 "필터링"은 이전 게시글에서 작성했듯이 JWT에서 필요한 값을 추출해 SecurityContextHolder에 보관하는 정말 중요한 역할을 담당한다.

 

뿐만 아니라 단 하나의 화이트리스트로 모든 인증 여부를 다 다룰 수 없다.

이곳에서 HttpMethod까지 구분해가며 디테일하게 케이스를 나눠줘야 했다.

 

3. "api/**" 는 "api" 라는 경로를 포함하지 않는다.

 

하위경로만 나타낸다고 하더라. 몰랐다. 미리 알았으면 참 좋았을 것을

 

써놓고 보니까 별거 아니기도 하고 한심하기도 한데 당시에는 하루안에 해결한 문제가 하나도 없다.

시큐리티도 "공부하고" 써먹자 "구글링"만 해서 써먹지 말고...