인프런 커뮤니티 질문&답변

모코코님의 프로필 이미지
모코코

작성한 질문수

Spring Security 와 JWT 난관에 봉착했습니다...

작성

·

697

·

수정됨

0

안녕하세요.

이제 개발 공부를 시작한 학생입니다.

이번에 Security 와 Jwt 에 대해서 배웠는데, 실습 간 문제가 발생하여 부끄럽지만,,, 질문드립니다.

문제의 핵심은 "토큰을 헤더에 담아서 요청했는데, 왜 토큰 값이 전달되지 않는가 ?" 입니다...

 

먼저 코드 보여드리겠습니다.

 

WebSecurityConfig

 @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/").permitAll() // 메인 페이지 요청 허가
                        .requestMatchers("/user/**").permitAll() // '/user/'로 시작하는 요청 모두 접근 허가
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        http.formLogin((formLogin) ->
                formLogin
                        .loginPage("/user/login-page").permitAll()
                        .defaultSuccessUrl("/test", true) // 로그인 성공 시 /test 경로로 리다이렉트
        );

        // 필터 관리
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

우선 config 쪽에서는 리소스 및 기본페이지, user 로 시작하는 페이지는 인증 없이 접근 가능하도록 했습니다.

.anyRequest().authenticated() <- 이걸 설정하면 나머지 요청들은 인증이 필요하다는 것으로 알고 있습니다.

 

JwtAuthenticationFilter

인증 필터는 다음과 같습니다.

   protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
        log.info("로그인 성공 및 JWT 생성");
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(new ObjectMapper().writeValueAsString(new ApiResponseDto("로그인 성공", HttpStatus.OK.value())));

        String loginId = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        String accessToken = jwtUtil.createAccessToken(loginId, role);
        String refreshToken = jwtUtil.createRefreshToken(loginId);

        redisService.saveRefreshToken(loginId, refreshToken);

        // Access Token 헤더에 저장
        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, accessToken);

        // Refresh Token 쿠키에 저장
        Cookie refreshCookie = new Cookie("refreshToken", refreshToken);
        refreshCookie.setHttpOnly(true); 
        refreshCookie.setSecure(true); 
        refreshCookie.setPath("/"); 
        response.addCookie(refreshCookie);
    }

Access 토큰과 Refresh 토큰을 구현해보고 싶어서 위와 같이 구현하였습니다.

 

login.html

<h1>로그인</h1>
<form id="loginForm">
    <label for="loginId">아이디:</label>
    <input type="text" id="loginId" name="loginId" required>
    <br>
    <label for="password">비밀번호:</label>
    <input type="password" id="password" name="password" required>
    <br>
    <button type="button" id="loginButton">로그인</button>
</form>
<script>
    $(document).ready(function() {
        $('#loginButton').on('click', function() {
            const loginId = $('#loginId').val();
            const password = $('#password').val();

            $.ajax({
                type: "POST",
                url: "/user/login",
                contentType: "application/json",
                data: JSON.stringify({ loginId: loginId, password: password }),
                success: function(data, textStatus, xhr) {
                    // 응답 헤더에서 Access Token 추출
                    const accessToken = xhr.getResponseHeader('Authorization');
                    if (accessToken) {
                        // 로컬 스토리지에 Access Token 저장
                        localStorage.setItem('Authorization', accessToken);
                        // 모든 AJAX 요청에 대해 Authorization 헤더를 설정
                        setupAjaxRequests(accessToken);
                        // 로그인 성공 후 리다이렉션
                        window.location.href = '/test';
                    } else {
                        alert('Authorization token not found');
                    }
                },
                error: function(jqXHR, textStatus, errorThrown) {
                    alert('로그인 실패: ' + textStatus);
                }
            });
        });
    });

    function setupAjaxRequests(token) {
        $.ajaxSetup({
            headers: { 'Authorization': token }
        });
    }
</script>
</body>
</html>

 

그래서 실제로 위와 같은 페이지를 만들어서 로그인을 시도해보면 로컬스토리지와 쿠키에 각각 토큰이 담기는 것을 확인할 수 있었습니다.

그런데 여기서 문제가 있는 게, window.location.href = '/test'; 아것이 동작이 안된다는 것입니다...

분명 '/test'; 라고 명시해두면

 

TestController

@Controller
public class TestController {
    @GetMapping("/test")
    public String testPage() {
        return "test";
    }
}

 

해당 api 가 호출되어 test 라는 페이지가 떠야하는 게 아닌가요 ? ㅠㅠㅠㅠ

test page 는 우선 body 에 "테스트 페이지입니다" 정도로만 제작해두었습니다.

 

왜 안될까,,, 고민하다가 우선 인가 필터 쪽 로그를 찍어서 확인했습니다.

 

JwtAuthorizationFilter

  @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
        // 헤더에서 토큰 추출
        log.info("헤더에서 토큰 추출");
        String tokenValue = jwtUtil.getJwtFromHeader(req);

        log.info("토큰 : " + tokenValue);
        if (StringUtils.hasText(tokenValue)) {

            // 토큰 유효성 검사
            if (!jwtUtil.validateToken(tokenValue)) {
                log.info("Token Error");

                return;
            }
            Claims info = jwtUtil.getUserInfoFromToken(tokenValue);

            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }

        else {
            log.info("토큰이 없습니다.");
        }

        filterChain.doFilter(req, res);
    }

이와 같으며, 로그인 성공 후 window.location.href = '/test'; 동작할때와 그냥 주소창에 localhost8080/test 로 접근해 본 결과

 

 

로그가 이렇게 찍히는 겁니다...

2024-02-25T22:49:52.276+09:00  INFO 54640 --- [nio-8081-exec-2] JWT 검증 및 인가                              : 헤더에서 토큰 추출
2024-02-25T22:49:52.276+09:00  INFO 54640 --- [nio-8081-exec-2] JWT 검증 및 인가                              : 토큰 : null
2024-02-25T22:49:52.276+09:00  INFO 54640 --- [nio-8081-exec-2] JWT 검증 및 인가                              : 토큰이 없습니다.

대체 왜 토큰이 없는 걸까....

분명 로컬 스토리지에 Access Token 저장된 거 개발자 도구로 확인했고, 이를 다시 Header 담아서 전송하는 것까지 login.html 쪽에 구현했다고 생각했는데,,, 이게 의도한 방향대로 흘러가질 않습니다.

 

대체 왜 토큰이 비어있으며, test 라는 페이지에 접속하지 못하는 걸까요...

제가 놓친 부분이 있을까요 ?

 

답변 1

0

안녕하세요? 도움이 될 지 모르겠지만 답변 남겨봅니다.

먼저 WebSecurityConfig 클래스에서 anyRequests().authenticated()

가 어디까지 제한을 거는 지 알고 가면 좋을 듯 합니다.

저는 저 제한이 어디까지 적용되는지 몰라서 저라면 requestMatcher(new AntPathRequestMatcher("/test/**")).authenticated() 처럼 인증이 필요한 부분에 인증이 되도록 걸어둘 것 같고 그러면 코드 의미가 더 명료해질 것 같습니다.

그리고 토큰이 콘솔 부분으로 잘 찍힌다면 발급이 잘 된 것으로 생각됩니다. ㅎㅎ..

모코코님의 프로필 이미지
모코코

작성한 질문수

질문하기