IT/Back-end

[Spring] Restful Web Service에서 교차 출처 공유 허용하기

omaeng 2020. 4. 10. 17:52

이번 포스팅에서는 요청에 Cross-Origin-Resource Sharing(CORS) 헤더를 포함하여 응답하는 웹서비스를 배워 보겠습니다!

 

 

CORS란?


https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

 

교차 출처 리소스 공유 (CORS)

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.

developer.mozilla.org

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.

 

뭐하러 출처에 대한 제한을 둘까? (귀찮기만 한데...)

 

보안 상의 이유로, 브라우저는 스크립트에서 시작한 교차 출처 HTTP 요청을 제한합니다. 예를 들어, XMLHttpRequest Fetch API 동일 출처 정책을 따릅니다. 즉, 이 API를 사용하는 웹 애플리케이션은 자신의 출처와 동일한 리소스만 불러올 수 있으며, 다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 합니다.

 

 

 

최종 결과물


우리가 최종적으로 만들게 될 결과물은 8080포트에서 9000포트와 9090포트의 교차출처를 허용하고, 루트 접근 이외의 접근을 막는 웹 서비스를 만들어 보겠습니다.

 

  • 프로젝트: 동일한 폴더
  • 기동: 8080포트, 9000포트, 9099포트
  • index.html: 8080포트의 응답을 받아옴

테스트 결과 요약

  • 8080포트 기동 -> index.html -> hello.js -> 8080포트의 greeting 경로를 받아옴.
  • 9090포트 기동 -> index.html -> hello.js -> 8080포트의 greeting 경로를 받아옴.
  • 9000포트 기동 -> index.html -> hello.js(응답을 받아오지 못함.) -> 8080포트의 greeting 경로를 받아오지 못하고 index.html의 값만 출력.

 

Spring Initializr


https://start.spring.io/

불러오는 중입니다...

위의 Spring Iniitalizr를 이용해 프로젝트를 생성.

 

spring web을 추가하고 제너레이트 해줍니다.

 

제너레이트 받은 압축파일을 workspace에 풀어 줍니다.

 

(그전에! 필히 외부망으로 네트워크를 변경해 주세요!)

 

자 이제 프로젝트를 vscode로 오픈 해주세요!

 

 

빌드를 하고 있는 장면이 보이십니다.

 

빌드가 끝나고 나면, pom.xml의 내용이 다음과 같은지 확인해 주세요.

 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.5.RELEASE</version>
		<!--<relativePath/>  -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

gradle이신 경우에도 마찬가지로 의존성이 잘 주입되었는지 확인해 주세요.

 

리소스 표현 클래스 작성


 

com/example/demo/ 경로에 새파일을 만들고, Greeting.java를 입력한 후 엔터를 눌러주세요.

 

Greeting.java 안에 내용은 다음과 같이 작성해 주세요.

 

package com.example.demo;
 
public class Greeting {
 
    private final long id;
    private final String content;
 
    public Greeting() {
        this.id = -1;
        this.content = "";
    }
 
    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }
 
    public long getId() {
        return id;
    }
 
    public String getContent() {
        return content;
    }
}

복사하여 붙여넣기를 하신다면 꼭! pakage 경로를 주의하세요.

 

실습하시는 프로젝트의 이름 구성에 따라 상이할 수 있으니, 에디터의 도움을 받아 qiuck fix를 하시거나 직접 수정해 주세요.

 

Controller 작성하기


com/example/demo/ 경로에 동일하게 GreetingController.java 파일을 만들어 줍니다.

 

package com.example.demo;
 
import java.util.concurrent.atomic.AtomicLong;
 
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class GreetingController {
 
    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();
 
    @GetMapping("/greeting")
    public Greeting greeting(@RequestParam(required=false, defaultValue="World") String name) {
        System.out.println("==== in greeting ====");
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }
 
}

 

이제 public 폴더를 src 폴더와 동일한 위치로 생성을 해봅시다.

public 폴더안에 index.html 그리고 hello.js 파일을 생성해 줍니다.

 

 

hello.js 파일에 ajax 통신으로 8080의 데이터를 받아옵니다.

 

$(document).ready(function() {
    $.ajax({
        url: "http://localhost:8080/greeting"
    }).then(function(data, status, jqxhr) {
       $('.greeting-id').append(data.id);
       $('.greeting-content').append(data.content);
       console.log(jqxhr);
    });
});

 

index.html에서 받아온 데이터를 뿌려줄 틀을 만들어 봅니다.

 

<!DOCTYPE html>
<html>
    <head>
        <title>Hello CORS</title>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        <script src="hello.js"></script>
    </head>
 
    <body>
        <div>
            <p class="greeting-id">The ID is </p>
            <p class="greeting-content">The content is </p>
        </div>
    </body>
</html>

 

굳이 서버를 기동해보지 않아도 어떤 값이 화면에 전달될지 상상이 됩니다. 

 

The ID is 0

The content is 000000000

 

이렇게 화면이 사용자에게 보여지겠죠?

 

하지만 8080포트에서만 접속이 가능합니다...

 

만약에 9090포트에서 hello.js의 ajax를 통해 8080의 데이터를 가지고 오려 한다면?

 

네... 보시는바와 같이 CORS문제 입니다...

 

이제 9090포트의 교차 출처 공유를 허용할텐데요!

 

두가지 방법이 있습니다. 

 

  • 9090포트: controller에서 허용해주기
  • 9090포트: main에서 config 설정으로 허용해주기

포트별로 다르게 적용하여 구현해보도록 하겠습니다.

 

 

먼저 메인클래스에서 

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
public class DemooApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemooApplication.class, args);
	}

	@Bean
	public WebMvcConfigurer corsConfigurer() {
		return new WebMvcConfigurer() {
			@Override
			public void addCorsMappings(CorsRegistry registry) {
				registry.addMapping("/greeting-javaconfig").allowedOrigins("http://localhost:9090");
			}
		};
	}

}

@Bean 어노테이션을 통해 @SpringBootApplication의 @Configuration 을 설정해 주겠습니다. 

 

보시는바와 같이 webMvcConfigurer corsconfigurer로 허용할 패스와 출처를 명시해 허용해주었습니다.

 

 

다음으로 controller에서 

package com.example.demoo;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @CrossOrigin(origins = "http://localhost:9090")
    @GetMapping("/greeting")
    public Greeting greeting(@RequestParam(required = false, defaultValue = "World") String name) {
        System.out.println("==== in greeting ====");
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }

}

@CrossOrigin 어노테이션을 통해 허용해줬습니다.

 

테스트


 

1. 터미널에서 gradlew bootrun 명령어를 실행.

2. Ctrl + Shift + ~ 를 통해 새 터미널 창을 실행

3. 터미널에서 gradlew bootRun --args='--server.port=9090' 명령어를 실행.

4. Ctrl + Shift + ~ 를 통해 새 터미널 창을 실행

5. 터미널에서 gradlew bootRun --args='--server.port=9000' 명령어를 실행.

 

자 이제 3개의 포트에서 같은 프로젝트가 돌아가고 있습니다. 사실상 코드만 같고 다른 웹서비스죠.

 

각 루트를 들어가보시면

 

허용된 포트의 요청만 올바르게 응답되었습니다!