Spring์์ CORS ํด๊ฒฐํ๋ ๋ฒ
์ด๋ฒ ๊ธ์์๋ ๊ฐ์ธ์ ์ธ ํ๋ก์ ํธ๋ฅผ ํ๋ฉด์ ๊ฒช์๋ SOP ๋ฌธ์ ๋ฅผ CORS๋ฅผ ํ์ฉํด์ฃผ๋ฉด์ ํด๊ฒฐํ๋ ๊ณผ์ ์ ๋ํด์ ๊ณต์ ํด๋ณด๋ ค ํฉ๋๋ค. (์ด ๊ธ์์๋ CORS๊ฐ ๋ฌด์์ธ์ง์ ๋ํด์๋ ์์ธํ ๋ค๋ฃจ์ง ์๊ฒ ์ต๋๋ค.)
์ฐธ๊ณ ๋ก ํ๋ก์ ํธ์์ ๋ฐฑ์๋๋ Spring Boot, ํ๋ก ํธ์๋๋ React๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
CORS๋ ๋ฌด์์ผ๊น?
CORS๊ฐ ๋ฌด์์ธ์ง ๊ฐ๋จํ๊ฒ ์์๋ณด๊ฒ ์ต๋๋ค. CORS(Cross-Origin Resource Sharing)๋ ๊ต์ฐจ ์ถ์ฒ ๋ฆฌ์์ค ๊ณต์ ๋ผ๊ณ ํฉ๋๋ค. ์ฌ๊ธฐ์ ๊ต์ฐจ ์ถ์ฒ๋ผ๊ณ ํ๋ ๊ฒ์ ๋ค๋ฅธ ์ถ์ฒ๋ฅผ ์๋ฏธํ๋ ๊ฒ์ ๋๋ค. ์ฆ, ๋ธ๋ผ์ฐ์ ์์ ๋ง๊ณ ์๊ธฐ ๋๋ฌธ์ CORS๋ฅผ ํ์ฉํด์ฃผ์ด์ผ ์ ๊ทผ์ด ๊ฐ๋ฅํฉ๋๋ค.
์ถ์ฒ(Origin)์ ๋ฌด์์ผ๊น?
์์ ๋ณด์ด๋ ๊ฒ์ฒ๋ผ ๋๋ฉ์ธ์์ Protocol + Host + Port๊ฐ ๊ฐ์ผ๋ฉด ๋์ผํ ์ถ์ฒ๋ผ๊ณ ์๊ธฐ๋ฅผ ํฉ๋๋ค. ์ฆ, 3๊ฐ ์ค์ ํ๋๋ผ๋ ๋ค๋ฅด๋ฉด ๋ค๋ฅธ ์ถ์ฒ๋ผ๊ณ ํ ์ ์์ต๋๋ค.
https://gyunny.io/test | ๊ฐ์ ์ถ์ฒ | Protocol, Host, Port ๋์ผ |
https://gyunny.io/test?q=work | ๊ฐ์ ์ถ์ฒ | Protocol, Host, Port ๋์ผ |
http://gyunny.io/test | ๋ค๋ฅธ ์ถ์ฒ | Protocol ๋ค๋ฆ |
http://gyunny.com/test | ๋ค๋ฅธ ์ถ์ฒ | Host ๋ค๋ฆ |
React์์ Spring์ ํธ์ถํ๋ค๋ฉด?
- React: http://localhost:3000
- Spring: http://localhost:8080
React, Spring์ ๊ฐ๊ฐ ๋ก์ปฌ์์ ์คํํ๋ฉด 3000, 8080 ํฌํธ๋ก ์คํํ๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ฌ๋ฉด React์์ Spring API๋ฅผ ํธ์ถํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? ์์์ ๋ณด์๋ฏ์ด ๋ ๋๋ฉ์ธ์ Port๊ฐ ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ SOP ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ผ ์์ธกํ ์ ์์ต๋๋ค.
React์์ Spring API ํธ์ถํด๋ณด๊ธฐ
๋๋ต์ ์ธ ๊ทธ๋ฆผ์ ๊ทธ๋ฆฌ๋ฉด ์์ ๊ฐ์ด SOP๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ ๋๋ค. ์ ๋ง SOP ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋์ง ํ ์คํธ ํด๋ณด๋ฉด์ ์์๋ณด๊ฒ ์ต๋๋ค.
const PostList = () => {
useEffect(() => {
(async () => {
try {
const { data } = await axios.get('http://localhost:8080/api/v1/post');
setDataList(data.data);
} catch (error) {
alert(error);
}
})();
}, []);
}
๊ฐ๋จํ๊ฒ React์์ Spring API๋ฅผ ํธ์ถํ๋ ์ฝ๋์ ๋๋ค. ์ด ์ํ๋ก ๋ธ๋ผ์ฐ์ ๋ฅผ ์คํํด์ ํ์ธํด๋ณด๊ฒ ์ต๋๋ค.
๊ทธ๋ฌ๋ฉด ์์ ๊ฐ์ด ์์ํ๋ ๊ฒ์ฒ๋ผ SOP ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค. SOP๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ Spring Server์์ ์ถ์ฒ๊ฐ ๋ค๋ฅธ React ์์์ด Spring Server ์์์ ์ ๊ทผํ ์ ์๋๋ก ๊ถํ์ ์ฃผ๋ ์์ ์ด ํ์ํฉ๋๋ค.(์ฆ, CORS ์์ )
Spring์์ CORS๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ๋ํ์ ์ผ๋ก 3๊ฐ์ง๊ฐ ์์ง๋ง ์ ๋ ๊ทธ ์ค์ WebMvcConfigurer ์ธํฐํ์ด์ค๋ฅผ ์ด์ฉํด์ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ์ ์ฉํด๋ณด๊ฒ ์ต๋๋ค.
WebMvcConfigurer addCorsMapiings ๊ตฌํํ๊ธฐ
@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final LoginUserIdArgumentResolver loginUserIdArgumentResolver;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("OPTIONS", "GET", "POST", "PUT", "DELETE");
}
}
WebMvcConfigurer ์ธํฐํ์ด์ค๊ฐ ๊ฐ์ง๊ณ ์๋ addCorsMappings ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉ ํ ํ์ ์์ ๊ฐ์ด http://localhost:3000์ ๋ํด์ ์ ๊ทผํ ์ ์๋ ๊ถํ์ ์ฃผ๋ฉด ๋ฉ๋๋ค. ์์ ๊ฐ์ด ์ค์ ๋ง ํด์ฃผ๋ฉด ๋๋ฌด๋๋ ์ฝ๊ฒ SOP๋ฅผ ๋ฐ๋ก ํด๊ฒฐํ ์ ์์ต๋๋ค.
ํ์ง๋ง.. ์์ ๊ฐ์ด ์ค์ ์ ํด์ฃผ์์ด๋ SOP ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋์ง ์์์ต๋๋ค. ์ด ๋ ์ SOP ๋ฌธ์ ๊ฐ ์ฌ๋ผ์ง์ง ์๋๊ฑฐ์ง? ์๋ฒ์์๋ ๋ถ๋ช ์ ๊ทผ ๊ถํ์ ์ฃผ์๋๋ฐ... ๋ผ๋ ์๊ฐ์ด ๋จธ๋ฆฌ์์ ์ง๋ฐฐํ๋ฉด์ ์ ์๋๋์ง ์์ธ์ ์ฐพ๊ธฐ๊ฐ ์ฝ์ง ์์์ต๋๋ค. ๊ฒฐ๊ตญ ๋ง์ ์ฝ์ง๊ณผ CORS ๋์ ์๋ฆฌ๋ฅผ ๋ค์ ๋ณด๋ฉด์ CORS ์ด์๊ฐ ์ฌ๋ผ์ง์ง ์๋ ์์ธ์ ์ฐพ์๋๋ฐ์. ์์ธ์ ๋ฐ๋ก ์ ๊ฐ Spring Security๋ฅผ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์ด์์ต๋๋ค.
์ํ๋ฆฌํฐ์ ๊ฐ๋จํ ๊ทธ๋ฆผ์ ๋ณด๋ฉด ์์ ๊ฐ์ ๊ตฌ์กฐ๋ก ๋์ด ์์ต๋๋ค. ์ฆ, Interceptor, Controller ์์ญ์ ๋ค์ด์ค๊ธฐ ์ ์ Filter ์์ญ์ ๋จผ์ ๊ฑฐ์น๊ฒ ๋๋ค๋ ๊ฒ์ ์ ์ ์์ต๋๋ค. ์ฆ, Filter์์ ๊ฑธ๋ฆฌ๊ธฐ ๋๋ฌธ์ CORS๊ฐ ํด๊ฒฐ๋์ง ์๋ ๊ฒ์ธ๋ฐ์. ์ Filter์์ ๊ฑธ๋ ค์ CORS๊ฐ ํด๊ฒฐ ๋์ง ์์๋์ง๋ฅผ ์ดํดํ๋ ค๋ฉด Preflight Request์ ๋ํด์ ์์์ผ ํฉ๋๋ค.
Preflight Request
Preflight Request๋ ๋จผ์ OPTIONS ๋ฉ์๋๋ฅผ ํตํด ๋ค๋ฅธ ๋๋ฉ์ธ์ ๋ฆฌ์์ค๋ก HTTP ์์ฒญ์ ๋ณด๋ด ์ค์ ์์ฒญ์ด ์ ์กํ๊ธฐ์ ์์ ํ์ง ํ์ธํฉ๋๋ค. ์ฆ, Preflight Request ์์ฒญ์ ์๋ต์ด 200์ผ๋ก ๋จ์ด์ ธ์ผ ๋ค์ ๋ณธ ์์ฒญ์ ์งํํ ์ ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ Spring Security Filter์์ Preflight Request์ ๋ํ ์๋ต์ 401๋ก ๋ด๋ ค์ฃผ๊ธฐ ๋๋ฌธ์ CORS ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋์ง ์์๋ ๊ฒ์ ๋๋ค.
`Response to preflight request doesn't pass access control check:
No Access-Contrlol-Allow-Origin header is present is one the requested resource`
์ด๋ฒ์๋ ๋ค์ ํ๋ฒ ์์ฒญ์ ๋ณด๋ด๋ณด๊ณ ๋ก๊ทธ๋ฅผ ํ์ธํด๋ณด๋ฉด ์์ ๊ฐ์ด Preflight Request์์ ๋ฌธ์ ๊ฐ ์๊ธด ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.mvcMatchers(HttpMethod.OPTIONS, "/**").permitAll() // Preflight Request ํ์ฉํด์ฃผ๊ธฐ
.antMatchers("/api/v1/**").hasAnyAuthority(USER.name());
}
}
Preflight Request๊ฐ 401๋ก ์๋ต์ค๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ค๋ฉด Security Config ์ค์ ์์ ํ๋ ์ถ๊ฐํด์ฃผ์ด์ผ ํ๋๋ฐ์. ์์ ์ฝ๋๋ Security Config ํ์ผ์ ์ผ๋ถ๋ถ์ธ๋ฐ, ์ฌ๊ธฐ์ mvcMatchers๋ฅผ ์ฌ์ฉํด์ Preflight Request OPTIONS ๋ฉ์๋ ์์ฒญ์ ํ์ฉํด์ฃผ๋ฉด ๋ฉ๋๋ค.(ํ์ฌ๋ ์์ ๊ฐ์ด ํด๊ฒฐํ์ง๋ง ์ถํ์๋ CORS Filter๋ฅผ ์ ์ฉํด๋ณผ ์๊ฐ์ ๋๋ค.)
Security Config์ OPTIONS ๋ฉ์๋๊ฐ ์ค๋ ๊ฒฝ์ฐ์ ํ์ฉ์ ํด์ฃผ์๋๋ Preflight Request๊ฐ 401์ด ์๋ 200์ผ๋ก ์๋ต ์ค๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ Preflight Request ์ดํ์ ๋ณธ ์์ฒญ๋ ์ ์์ ์ผ๋ก ์๋ต์ด ์ค๊ณ ์ ์๋ํ๋ ๊ฒ๊น์ง ํ์ธํ ์ ์์ต๋๋ค.
React Proxy๋ฅผ ์ฌ์ฉํด์ SOP ํด๊ฒฐํ๊ธฐ
์์์ WebMvcConfigurer ์ธํฐํ์ด์ค๊ฐ ๊ฐ์ง๊ณ ์๋ addCorsMappings ๋ฉ์๋์์ http://localhost:3000์ ๋ํ CORS ์ ๊ทผ ํ์ฉ ์ค์ ์ ํด์ฃผ๋ฉด SOP ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋๋ค๊ณ ํ์๋๋ฐ์. ์ด ๋ถ๋ถ์ Spring Server์์ ํ ์ ์๋ ๊ฒ์ด๊ณ React์์๋ SOP๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด Proxy ๋ผ๋ ๋ฐฉ๋ฒ์ด ์กด์ฌํฉ๋๋ค.
๋ฐ๋ก Proxy๋ฅผ ์์๋ณด๊ธฐ ์ ์ ๊ฐ๋จํ๊ฒ ๊ฒ์๊ธ ํ๋๋ฅผ ๋ถ๋ฌ์ค๋ JavaScript ์ฝ๋๋ฅผ ๋จผ์ ๋ณด๊ฒ ์ต๋๋ค.
export default function PostView() {
useEffect(() => {
(async () => {
try {
const { data } = await axios.get(
`/api/v1/post/${postId}`, {
headers: { 'Authorization': `Bearer ${result}` },
}
);
setData(data.data);
} catch (error) {
alert(error);
}
})();
}, []);
}
์ฝ๋์์ axios๋ก ์๋ฒ API๋ฅผ ํธ์ถํ๋ ๊ณณ์ ๋ณด๋ฉด ์ ๋ ์ฃผ์๊ฐ ์๋ ์๋ ์ฃผ์๋ก ์ ํ์๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค. ์ด ์ํ๋ก ์๋ฒ API๋ฅผ ํธ์ถํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น์?
์๋ ์ฃผ์๋ก ํธ์ถํด๋ ์๋์ผ๋ก http://localhost:3000์ด ์์ ๋ถ๋ ๊ฒ์ ๋ณผ ์ ์๊ณ ๋น์ฐํ๊ฒ๋ http://localhost:3000/api/v1/post/1 ์ ์กด์ฌํ์ง ์๊ธฐ ๋๋ฌธ์ 404 Not Found๊ฐ ์๋ต์ผ๋ก ์ค๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ํ์ํ ๊ฒ์ด ๋ฐ๋ก Proxy ์ ๋๋ค.
React์์ Proxy ์ ์ฉํ๊ธฐ
๋ธ๋ผ์ฐ์ ์์ React Dev Server๋ฅผ ํธ์ถํ๊ณ React Dev Server์์ Proxy๋ฅผ ํตํด์ http://localhost:8080 -> http://localhost:3000๋ก ๋์ฒดํ์ฌ Spring Server๋ฅผ ํธ์ถํ๊ฒ ๋ฉ๋๋ค. ์ฆ, Proxy๋ฅผ ํตํด์ {{Base_url}}์ ๋์ฒดํ ์ ์๊ธฐ์ ๋ค๋ฅธ ์ถ์ฒ๋ก ์ธ์ํ์ง ์๊ณ ๊ฐ์ ์ถ์ฒ๋ก ์ธ์ํ์ฌ CORS ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์๋ ๊ฒ์ ๋๋ค.
๊ทธ๋์ React์์ Proxy๋ฅผ ์ค์ ํ๋ ๋ฒ์ ๋ํด์ ์์๋ณด๊ฒ ์ต๋๋ค.
yarn add http-proxy-middleware
๋จผ์ http-proxy-middleware ๋ชจ๋์ ์ค์นํ๊ฒ ์ต๋๋ค.
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
createProxyMiddleware('/api/v1', {
target: 'http://localhost:8080',
changeOrigin: true,
})
);
};
๊ทธ๋ฆฌ๊ณ src ํด๋ ์๋์ setProxy.js ๋ผ๋ ํ์ผ์ ๋ง๋ ํ์ ์์ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ๋ฉด Proxy ์ค์ ์ด ๋์ ๋๋ค.
๊ทธ๋ฐ๋ฐ ๋ฌธ์ ๋ ์์ ๊ฐ์ด ์ค์ ํด๋ Proxy๊ฐ ์ ์ฉ๋์ง ์๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค. ์ด๋ฒ์๋ ์ ์๋ ๊น.. ํ๋ฉด์ ์ข ์ฐพ์๋ณด๋ ์ฌ๊ธฐ ์์ ์์ธ์ ์ฐพ์ ์ ์์๋๋ฐ์. ์ถ์ธก๋๋ ์์ธ์ yarn.lock, node_modules์์ Cache ํ๊ณ ์์ ์ ์์ด์ ์ง์ ๋ค ๋ค์ ์ค์นํด๋ณด๋ผ๋ ๋ด์ฉ์ด์์ต๋๋ค.
rm -rf yarn.lock node_modules
yarn install
๊ทธ๋์ ์์ ๊ฐ์ด ๋ค์ ์ค์นํ ํ์ ์คํํ๋ ์ ์์ ์ผ๋ก Proxy๊ฐ ๋์ํ์์ต๋๋ค.
๋ธ๋ผ์ฐ์ ์ Network ํญ์ ์ด์ด๋ณด๋ฉด ์ด๋ฒ์๋ http://localhost:3000์ผ๋ก ์์ฒญ์ ๋ณด๋๊ณ ์์ง๋ง 404 Not Found๊ฐ ๋ฐ์ํ์ง ์๊ณ ์ ์์ ์ผ๋ก ์๋ต์ด ์ค๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค. ์ฆ, Proxy๊ฐ ์ ๋๋ก ๋์ํ๊ธฐ์ http://localhost:3000์ผ๋ก ์์ฒญํ ๊ฒ์ฒ๋ผ ๋ณด์ด์ง๋ง ์ค์ ๋ก๋ http://localhost:8080์ผ๋ก ์์ฒญ์ ๋ณด๋ธ ๊ฒ์ ๋๋ค.
๊ธ์ ๋ง๋ฌด๋ฆฌ ํ๋ฉฐ
๋น์ฐํ ๋ง์ด์ง๋ง, ์ฝ๋๋ ์ปดํจํฐ๋ ๊ฑฐ์ง๋ง ํ์ง ์๊ธฐ ๋๋ฌธ์.. ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด ๋ถ๋ช ํ ์ ๊ฐ ๋ฌด์๊ฐ ์๋ชปํด์ ๊ทธ๋ฐ ๊ฒ์ด๋ค๋ฅผ ํ๋ฒ ๋ ๊นจ๋ซ๊ฒ ๋ ๊ฒ ๊ฐ์ต๋๋ค. ์ฒ์์๋ CORS๋ WebMvcConfigurer ์ธํฐํ์ด์ค์ addCorsMappings ๋ฉ์๋๋ง ์ค๋ฒ๋ผ์ด๋ฉ ํ ํ์ ์ค์ ํด์ฃผ๋ฉด ์ฝ๊ฒ ํด๊ฒฐ๋ ๊ฒ์ด๋ผ๋ ์๊ฐ์ด ์ ๋ฅผ ์ฐ๋ฌผ ์์ผ๋ก ๊ฐ๋์๋ ๊ฒ ๊ฐ์ต๋๋ค.
์ง๊ธ๊น์ง ๋งค๋ฒ Android or iOS์ ํ์ ์ ํด๋ณด๋ค ๋ณด๋ CORS๋ฅผ ๋ง๋ ์ผ์ด ์์๋๋ฐ ์ด๋ฒ ๊ธฐํ์ ์ด๋ก ์ ๋์ด์ ์ง์ ๊ฒฝํํด๋ณด๋ฉด์ ํด๊ฒฐํด๋ณด๋๊น ์ข ๋ ๋ฟ๋ฏํ ๊ฒ ๊ฐ์ต๋๋ค.
reference : https://devlog-wjdrbs96.tistory.com/429