Refresh Token (HttpOnly) - Spring Boot, React 연동

형석이의 성장일기·2024년 5월 16일
1

Spring Security

목록 보기
6/6

ISSUE 1

근데 재발급 로직이 안됨..
React 에서 요청을 보냈을 때, 아예 Controller 의 메서드로 들어가지도 못한다..


일단 Postman 으로 로그인을 했을 때, refresh Token 을 Cookie 로 받음 (이 쿠키는 Http only 상태임)

다시 재발급 받는 요청을 보냈을 때, 정상적으로 새로운 Access Token 이 리턴됨


다시 React 로 돌아와서,

로그인을 시도했을 때, 응답 헤더의 Set-Cookie를 보면 잘 들어오는데..


@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/member")
public class MemberController {

    ...

    @GetMapping("/reissue")
    public ResponseEntity<TokenDto> reissue(@CookieValue("refreshToken") String refreshToken) {
        log.info("재발급 로직 입장");
        log.info("refreshToken 정보 = {}", refreshToken);
        String newAccessToken = authService.reissueAccessToken(refreshToken);
        log.info("AccessToken 재발급 = {}", newAccessToken);
        return ResponseEntity.status(HttpStatus.OK).body(new TokenDto(newAccessToken, ""));
    }

근데 토큰을 재발급받는 메서드에 아예 접근이 안됨


혹시

@CookieValue("refreshToken") String refreshToken

이 부분 때문인가 싶어서

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/member")
public class MemberController {

		...

    @GetMapping("/reissue")
    public ResponseEntity<TokenDto> reissue() {
        log.info("재발급 로직 입장");
        return ResponseEntity.status(HttpStatus.OK).body(new TokenDto(null, ""));
    }

아예 파라미터로 아무것도 받지 않게 코드를 변경함


변경하고 다시 재발급 요청을 보내니까,

요청이 메서드에 정상적으로 접근함..


이걸 보고 든 생각이, React 에서도 Http Only 인 Cookie 지만 따로 설정해줘야 하는 것이 있나..? 라는 생각을 함

→ 맞았다.. 물론 Spring 코드도 좀 변경했음!

// Axios 인스턴스 생성
const interceptor = axios.create({
  baseURL: 'http://localhost:8080/api/v1',
  withCredentials: true,
  headers: {
    'Access-Control-Allow-Origin': 'http://localhost:8080',
  }
});

이건 React 인터셉터 생성 코드인데,

  withCredentials: true,

이 부분을 추가해줘야 백엔드에 요청보낼 때, 토큰도 보낼 수 있다고 한다..


그리고

@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    ...
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);
        http.exceptionHandling(e -> e.authenticationEntryPoint(jwtAuthenticationEntryPoint).accessDeniedHandler(jwtAccessDeniedHandler));

        // 권한 규칙 설정
        http.cors((cors) -> cors
                        .configurationSource(corsConfigurationSource()))
                .logout((logoutConfig) -> {
                    logoutConfig.logoutUrl("/api/v1/member/logout")
                            .addLogoutHandler((request, response, authentication) -> {
                                HttpSession session = request.getSession();
                                session.invalidate();
                            }).deleteCookies("refreshToken");
                    logoutConfig.logoutSuccessHandler((request, response, authentication) -> {
                        response.setStatus(HttpStatus.OK.value());
                        response.getWriter().write("logout");
                        response.getWriter().flush();
                    });
                })
                .authorizeHttpRequests(
                        authorize -> authorize
                                .requestMatchers("/api/v1/member/login").permitAll()
                                .requestMatchers("/api/v1/member/join").permitAll()
                                .requestMatchers("/api/v1/member/reissue").permitAll()
                                .requestMatchers("/api/v1/member/find-id").permitAll()
                                .requestMatchers("/api/v1/member/find-password").permitAll()
                                .anyRequest().authenticated()
                ).apply(new JwtSecurityConfig(jwtTokenProvider));

        return http.build();
    }

    // CORS 허용 적용
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("http://localhost:3000"));
        configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        configuration.addExposedHeader("Set-Cookie"); // Set-Cookie 라는 헤더 노출
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

CORS 설정 코드에 Set-Cookie 라는 헤더를 노출하는 설정을 추가함


다 하고, 다시 요청을 보내니까

쿠키 저장 잘 되고, Http Only 설정도 잘 되고..

백엔드에 요청도 잘 감


React 인터셉터 코드

import axios from 'axios';
import { setCookie, getCookie, removeCookie } from "../util/Cookie";
import { useNavigate } from "react-router-dom";

// Axios 인스턴스 생성
const interceptor = axios.create({
  baseURL: 'http://localhost:8080/api/v1',
  withCredentials: true,
  headers: {
    'Access-Control-Allow-Origin': 'http://localhost:8080',
  }
});

// 요청 인터셉터 추가
interceptor.interceptors.request.use(
  (config) => {
    if (config.url !== '/member/login' && config.url !== '/member/join') {
      const accessToken = getCookie('accessToken');
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => {
    console.log(error);
  }
);

// 응답 인터셉터 추가
interceptor.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    const originRequest = error?.config;
    if (error.response.status === 401) {
      try {
        const response = await interceptor.get('/member/reissue');        
        if (response.data.accessToken) {
            removeCookie('accessToken');
            setCookie('accessToken', response.data.accessToken);
          
            // 업데이트된 액세스 토큰을 사용하여 Authorization 헤더 업데이트
            originRequest.headers["Authorization"] = "Bearer " + response.data.accessToken;
            
            // 재요청
            return interceptor(originRequest);
        }
    } catch (error) {
        return Promise.reject(error);
    }    
    }

  },
);

export default interceptor;
profile
이사중 .. -> https://gudtjr2949.tistory.com/

0개의 댓글

관련 채용 정보