목표
리액트 클라이언트에서 스프링부트 인증서버 요청, 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
/>
@
</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: '*'
리액트 프록시
리액트에서 위와 같이 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("입력을 확인해 주세요.")
));
참고
'Front-end > React\RN' 카테고리의 다른 글
리액트 포털 createPortal, HTML 이동 (0) | 2023.01.11 |
---|---|
리액트 커스텀 컴포넌트 (0) | 2023.01.10 |
리액트 Styled Components , CSS 모듈 (0) | 2022.12.20 |
리액트 자식 대 부모 컴포넌트 통신 (0) | 2022.12.08 |
리액트 state 상태 저장, 업데이트 (0) | 2022.12.08 |
댓글