반응형

스프링 시큐리티와 앵귤러JS Spring Security and AngularJS


Part I : 단일 페이지 보안 어플리케이션 (A Secure Single Page Application)

Part II: 로그인페이지 (The Login Page)

Part III: 리소스 서버 (The Resource Server)

Part IV: API 게이트웨이 (The API Gateway)

Part V: OAuth2와 싱글사인온 (Single Sign On with OAuth2)

Part VI: 다중 UI 어플리케이션과 게이트웨이 (Multiple UI Applications and a Gateway)

Part VII: 모듈화한 AngularJS 어플리케이션  (Modular AngularJS Application)

Part VIII: Angular 어플리케이션 테스트 (Testing an Angular Application)


--------------------------------------------------------------------------------


API 게이트웨이 The API Gateway

이 섹션에서 우리는 어떻게 스프링 시큐티리와 앵귤러JS로 단일 페이지 어플리케이션을 만드는지 계속 얘기해볼 것이다. 이제 스프링 클라우드를 사용하여 백엔드 리소스에 접근하고 인증을 제어하기 위한 API 게이트웨이를 만드는 법을 보여줄 것이다. 이 글을 시리즈의 4번째 섹션으로 당신이 어플리케이션의 기본구성을 이해하거나 처음부터 빌드해보려면 첫번째 섹션부터 읽도록 하자 또는 그냥 Github의 소스코드로 바로 가도 된다. 이전 세션에서 스프링 세션을 사용해 백엔드 리소스를 인증하기 위한 간단히 배포한 어플리케이션을 만들어보았다. 이번 섹션에서 우리는 UI서버를 백엔드 리소스 서버로 reverse proxy로 만들것이다. 지난 구현물의 문제(커스텀 토큰 인증에 의해 언급된 기술적 복잡성)를 해결하고 브라우저 클라이언트의 접근 제어를 위한 새로운 옵션을 제공할 것이다.

기억하기: 만일 당신이 이섹션의 샘플을 동작시키려면 브라우저의 쿠키와 HTTP Basic credential에 대한 캐시값을 지워주어야 한다. 크롬에선 새 incognito 창을 쓰는게 최선의 방법이다.

API 게이트웨이 만들기 Creating an API Gateway

API게이트웨이는 (이 섹션의 예제들과 같은) 브라우저에 기반의 프론트앤드 클라이언트들을 위한 단일 진입로entry이다. 클라이언트는 서버의 URL을 알아야만 하고 백엔드는 - 주요한 장점으로- 수정없이 재정의 될것이다. 중앙집중화와 제어의 또 다른 이점은 한계치 설정 인증, 감사(audit) 그리고 로깅(logging)이다. 그리고 간단한 reverse proxy를 구현하는 것은 스프링 클라우드를 쓰면 정말 간단하다.

당신이 코드를 함께 따라가려 한다면, 지난 섹션의 마지막에 했던 어플리케이션 구현은 조금 복잡하다는 것을 알고 것이다. 따라서 이 여행을 다시 시작하기에 적합한 장소는 아니다. 그러나 우리가 좀더 쉽게 시작할 수 있는 중간점이 있는데 백엔드 리소스가 아직 스프링 시큐리티에 의해 보호되지 않았던 곳이다. 이 부분의 소스코드는 Github에 별도의 프로젝트로 넣어두었으므로 우리는 여기서부터 출발할 것이다. 이 프로젝트는 UI 서버와 리소스 서버를 가지고 있고 이 둘이 서로 각각 소통하고 있다. 리소스 서버는 아직 스프링 시큐리티가 없는 상태이므로 우리는 먼저 시스템을 동작하게 한뒤 그 후 이 레이어를 추가하자.

단 한줄의 선언적 Reverse Proxy Declarative Reverse Proxy in One Line

이 프로젝트를 API 게이트웨이로 바꾸려면, UI서버를 약간 손봐줘야한다. 스프링 설정이 있는 그곳에 @EnableZuulProxy 어노테이션을 추가해줘야한다. 예를 들면 메인 어플리케이션안에 다음과 같이:

UiApplication.java

@SpringBootApplication
@RestController
@EnableZuulProxy
public class UiApplication {
  ...
}

외부 설정 파일안에 우리는 UI서버의 로컬 리소스를 외부설정("application.yml")의 원격 리소스와 맵핑 해줘야한다. 

application.yml

security:
  ...
zuul:
  routes:
    resource:
      path: /resource/**
      url: http://localhost:9000

이것의 의미는 서버의 패턴 /resource/** 의 경로를 외부서버인 localhost:9000의 같은 경로와 맵핑하라는 것이다. 간단하지만 효율적이다. (맞다. YAML로 6줄이다. 항상 이것이 필요한 것은 아니다)

이것을 동작시키려고 우리가 해야하는 전부는 클래스패스에 올바른 의존성들을 넣어주는 것이다. 이 목적을위해 우리의 메이븐 POM에 몇줄을 추가했다:

pom.xml

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-parent</artifactId>
      <version>1.0.0.BUILD-SNAPSHOT</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
  </dependency>
  ...
</dependencies>

"spring-cloud-starter-zuul"의 사용에 덧붙이자면, 이것은 스프링 부트가 가지고 있는 하나의 starter POM이다. 하지만 의존성을 관리하면 우리는 Zuul proxy가 필요하다. 또한 <dependencyManagement>를 사용하고 있는데 왜냐하면 우리는 모든 버젼의 의존성이 올바르게 의존될 수 있기를 원하기 때문이다.

클라이언트에서 Proxy 소비하기 Consuming the Proxy in the Client

위의 수정후에도 우리의 어플리케이션은 여전히 동작한다. 하지만 클라이언트를 수정할때까지 아직 새로운 프락시를 실제로 사용할 수 없다. 운좋게도 이것는 "home" 컨트롤러의 구현물을 다음과 같이 아주 경미하게 수정해주면 된다:

hello.js
angular.module('hello', [ 'ngRoute' ])
...
.controller('home', function($scope, $http) {
	$http.get('http://localhost:9000/').success(function(data) {
		$scope.greeting = data;
	})
});

로컬 리소스로는:

hello.js
angular.module('hello', [ 'ngRoute' ])
...
.controller('home', function($scope, $http) {
	$http.get('resource/').success(function(data) {
		$scope.greeting = data;
	})
});

이제 서버를 돌리면, 모든게 잘 작동한다. 요청은 UI (API게이트웨이)를 거쳐 리소스 서버로 프락시되어진다.

더욱 간소화 Further Simplifications

훨씬 더 나은점: 우리는 더이상 리소스 서버에 CORS 필터가 필요없다. 어쨋든 우리는 매우 신속하게 이것들을 내버렸고 우리가 무언가를 손에 의해 의존해야한다면 그것은 빨간불이다 (특히 보안과 관련해서). 운좋게도 이제 불필요하기때문에 우리는 이것을 버릴수 있고 밤에 편히 발뻗고 잘 수 있게 되었다.

리소스 서버 보안적용하기 Securing the Resource Server

아마 중간정도에 우리가 보안이 없는 리소스 서버로 시작한다고 얘기한 것을 기억해보자.

여담: 만일 당신의 네트워크 아키텍쳐가 어플리케이션 아키텍쳐를 미러(mirror)한다면, 소프트웨어에 보안의 결여는 문제조차 되지않을 수 있다. (당신은 리소스 서버를 물리적으로 접근불가능하게 만들수 있겠지만 UI 서버는 그렇지 않다). 리소스 서버를 로컬호스트에서만 접근가능하도록 만드는 간단한 실예로서, 리소스 서버의 application.properties에 다음을 추가해보자:

application.properties
server.address: 127.0.0.1

우와, 엄청 쉽네! 당신의 데이터 센터에서만 볼 수 있는 네트워크주소를 가지고 이렇게 해보라. 그리면 당신은 모든 사용자 데스크탑과 리소스서버에서 작동하는 시큐리티 솔루션을 가질 수 있다.

우리는 (수많은 이유와 같이) 소프트웨어 레벨의 시큐리티가 필요하다고 결정했다고 가정했다. 이것은 문제가 될수 없다. 왜냐하면 우리가 해야하는 모든 것은 스프링 시큐리티를 (리소스 서버 POM에) 의존성을 추가해주는것 뿐이기때문이다.

pom.xml

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

이거면 보안적용된 리소스 서버를 갖추기에 충분하다. 그러나 아직 동작하는 어플리케이션을 가질 수 없다. 두 서버 사이에 인증상태를 공유할 수 없다는 세번째 섹션에서 하지 않았던 같은 이유때문이다.

인증 상태 공유하기 Sharing Authentication State

우리는 인증(과 CSRF) 상태를 공유하기 위해 우리가 지난 섹션에서 스프링 세션을 가지고 한것과 같은 메카니즘을 사용할 수 있다. 그전에 양쪽 서버에 의존성을 추가해주자:

pom.xml
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session</artifactId>
  <version>1.0.0.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-redis</artifactId>
</dependency>

그러나 이 시점에서의 설정은 훨씬 더 쉽다. 우리가 양쪽에 동일한 Filter선언을 추가해 두었기 때문이다. 먼저 (@EnableRedisHttpSession를 추가했던) UI서버: 

UiApplication.java

@SpringBootApplication
@RestController
@EnableZuulProxy
@EnableRedisHttpSession
public class UiApplication {

  ...

}

그 다음 리소스 서버. 다음의 3가지 작은 수정이 있다. 하나는ResourceApplication에 @EnableRedisHttpSession 추가하기:

ResourceApplication.groovy
@SpringBootApplication
@RestController
@EnableRedisHttpSession
class ResourceApplication {
  ...
}

또 하나는 리소스 서버의 HTTP Basic을 명시적으로 disable하기 (브라우저가 인증 다이얼로그가 팝업되는 것을 막기위해):

ResourceApplication.groovy
@SpringBootApplication
@RestController
@EnableRedisHttpSession
class ResourceApplication extends WebSecurityConfigurerAdapter {

  ...

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic().disable()
    http.authorizeRequests().anyRequest().authenticated()
  }

}

여담: 인증 다이얼로그를 막는 하나의 대안은 HTTP Basic을 유지하되 401 에러를 "기본"이외의 다른 액션을 취하게 변경하는 것이다. 당신은  HttpSecurity설정 콜백내의AuthenticationEntryPoint의 다 한 줄 구현으로 이렇게 할 수 있다.

그리고 마지막으로 application.properties에 none-stateless세션 생성정책을 명시화 하는 것이다:

application.properties

security.sessions: NEVER

레디스가 백그라운드에서 동작하고 있는 한 (이렇게 시작하려면 fig.yml을 사용하자) 시스템은 동작할 것이다. http://localhost:8080 의 UI에서 홈페이지를 불러서 로그인하면 백엔드로 부터의 메세지가 홈페이지에 랜더된 것을 볼 수 있을 것이다.

어떻게 동작하는가? How Does it Work?

보이지않는 곳에서 어떻게 동작되는가? 먼저 UI서버(API 게이트웨이)의 HTTP요청을 둘러보자:

VerbPathStatusResponse

GET

/

200

index.html

GET

/css/angular-bootstrap.css

200

Twitter bootstrap CSS

GET

/js/angular-bootstrap.js

200

Bootstrap and Angular JS

GET

/js/hello.js

200

Application logic

GET

/user

302

Redirect to login page

GET

/login

200

Whitelabel login page (ignored)

GET

/resource

302

Redirect to login page

GET

/login

200

Whitelabel login page (ignored)

GET

/login.html

200

Angular login form partial

POST

/login

302

Redirect to home page (ignored)

GET

/user

200

JSON authenticated user

GET

/resource

200

(Proxied) JSON greeting

쿠키이름이 사소하게 다른 것("JSESSIONID"대신 "SESSION")만 제외하면 두번째 섹션의 마지막에 언급된 순서와 동일하다. 이는 우리가 스프링 세션을 사용하고 있기 때문이다. 하지만 아키텍쳐는 다르다. 마지막 "/resource"로의 요청은 특별한데 이는 리소스 서버로의 프록시된 것이기 때문이다.

이제 UI서버의 "/trace" endpoint에서 보여지는 것처럼 실제 reverse proxy를 볼 수 있다. (스프링 클라우드의 의존성으로 우리가 추가했던 스프링 부트 Actuator). http://localhost:8080/trace 를 새 브라우저에서 열고 맨 밑으로 스크롤해보자 (아직 브라우저에 JSON 플러그인이 없다면 하나 받아두면 보기에도 가독성에도 좋다).  (브라우저 팝업에서) HTTTP Basic 인증을 해야할 것이지만 로그인폼에서 했던 같은 Credential이면 된다. 거의 끝무렵에 다음과 같은 요청의 묶음을 볼 수 있을 것이다:

인증이 겹치지 않게 하려면 다른 브라우저를 사용하자 (이를 테면 당신이 크롬으로 UI를 테스트해왔다면, 파이어폭스를 써라) - 동작하는 어플이 멈추진 않을테지만, 같은 브라우저에서 인증이 섞이면 trace의 가독성이 매우 떨어질 것이다. 

/trace

{
  "timestamp": 1420558194546,
  "info": {
    "method": "GET",
    "path": "/",
    "query": ""
    "remote": true,
    "proxy": "resource",
    "headers": {
      "request": {
        "accept": "application/json, text/plain, */*",
        "x-xsrf-token": "542c7005-309c-4f50-8a1d-d6c74afe8260",
        "cookie": "SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260",
        "x-forwarded-prefix": "/resource",
        "x-forwarded-host": "localhost:8080"
      },
      "response": {
        "Content-Type": "application/json;charset=UTF-8",
        "status": "200"
      }
    },
  }
},
{
  "timestamp": 1420558200232,
  "info": {
    "method": "GET",
    "path": "/resource/",
    "headers": {
      "request": {
        "host": "localhost:8080",
        "accept": "application/json, text/plain, */*",
        "x-xsrf-token": "542c7005-309c-4f50-8a1d-d6c74afe8260",
        "cookie": "SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260"
      },
      "response": {
        "Content-Type": "application/json;charset=UTF-8",
        "status": "200"
      }
    }
  }
},

두번째 엔트리는 클라이언트로부터 "/resource"의 게이트웨이까지의 요청이다. (브라우저에 추가된) 쿠키와 (두번째 섹션에서 언급된 앵귤러에 의해 추가된) CSRF헤더를 볼 수 있을 것이다. 첫번째 엔트리는 리소스 서버로의 호출이 트랙킹된다는 의미로remote: true 값을 가진다. "/" uri 경로를 가면 쿠키와 CSRF헤더가 역시 보내진 것을 볼 수 있다. 스프링 세션없이 이들 헤더는 리소스 서버에서의 의미가 없지만 우리는 이제 이들 헤더들을 인증과 CSRF토큰 데이터를 가지는 하나의 세션으로 재구성하도록 설정함으로서 요청이 받아들여진다.

결 론 Conclusion

우리는 이섹션에서 많은 것을 커버했다. 그러나 우리의 두서버의 기본 껍데기의 코드량을 최소화한 멋진 방법을 찾았다. 이 두 서버 모두 멋지게 보안이 적용되었고 사용자 경험을 절충하지 않아도 된다. 이것이 API 게이트웨이 패턴을 사용한 이유지만 사실 우리는 수박겉핥기수준으로 써본거다 ( 넷플리스는 수많은 부분에서에서 사용하고 있다). 스프링 클라우드를 읽고 게이트웨이의 수많은 기능들을 얼마나쉽게 추가할 수 있는지 찾아보자. 이 시리즈의  다음 섹션에서는 어플리케이션 아키텍쳐를 조금 확장하여 각각 별도의 서버에서 인증 책임을 뽑아낼 것이다 (싱글 사인온 패턴 the Single Sign On pattern).



반응형

+ Recent posts