본문 바로가기
Front-end/React\RN

리액트 스프링부트 연동 API Gateway , CORS 처리

by javapp 자바앱 2022. 12. 23.
728x90

 

 

 

목표

리액트 클라이언트에서 스프링부트 인증서버 요청, API Gateway 서버 통과하여 인증서버 요청


 

리액트

import React, { useState } from 'react';
import {useNavigate} from 'react-router-dom'
import Logo from '../Header/Logo';
import './Signup.css'
import RightLayoutLinks from "../Header/HeaderLinks/RightLayoutLinks";

const authServer = "http://192.168.45.1:8100";

const SignupView = props => {
  const options = [
    { value: "type", text: "직접 입력" },
    { value: "naver.com", text: "naver.com" },
    { value: "google.com", text: "google.com" },
    { value: "hanmail.net", text: "hanmail.net" },
    { value: "nate.com", text: "nate.com" },
    { value: "daum.net", text: "daum.net" },
  ];

  const [signupRequestBody, setSignupRequestBody] = useState({
    username: "",
    email: "",
    password: "",
  });

  const [passCheckText, setPassCheckText] = useState("비밀번호 일치하지 않음");

  const [domainSelected, setDomainSelected] = useState(options[0].value);

  const [domain, setDomain] = useState("");

  const [passCheck, setPassCheck] = useState("");

  const [isPassCheck, setIsPassCheck] = useState(false);

  const navigate = useNavigate();

  const onSignupInputChangeHandler = (event) => {
    setSignupRequestBody({
      ...signupRequestBody,
      [event.target.name]: event.target.value,
    });
    verifyEqualPasswordCheckAnd(event.target.value);
  };

  const onSelectDomainHandler = (event) => {
    // 옵션 도메인 선택시
    if (event.target.value !== "type") {
      setDomain(event.target.value);
    } else {
      setDomain("");
    }
    setDomainSelected(event.target.value);
  };

  const onInputDomainHandler = (event) => {
    if (domainSelected === "type") {
      setDomain(event.target.value);
    }
  };

  const onInputPassCheckChangeHandler = (event) => {
    verifyEqualPasswordAnd(event.target.value);
    setPassCheck(event.target.value);
  };

  const verifyEqualPasswordAnd = (passcheck) => {
    if (passcheck === signupRequestBody.password) {
      setPassCheckText("비밀번호 일치");
      setIsPassCheck(true);
    } else {
      setPassCheckText("비밀번호 일치하지 않음");
      setIsPassCheck(false);
    }
  };

  const verifyEqualPasswordCheckAnd = (password) => {
    if (password === passCheck) {
      setPassCheckText("비밀번호 일치");
      setIsPassCheck(true);
    } else {
      setPassCheckText("비밀번호 일치하지 않음");
      setIsPassCheck(false);
    }
  };

  // main page 이동
  const goToMain = () => {
    alert('회원가입 성공!')
    navigate("/login");
  };


  // 회원가입 과정
  const onSubmitHandler = (event) => {
    event.preventDefault();
    signupRequestBody.email = signupRequestBody.email + "@" + domain;
    console.log(JSON.stringify(signupRequestBody));

    fetch(`${authServer}/api/v1/sign/signup`, {
      method: "POST",
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(signupRequestBody),
    })
      .then((response) => {
        return response.json();
      })
      .then((result) => (
        result.userId ? goToMain() : alert("입력을 확인해 주세요.")
      ));

  };

  return (
    <div className="signup_main">
      <RightLayoutLinks />
      <Logo />
      <div className="signup_div">
        <div className="signup_text">
          <h3>회원가입</h3>
        </div>
        <div className="signup_form">
          <form onSubmit={onSubmitHandler}>
            <div>
              <p>
                <label htmlFor="username">이름</label>
              </p>
              <input
                onChange={onSignupInputChangeHandler}
                type="text"
                id="username"
                name="username"
                required
              />
            </div>

            <p>
              <label htmlFor="email">이메일</label>
            </p>
            <div className="email_div">
              <div className="email1">
                <input
                  onChange={onSignupInputChangeHandler}
                  type="text"
                  id="email"
                  name="email"
                  required
                />
                &nbsp;@ &nbsp;
              </div>

              <div className="email2">
                <input
                  type="text"
                  id="domain"
                  name="domain"
                  value={domain}
                  onChange={onInputDomainHandler}
                  required
                />
                <select
                  value={domainSelected}
                  className="box"
                  id="domain-list"
                  onChange={onSelectDomainHandler}
                >
                  {options.map((option) => (
                    <option key={option.value} value={option.value}>
                      {option.text}
                    </option>
                  ))}
                </select>
              </div>
            </div>

            <div>
              <p>
                <label htmlFor="password">비밀번호</label>
              </p>
              <input
                onChange={onSignupInputChangeHandler}
                type="password"
                name="password"
                id="password"
                required
              />

              <div className="pass_chk_div">
                <p>
                  <label htmlFor="password_chk">비밀번호 재입력</label>
                </p>
                <input
                  type="password"
                  id="password_chk"
                  onChange={onInputPassCheckChangeHandler}
                  required
                />
              </div>
              <div
                className={
                  isPassCheck
                    ? "password_check_text invalid"
                    : "password_check_text"
                }
              >
                <p>{passCheckText}</p>
              </div>
            </div>

            <input type="submit" id="btn" value="회원가입" />
          </form>
        </div>
      </div>
    </div>
  );
};

export default SignupView;

 

port: 8765 (API Gateway)

port: 8100 (auth server)

 

  • 우선 인증서버와 연동 시도
  • const authServer = "http://192.168.45.1:8100";

CORS (Cross-Origin Resource Sharing)  발생

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

 


 

스프링부트

@CrossOrigin 애노테이션을 통해 CORS 를 설정할 수 있음.

  • Controller method CORS configuration
@CrossOrigin("*")
@CrossOrigin(origins = "http://localhost:3000", maxAge = 3600)
@RestController
@RequestMapping("/user")
public class AccountController {

 

 

 

글로벌 CORS 설정을 통해 CORS를 활성화할 수 있음.

스프링 인증서버에서는 시큐리티를 사용하고 있어서

SecurityConfig 에 WebMvcConfigurer 를 빈에 추가

    @Bean
    public WebMvcConfigurer corsConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("http://localhost:3000")
                        .allowedMethods("POST","GET","PUT","DELETE","HEAD","OPTIONS")
                        .allowCredentials(true);
            }
        };
    }

.addMapping : 적용할 URL 패턴 ("/**") , ("/user/**")

.allowedOrigins("{url}") : 허가 출처 "*" 를 해줘도 좋지만 쿠키나 JWT 을 담아 보낼 경우 특정해주는 것이 좋다고함

.allowedMethods : 허용할 HTTP method 지정

.allowCredentials : 요청이 자격증명 모드가 Include

    allowedOrigins에는 "*"사용 불가, 명시적인 URL이어야 함.

   응답 헤더에는 반드시 allowCredentials 가 true 여야 함.

 

 

 

필터를 통해 CORS 설정

@Configuration
public class MyConfiguration {

	@Bean
	public FilterRegistrationBean corsFilter() {
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		CorsConfiguration config = new CorsConfiguration();
		config.setAllowCredentials(true);
		config.addAllowedOrigin("http://localhost:3000");
		config.addAllowedHeader("*");
		config.addAllowedMethod("*");
		source.registerCorsConfiguration("/**", config);
		FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
		bean.setOrder(0);
		return bean;
	}
}

 


 

다음은 API Gateway CORS 설정

요청 url : http://192.168.45.1:8765/auth/api/v1/sign/signup

applicaiton.yml 설정 파일

server:
  port: 8765

spring:
  application:
    name: api-gateway

  cloud:
    gateway:
      # CORS
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "http://localhost:3000"
            allow-credentials: true
            allowedMethods:
              - PUT
              - GET
              - POST
              - DELETE
              - OPTIONS
            allowedHeaders: '*'

여기까지 설정해도 api gateway에서 CORS 에러가 발생하는데

 

 

Response Headers를 살펴보면

본 요청에서 뒷단 서버에 갔다오면 Origin 헤더를 또 보내어 두개가 되버림

그래서 Gateway 에서 헤더 중복을 제거해줘야 함.

 

DedupeResponseHeader GatewayFilter Factory 를 통해

응답 해더의 중복값을 제거

  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      # CORS
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "http://localhost:3000"
            allow-credentials: true
            allowedMethods:
              - PUT
              - GET
              - POST
              - DELETE
              - OPTIONS
            allowedHeaders: '*'

 

해결

 

 


 

 

리액트 프록시

const authServer = "http://192.168.45.10:8765/auth";

리액트에서 위와 같이 url 코드를 하드코딩 하고 있었다.

이러면 화면마다 url 변수를 둬야 되고 만약 주소가 바뀌면 일일이 추적해야 되기 때문에 유지보수하기가 힘들다.

그래서 프록시를 둬서 설정된 경로("/auth") 로 요청시 프록시를 통해 서버 요청 주소를 변경해 보내줌

 

/auth/... 이면

http://localhost:3000/auth/..  --> http://192.168.45.10:8765/auth/...

 

 

방법

  • npm install http-proxy-middleware
  • src/setupProxy.js 경로로 파일생성
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = (app) => {
    app.use(
        createProxyMiddleware("/auth", {
            target: "http://192.168.45.10:8765",
            changeOrigin: true,
        })
    );
}

 

fetch POST 요청

    fetch('/auth/api/v1/sign/signup', {
      method: "POST",
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(signupRequestBody),
    })
      .then((response) => {
        return response.json();
      })
      .then((result) => (
        result.userId ? goToMain() : alert("입력을 확인해 주세요.")
      ));

 

 

 

 

 

 

 

 

 

 

 

 

참고

Web on Servlet Stack (spring.io)

Spring Cloud Gateway CORS 문제 해결하기 (velog.io)

댓글