반응형

스프링 시큐리티와 앵귤러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)


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


모듈화한 앵귤러 JS 어플리케이션 Modular AngularJS Application

이 섹션에서 우리는 어떻게 스프링 시큐티리와 앵귤러JS로 단일 페이지 어플리케이션을 만드는지 계속 얘기해볼 것이다. 이제 우리는 어떻게하면 클라이언트 코드를 모듈화하는지, 어떻게 하면 앵귤러가 기본값으로 사용하지만 대부분의 사용자가 싫어하는 ("/#/login"같은) 파편화된 표기없이 "멋진" URL 경로를 사용할 수 있는지를 보여줄 것이다. 이 글은 시리즈의 일곱번째 섹션으로 첫번째 섹션부터 어플리케이션의 기초 구성단위를 처음부터 배워가도 된고 아니면 Github의 소스코드를 바로 가봐도 된다. 우리는 이 시리즈의 남은 섹션을 자바스크립트의 미진한 부분을 깔끔하게 정리 해볼 것이다. 동시에 어떻게 하면 스프링 시큐리티와 스프링 부트로 빌드된 백엔드 서버에 포근하게 맞출 수 있는지 보여줄 것이다.

어플리케이션 쪼개기 Breaking up the Application

이 시리즈에서 우리가 지금까지 동작했던 예제 어플리케이션 전체를 단하나의 자바스크립트 소스파일로 때우기에 충분히 사소했다. 더 큰 어플리케이션은 이와 같은 방식으로 가서는 안된다. 이렇게 하나로 시작했을 때조차,  예제에서 실제의 환경에서 동작하는 것처럼 만들어야 하므로 우리는 이들을 나눌것이다. 좋은 시작점은 두번째 섹션에서 만들었던 "단일"어플리케이션을 취해 그 소스코드의 구조를 들여다 보는 것이다. 여기 정적 컨탠트를 위한 디렉토리 리스트가 있다( 서버에 있는 "application.yml"은 제외했다):

static/
 js/
   hello.js
 home.html
 login.html
 index.html

여기에 몇가지 문제점이 있는데 하나는 명확하다: 모든 자바스크립트가 하나의 파일(hello.js)에 있다는 것이다. 또 하나는 감지하기 힘든데: 우리의 어플리케이션 내부의 뷰가 HTML이 "부분적"("login.html"과 "home.html")이지만 이들은 모두 평이한 구조안에 있으며 이들을 사용하는 컨트롤러 코드와 연관되어있지 않다.

자바스크립트를 더 자세히 들여다보자. 우리는 좀 더 관리하기 쉬운 조각들로 나누는 것을 앵귤러가 더 쉽게 만들어주고 있는 것을 볼 수 있다:

hello.js

angular.module('hello', [ 'ngRoute' ]).config(

  function($routeProvider, $httpProvider) {

    $routeProvider.when('/', {
      templateUrl : 'home.html',
      controller : 'home'
    }).when('/login', {
      templateUrl : 'login.html',
      controller : 'navigation'
    }).otherwise('/');

    ...

}).controller('navigation',
    function($rootScope, $scope, $http, $location, $route) {
      ...
}).controller('home', function($scope, $http) {
    ...
  })
});

여기에 약간의 "config" 와 두개의 컨트롤러("home" 과 "navigation")이 있고 컨트롤러는 이 일부분(각각 "html.html"과 "login.html")을 멋지게 맵핑해주고 있다. 이 일부들을 각각 쪼개보자:

static/
  js/
    home/
      home.js
      home.html
    navigation/
      navigation.js
      login.html
    hello.js
  index.html

컨트롤러 정의는 동작하는 데 필요한 HTML과 함께 각각의 모듈안으로 이동하였다. - 멋게 모듈화되었다. 만일 이미지나 커스텀 스타일시트가 필요하면, 우리는 이들과 똑같은 방식으로 만들어주면 된다.

same with those.

 모든 클라이언트 코드는 (index.html 를 제외하고) 단일 디렉토리 밑에 놓는다. 왜냐하면 "welcome" 페이지가 있어 "정적" 디렉토리로부터 자동적으로 불러지기 때문이다. 이것은 의도적으로서 모든 정적 리소스로의 단일 스프링 시큐리티 접근 규칙을 만들기 쉽게 하기 위해서다. 이들은 모두 보호받지 않는다. (스프링 부트 어플리케이션에서는 기본적으로  /js/** 를 보호하지 않기 때문이다), 하지만 다른 어플리케이션을 위해 다른 규칙이 필요하다면 다른 경로를 선택해주면 된다.

예를 들어 여기 home.js를 보자:

code,javascript
angular.module('home', []).controller('home', function($scope, $http) {
    $http.get('/user/').success(function(data) {
        $scope.user = data.name;
    });
});

그리고 여기 새로운 hello.js:

code,javascript
angular
    .module('hello', [ 'ngRoute', 'home', 'navigation' ])
    .config(

        function($routeProvider, $httpProvider) {

          $routeProvider.when('/', {
            templateUrl : 'js/home/home.html',
            controller : 'home'
          }).when('/login', {
            templateUrl : 'js/navigation/login.html',
            controller : 'navigation'
          }).otherwise('/');

          $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

        });

어떻게 "hello" 모듈이 그들을 목록화함으로서 ngRoute와 함께 초기화 선언된 다른 두개를 신뢰하는지 알아두자. 이것을 동작하게 만드려면, 당신은 index.html안에 올바른 순서로 모듈정의를 불러주어야 한다:

...
<script src="js/angular-bootstrap.js" type="text/javascript"></script>
<script src="js/home/home.js" type="text/javascript"></script>
<script src="js/navigation/navigation.js" type="text/javascript"></script>
<script src="js/hello.js" type="text/javascript"></script>
...

이것은 실제 쓰이는 앵귤러 JS 의존성 관리 시스템이다. 다른 프레임워크들도 (단언컨대 더 우수한) 유사한 기능을 가지고 있다. 또한 더 큰 사이즈의 어플리케이션에서, 당신은 모든 자바스크립트를 함께 번들화 하는 빌드하는 절차를 밟아 브라우저가 더 효율적으로 불러올수 있게할 것이다. 그러나 이건 거의 취향의 차이이다.

"자연스러운" 라우트 사용하기 Using "Natural" Routes

앵귤러의 $routeProvider는 기본값으로 URL 경로안에 파편화된 위치탐지자를 가지고 동작한다. 예를 들어 "/login"경로로서 hello.js 안에 라우트되도록 명시한 로그인 페이지는 (브라우저 윈도우에서 당신이 보게되는) 실제 URL에서 "/#/login" 로 해석되어진다. 이는 루트패스 "/"를 통해 불러오는 index.html안의 자바스크립트에서 모든 라우트들을 이렇게 활성화한다. 파편화된 이름은 약간 사용자에 익숙하지않으며 때론 URL 경로가 앵귤러 라우트 선언과 같도록 "자연스럽게" 라우트되도록하는게 더 편리하다. 예를 들어 "/login"에 "/login"을 씀. 만일 당신이 오직 정적 리소스만 가지고 있다면 이렇게 할 수 없다. 왜냐하면 index.html 은 한방향으로만 불러지기 때문이다. 그러나 (프록시나 어떤 서버사이드 로직) 스택안에 어떤 활성화된 컴포넌트가 를 가지고 있다면, 모든 앵귤러 라우트로부터 index.html 를 불러옴으로서 처리할 수 있다.

이 시리즈에서 당신은 스프링 부트를 사용중이므로 물론, 당신은 서버-사이드 로직을 가지고 있다. 간단한 스프링 MVC 컨트롤러를 사용하여 당신은 어플리케이션의 라우트를 원래대로 사용할 수 있다. 이를 위해 당신이 해줘야 하는 일은 서버에 앵귤러를 열거해주는 것이다. 식별자규칙(naming convention)에 의해 이것을 하도록 선택하였다. 마침표를 포함하지 않은 (그리고 명시적으로 이미 맵핑되지않은) 모든 경로는 앵귤러 라우트로서 홈페이지에 보내져야(forward)한다:

@RequestMapping(value = "/{[path:[^\\.]*}")
public String redirect() {
  return "forward:/";
}

이 메소드는 스프링 어플리케이션의 어딘가의 (@RestController이 아니라) @Controller 에서 넣어줘야한다. 우리는 브라우저가 사용자가 실제 URL에서 보는 "실제" 라우트를 기억할 수 있도록 ("redirect가 아니라) "foward"를 사용했다. 비록, 우리가 이 어플리케이션에어 이 장점을 적용하진 않겠지만, 이는 또한 스프링 시큐리티의 인증을 위한 저장된 요청(saved request) 메카니즘이 막 바로 사용가능(out of the box)하도록 동작한다는 것을 의미한다.

 github의 예제코드에 있는 어플리케이션은 추가적인 라우트를 가지고 있어 당신은 더 완전한 기능을 확인할 수 있다. 그러므로 실사용이 가능한 어플리케이션일 것이다. ("/home"과 "/message"는 약간 다른 뷰를 가지는 다른 모듈들이다).

이 "자연스러운" 라우트를 가지는 어플리케이션을 완성시키기위해, 당신은 앵귤러에게 이 두가지 절차를 알려줘야한다. 첫번째는hello.js에서 $locationProvider안의 config함수에 "HTML5모드"설정을 추가해야한다:

angular.module('hello', [ 'ngRoute', 'home', 'navigation' ]).config(

  function($locationProvider, $routeProvider, $httpProvider) {

    $locationProvider.html5Mode(true);
    ...
});

index.html안의 HTML헤더안에 추가적인 <base/> 엘리먼트와 같이 묶어, 당신은 메뉴바의 링크를 파편화("#")를 없애기 위해 바꿔줘야한다:

<html>
<head>
<base href="/" />
...
</head>
<body ng-app="hello" ng-cloak class="ng-cloak">
    <div ng-controller="navigation" class="container">
        <ul class="nav nav-pills" role="tablist">
            <li><a href="/">home</a></li>
            <li><a href="/login">login</a></li>
            <li ng-show="authenticated"><a href="" ng-click="logout()">logout</a></li>
        </ul>
    </div>
...
</html>

앵귤러는<base/> 엘리먼트에 라우트를 정박하고 브라우저에 나타나는 URL들을 쓰기 위해 사용한다. 당신은 스프링 부트 어플리케이션을 동작하고 있으므로 기본 설정은 (8080포트상의) "/" 루트 경로로 부터 제공된다. 만일 같은 어플리케이션의 다른 루트 경로로부터 제공할 필요가 있다면, 그 경로를 서버-사이드 템플릿(많은 사람들이 단일 페이지 어플리케이션을 위해 정적리소스와 함께 쓰기를 원한다 그래서 그들은 정적 루트 경로와 함께 쓰인다)을 사용하여 HTML안으로 랜더해줘야 할 것이다.

인증 관련정보 추출하기 Extracting the Authentication Concerns

위에서 처럼 어플리케이션을 모듈화할 때, 당신은 코드가 그저 모듈들로 나누어져서 작동시킨 것뿐이라는 걸 알수 있을 것이다. 하지만, 사소하게 트집을 잡자면 우리는 여전히 $rootScope를 사용하여 컨트롤러간의 상태를 공유한 다는 점이다. 이것은 이런 작은 규모의 어플리케이션에서는 크게 잘못된 것은 아니다. 이렇게 함으로서 아주 괜찮은 수준의 프로토타입을 아주, 아주 빠르게 만들수 있게 된다. 그러니 이것에 대해 너무 슬퍼할 필요는 없다. 그러나 우리가 각각 분리된 모듈에서 인증관련 모든정보를 추출할 기회가 있다면, 앵귤러 용어에서 당신이 필요한 것을 "서비스"라고 정의한다. 그래서 ("auth"라 부르는) 새로운 모듈을 당신의 "home"과 "navigation" 모듈 다음으로 만들어보자:

static/
  js/
    auth/
      auth.js
    home/
      home.js
      home.html
    navigation/
      navigation.js
      login.html
    hello.js
  index.html

auth.js 코드를 작성하기 전, 우리는 다른 모듈의 수정을 예상할 수 있을 것이다. 먼저 navigation.js 에서 "navigation"모듈을 새로운 "auth"모듈에 의존성을 가지도록 만들어주어야한다. "auth"서비스를 컨트롤러에 주입하자 (물론 $rootScope 은 이제 더이상 필요없다):

angular.module('navigation', ['auth']).controller(
        'navigation',

        function($scope, auth) {

            $scope.credentials = {};

            $scope.authenticated = function() {
                return auth.authenticated;
            }

            $scope.login = function() {
                auth.authenticate($scope.credentials, function(authenticated) {
                    if (authenticated) {
                        console.log("Login succeeded")
                        $scope.error = false;
                    } else {
                        console.log("Login failed")
                        $scope.error = true;
                    }
                })
            };

            $scope.logout = function() {
              auth.clear();
            }

        });

이전의 컨트롤러와 많이 다르지않다 (여전히 사용자 액션, 로그인과 로그아웃을 위한 함수들과 로그인후 credential을 유지하고 있는 객체가 필요하다). 하지만 새 "auth" 서비스로 구현을 추상화하였다. "auth"서비스는 login()을 지원하기 위해 authenticate()함수가, logout()을 지원하기위해 clear() 함수를 필요로한다. 또한 authenticated 플래그가 있어 이전 컨트롤러의 $rootScope.authenticated 를 대체한다. 우리는 컨트롤러의 $scope에 붙여진 같은 이름으로 함수안에서 authenticated 플래그를 사용한다. 따라서 앵귤러가 이 값을 계속 확인하고 사용자가 로그인할때 UI를 업데이트한다.

"auth" 모듈을 재사용가능하게 만들길 원한다는 가정하에, 당신은 여기에 하드코드된 경로를 원하지 않을 것이다. 문제는 없지만, hello.js 모듈안의 경로를 초기화 하거나 설정해줘야할 것이다. 이것을 위해 run() 함수를 추가해보자:

angular
  .module('hello', [ 'ngRoute', 'auth', 'home', 'navigation' ])
  .config(
    ...
  }).run(function(auth) {

    auth.init('/', '/login', '/logout');

});

run() 함수는 "hello" 에 의존하는 어떠한 모듈에서 호출할 수 있다. 이 경우, auth 서비스를 주입하고 각각 홈페이지,로그인, 로그아웃 종단endpoint의 경로로 이것을 초기화해준다.

이제 당신은 index.html안에  다른 모듈뿐만 아니라 "auth" 모듈을 불러와야 한다.("auth"에 의존성이 있는 "login"모듈 전에):

...
<script src="js/auth/auth.js" type="text/javascript"></script>
...
<script src="js/hello.js" type="text/javascript"></script>
...

이렇게 하면 마침내 당신은 위에서 적어둔 세 함수 (authenticate()clear() 그리고 init())를 위한 코드를 작성할 수 있다. 여기 코드가 있다:

angular.module('auth', []).factory(
    'auth',

    function($http, $location) {

      var auth = {

        authenticated : false,

        loginPath : '/login',
        logoutPath : '/logout',
        homePath : '/',

        authenticate : function(credentials, callback) {

          var headers = credentials && credentials.username ? {
            authorization : "Basic "
                + btoa(credentials.username + ":"
                    + credentials.password)
          } : {};

          $http.get('user', {
            headers : headers
          }).success(function(data) {
            if (data.name) {
              auth.authenticated = true;
            } else {
              auth.authenticated = false;
            }
            $location.path(auth.homePath);
            callback && callback(auth.authenticated);
          }).error(function() {
            auth.authenticated = false;
            callback && callback(false);
          });

        },

        clear : function() { ... },

        init : function(homePath, loginPath, logoutPath) { ... }

      };

      return auth;

    });

 (예를 들어 "navigation" 컨트롤러엣 이미 주입된auth 서비스를 위해 "auth" 모듈은 팩토리를 생성한다. 이 팩토리는 단지 (auth) 객체를 돌려주는 함수이고, 이 객체는 3개의 함수를 가져야하고 플래그는 위에 우리가 예상한대로다. 위에서 우리가 "navigation" 컨트롤러안의 예전것과 대체로 같은 authenticate() 함수의 구현을 보여주었다. 이는 백앤드 리소스 "/user"를 호출하여 authenticated 플래그를 설정한다 그리고 그 플래그의 값에 따라 선택적인 콜백을 호출한다. 만일 성공적이면, $location서비스 (바로 아래서 이것을 다룬다)를 사용하여 사용자를 homePath 로 보낸다.

여기 당신이 "auth" 모듈에서 하드코드하는 것을 원하지 않는 다양한 경로들을 설정하는 init() 함수의 기본 뼈대 구현이 있다:

init : function(homePath, loginPath, logoutPath) {
  auth.homePath = homePath;
  auth.loginPath = loginPath;
  auth.logoutPath = logoutPath;
}

다음은 clear() 함수 구현이다 더 간단하다:

clear : function() {
  auth.authenticated = false;
  $location.path(auth.loginPath);
  $http.post(auth.logoutPath, {});
}

이는 authenticated 플래그를 설정해지하고 사용자를 로그인페이지로 되돌려보낸다. 그다음 HTTP POST를 로그아웃 경로로 보낸다. POST 호출은 우리가 여전히 "단일" 어플리케이션으로부터 CSRF 보호기능을 가지고 있기 때문에 성공할 것이다. 만일 403 메세지를 본다면 에러메세지와 서버로그를 확인하고 그다음 보내진 XSRF 쿠키를 확인하는 필터를 확인해보자

거의 마지막 수정은 사용자가 인증하지 않았을경우 "로그아웃" 링크를 숨기기위한 index.html이다:

<html>
...
<body ng-app="hello" ng-cloak class="ng-cloak">
  <div ng-controller="navigation" class="container">
    <ul class="nav nav-pills" role="tablist">
          ...
      <li ng-show="authenticated()"><a href="" ng-click="logout()">logout</a></li>
    </ul>
  </div>
...
</html>

"navigation" 컨트롤러는 "auth" 서비스를 얻기 위해서 그리고  $rootScope에 있지않은 플래그의 값을 찾기 위해서 당신은 그냥 authenticated플래그를 바꿔주고 authenticated()함수를 호출하면 된다.

로그인 페이지로 리다이렉션하기 Redirecting to the Login Page

이제껏 우리가 홈페이지를 구현했던 방법은, (그냥 로그인함으로서) 사용자가 인증받았을 때 보여주는 약간의 컨텐트를 가지고 있는 것이다. 어떤 어플리케이션들은 이 방식으로 동작하고 어떤것은 그렇지않다. 어떤 어플은 다른 사용자 경험을 제공해주어, 사용자가 인증받을 때까지 로그인페이지이외에 아무것도 볼수 없다. 우리도 어떻게하면 어플리케이션에 이 패턴을 가지도록 바꿔보자.

로그인페이지와 함께 모든 컨텐트를 숨기는 건 전통적으로 많은 것에 영향을 미치는 중요한 관심사다:당신은 UI 모듈에 위치한 로그인 페이지를 보여주기 위한 어떠한 로직도 원하지 않는다 ( 이것은 어디서나 중복적어서 코드를 더묵 만들기 힘들게 하고 유지보수가 더 어려워진다). 스프링 시큐리티는 서버에서 이런 많은 것 영향을 미치는 모든 관심사에 대한 것이다. Filters 과 AOP 인터셉터의 정점에서 빌드되었기 때문이다. 불행하게도 이것은 이 단일 페이지 어플리케이션에서는 크게 도움이 되지않는다. 하지만 앵귤러는 우리가 원하는 이러한 패턴의 구현을 쉽게 만들어줄 몇가지 기능을 가지고 있다. 이 기능의 도움을 받아 우리는 "라우트 수정route changes"를 위한 리스너를 설치할 수 있다. 따라서 매번 사용자가 새 라우트로 이동된다 (예를 들어 메뉴바 또는 어떠한 것을 클릭할때), 또는 페이지가 처음으로 로드될때, 당신은 라우스틀 조사하여 필요시 바꿀 수 있다.

리스너를 인스톨하려면, 우리는 auth.init()함수에 추가적인 약간의 코드를 써줘야한다.("hello" 모듈이 불러질때, 동작하도록 이미 준비되었으므로):

angular.module('auth', []).factory(
    'auth',

    function($rootScope, $http, $location) {

      var auth = {

        ...

        init : function(homePath, loginPath, logoutPath) {
          ...
          $rootScope.$on('$routeChangeStart', function() {
            enter();
          });
        }

      };

      return auth;

    });

새로운 enter()함수에 할당된 간단한 리스너를 등록했다. 이제 당신은 "auth" 모듈 팩토리 함수(팩토리 객체 자체에 접근할 수 있는)에 이것을 구현해줘야한다: 

enter = function() { if ($location.path() != auth.loginPath) { auth.path = $location.path(); if (!auth.authenticated) { $location.path(auth.loginPath); } } }

로직은 간단하다: 만일 경로가 로그인 페이지가 아닌 다른 어떠한 경로로 바뀌면 경로값을 기록한다. 그뒤 만일 사용자가 인증되있지않으면 로그인페이지로 간다. 우리가 경로값을 저장하는 이유는 그럼으로서 우리가 성공적인 인증후 이 값으로 돌아갈 수 있기 때문이다. (스프링 시큐리티는 서버쪽의 이 기능을 가지고 있으며 이것은 사용자들에게 아주 멋진 기능이다). 성공 핸들러에 약간의 코드를 추가함으로서 authenticate() 함수안에서 이것을 할 수 있다:

authenticate : function(credentials, callback) { ... $http.get('user', { headers : headers }).success(function(data) { ... $location.path(auth.path==auth.loginPath ? auth.homePath : auth.path); }).error(...); },

인증에 성공하면, 우리는그냥 홈페이지나 가장 최근에 선택했던 경로(로그인 페이지가 아니라면)로 이동하게 설정하면 된다.

마지막 수정할 것중 하나는 사용자 경험을 더욱 획일적으로 만드는 것이다: 우리는 어플리케이션이 처음 시작할때 홈페이지 대신 로그인 페이지를 보여주려고 한다. 당신은 이미 authenticate() 함수안에 (로그인페이지로 리다이렉트하는) 로직을 가지고 있으며, 따라서 당신이 필요한 것은 (사용자가 이미 쿠키를 가지고 있지 않으면 실패하게 되는) 텅빈 credential을 가지고 인증하기 위해 init() 함수에 다음과 같이 약간의 코드를 넣어주는게 전부다:

init : function(homePath, loginPath, logoutPath) {
  ...
  auth.authenticate({}, function(authenticated) {
    if (authenticated) {
      $location.path(auth.path);
    }
  });
  ...
}

auth.path 가 $location.path()을 가지고 초기화 했다면, 사용자가 브라우저에 명시적으로 라우트를 타입해주었을 때도 동작할 것이다 (예를 들어 홈페이지를 먼저 불러오기 싫을 때):

(IDE에서 main() 메소드를 통해 또는 커맨드라인에서 mvn spring-boot:run를 사용하여) 어플리케이션을 시작하고 http://localhost:8080를 방문하고 결과를 확인해보자:

기억하기:  쿠키와 HTTP Basic credential에 대한 브라우저 캐시를 삭제해야한다. 크롬에서 새 incognito 창을 여는게 최선의 방법이다.

결 론 Conclusion

이번 섹션에서 우리는 (이 튜토리얼의 두번째 섹션 의 어플리케이션을 가지고 시작하여) 어떻게 하면 앵귤러 어플리케이션을 모듈화하는지 살펴봤다. 어떻게 로그인 페이지로 리다이렉트하는지, 어떻게 사용자에 의해 쉽게 북마크하고 타입할 수 있게 "자연스럽게" 라우트를 사용하는지. 우리는 이 튜토리얼의 마지막 몇 섹션에서 클라이언트 코드에 좀 더 집중했었고 우리가 섹션III-VI. 에서 만든 분산 아키텍쳐를 잠시 파보았다. 이것이 여기에서 바꾼 것들이 다른 어플리케이션에서 적용할 수 없다는 의미가 아니다. (실제로 이것은 꽤나 사소한것이다) - 단신 우리가 클라이언트 쪽에서 하는 일을 배우는 도안 서버쪽 코드를 간소화했을 뿐이다. 서버사이드 몇몇 기능을 우리는 간단하게 사용했었고 의논하였다 (예를 들면 스프링 MVC에서 "자연스러운" 라우트를 가능하게 하기 위해 뷰를 "forward"했었다) 이렇게 우리는 앵귤러와 스프링을 함께 동작시키자는 주제를 계속 이어왔고 이곳 저곳에 작은 수정을 함으로서 매우 잘 이루어냈다.


반응형

반응형

스프링 시큐리티와 앵귤러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)


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


다중 UI 어플리케이션과 게이트웨이 Multiple UI Applications and a Gateway

이 섹션에서 우리는 어떻게 스프링 시큐티리와 앵귤러JS로 단일 페이지 어플리케이션을 만드는지 계속 얘기해볼 것이다. 이제 두번째 섹션과 네번째 섹션에서 만들었던 시스템의 기능을 섞기위해 어떻게 스프링 클라우드와 스프링 세션을 함께 사용하는지 보여줄 것이다. 그리고 서로 다른 책임을 가지는 3개의 단일 페이지 어플리케이션을 만들어볼것이다. (네번째 섹션과 같은) 게이트웨이를 만드는 목표는 API리소스를 사용하는 것뿐만 아니라 백엔드로부터 UI를 불러오는데 사용된다. 우리는 백엔드에 인증을 통과하기 위해 게이트웨이를 사용함으로서 두번째 섹션의 토큰 논쟁을 간소화할 수 있다. 그럼 다음 우리는 게이트웨이에서 인증과 식별을 제어하고 있는 동안 백엔드에서 로컬 접근 결정 입자를 어떻게 만드는가 보여주기위해 시스템을 확장할 것이다. 이것은 일반적으로 분산 시스템을 만드는 수많은 혜택을 가진 강력한 모델이다:

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

타겟 아키텍쳐 Target Architecture

우리가 이제부터 만드려는 시스템의 기본 그림이다:

Components of the System

이 시리즈의 다른 샘플 어플리케이션과 같이 UI(HTML과 자바스크립트)와 리소스 서버를 가지고 있다. 네번째 섹션의 샘플같이 게이트웨이를 가지고 있으나 여기에선 UI의 일부가 아닌 별도로 분리되었다. UI는 효율적으로 백엔드의 일부가 되었다. 이것은 우리에게 재설정과 재구현의 기능에 더 많은 선택을 가져다 준다. 또한 우리가 앞으로 볼수 있듯 더 많은 혜택을 가져다 줄것이다.

브라우저의 모든 요청이 게이트웨이로 간다. 이것은 백엔드의 아키텍쳐에 대해 알 필요가 없다(기본적로 백엔드에 대해 아무것도 모른다.) 브라우저가 이 게이웨이에서 하는 일중 하나는 인증이다. 예를 들어 두번째 섹션과 같이 사용자명과 패스워드를 보내고 쿠키를 돌려받는다. 이어지는 요청에서 자동적으로 쿠키가 들어있고 게이트웨이는 이것으로 백엔드를 통과한다. 쿠키를 통과시키기위해 클라이언트에서 어떠한 코드도 필요하지않다. 백엔드는 쿠키를 사용하여 인증을 하고 모든 컴포넌트가 해당 사용자에 대한 같은 정보를 공유하는 세션을 공유하기 때문이다. 게이트웨이에서 쿠키가 억세스 토큰으로 변환되었던 다섯번째 섹션과는 반대로, 엑세스 토큰은 모든 백엔드 컴포넌트에 의해 각각 독립적으로 디코드되어져야 한다.

네번째 섹션에서 처럼, 게이트웨이는 클라이언트와 서버간의 연동을 간소화한다. 이는 보안을 다루는 작고 잘 정의된 표면으로 표현된다. 예를 들면, 우리는 Cross Origin Resource Sharing를 걱정할 필요가 없다. 이는 아주 쉽게 잘못될 수 있기 때문에 우리에게 안도감을 줄 것이다.

완전한 프로젝트의 소스코드는 이곳 Github에서 받을 수 있다. 따라서 그냥 프로젝트를 클론하여 당신이 원하는 곳에서 직접 돌려볼 수 있다. 이 시스템의 마지막 상태에 ("double-admin"이라는) 추가적인 컴포넌트가 있지만 지금은 당장 무시하도록하자 

백엔드 만들기 Building the Backend

이 아키텍쳐에서 백앤드는 세번째 섹션에서 만들었던 이례적으로 실제 로그인페이지가 필요하지 않았던 스프링 세션 샘플과 매우 유사하다. 우리가 여기서 원하는 것을 얻는 가장 쉬운 방법은 아마도 첫번째 섹션의 "기초" 샘플에서 UI를 떼어내고, 세번째 섹션으로부터의 "리소스"서버를 복사하는 것이다.  "기본" UI를 얻기위해 몇가지 의존성을 추가해주면 된다.(세번째섹션에서 스프링 세션을 처음 사용했을 때와 같이):

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>

그리고 메인 어플리케이션 클래스에 @EnableRedisHttpSession 어노테이션을 추가하자:

UiApplication.java
@SpringBootApplication
@EnableRedisHttpSession
public class UiApplication {

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

}

이제 UI가 있지만 "/resource" endpoint는 필요없다. 이것을 마치면, 당신이 매우 간단한 ("basic"샘플과 동일한) 앵귤러 어플리케이션을 가지게 되어 이것의 행위의 이유나 테스트를 간소화할 수 있다.

마지막으로 우리는 이 서버가 백엔드로서 동작하길 원하므로 application.properties에 기본값이 아닌 포트값을 설정할 것이다.

application.properties

server.port: 8081
security.sessions: NEVER

만일 위의 설정이application.properties의 전체 컨텐트라면 어플리케이션은 보호될것이고 (시작시 로그레벨 INFO로 콘솔에 표시되는) 랜덤 패스워드를 가지는 "user"라 불리는 사용자의 접근이 허용된다. "security.sessions" 설정은 스프링 시큐리티가 인증 토큰으로 쿠키를 받아들인다는 의미로 만일 이미 존재하지 않는다면 그들을 만들지 않을 것이다.

리소스 서버 The Resource Server

리소스 서버는 우리가 이미 만들어놓은 샘플들중하나로 쉽게 만들수 있다. 두번째 섹션의 "spring-session" 리소스 서버와 동일하다: 단지 "/resource" endpoint과 분산 세션 데이터를 얻기 위한 @EnableRedisHttpSession. 우리는 이 서버를 기본 포트값이 아닌 특정 값을 설정해주어 세션에서 인증을 찾아볼 때 쓸 수 있길 원한다. 이것을 위해서 application.properties에 다음과 같이 설정해주자:

application.properties
server.port: 9000
security.sessions: NEVER

 여기 github에 필요시 둘러볼 수 있는 완전한 샘플이 있다.

게이트웨이 The Gateway

(작동하는 가장 간단한) 게이트웨이의 초기 구현을 위해, 우리는 아무것도 없는 스프링 부트 껍데기 어플리케이션을 가지고 @EnableZuulProxy 어노테이션을 추가한다. 우리가 첫번째 섹션에서 봤듯이 이것을 위해서 여러가지 방법이 있지만 프로젝트의 뼈대를 만들기 위해 Spring Initializr를 이용하는게 하나의 방법이다. Spring Cloud Initializr를 사용하면 똑같지만 스프링 클라우드 어플리케이션을 위해서 전보다 더 쉬워진다. 첫번째 섹션과 똑같은 커맨드라인을 순서대로 사용해보자:

$ mkdir gateway && cd gateway
$ curl https://cloud-start.spring.io/starter.tgz -d style=web \
  -d style=security -d style=cloud-zuul -d name=gateway \
  -d style=redis | tar -xzvf -

그 다음으로 당신이 선호하는 IDE에서 프로젝트를 (보통의 메이븐 자바 프로젝트로서) import하자 또는 커맨드라인에서 "mvn"을 써서 파일로 동작시켜도 된다. 바로 시작해보고 싶으면 github에 있는 버전을 이용하자 몇가지 추가적인 기능이 있지만 지금은 아직 필요없다.

아무것도 없는 빈 초기 어플리케이션으로 시작하여 우리는 스프링 세션 의존성을 추가하고 (위의 UI에서 처럼) 거기에  @EnableRedisHttpSession 어노테이션을 더하자:

GatewayApplication.java

@SpringBootApplication
@EnableRedisHttpSession
@EnableZuulProxy
public class GatewayApplication {

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

}

게이트웨이는 이제 동작할 준비가 되었다, 하지만 아직 우리의 백엔드 서비스에 대해 알지 못한다. application.yml에 다음을 설정하자 (만일 당신이 위의 것을 curl로 이미 했다면 application.properties 에서 이름을 바꿔주자):

application.yml
zuul:
  routes:
    ui:
      url: http://localhost:8081
    resource:
      url: http://localhost:9000
security:
  user:
    password:
      password
  sessions: ALWAYS

프록시에는 2개의 라우트가 있다, 하나는 각각 UI와 리소스 서버를 위한 것으로 우리는 기본 패스워드와 세션 저장 전략session persistence strategy을 설정하였다. (스프링 시큐리티에 항상 인증을 위해 세션을 만들라고 알려줌으로서). 이 마지막 단계는 우리가 인증하기를 원하기 때문에 세션이 게이트웨이에서 관리되기 때문에 중요하다. 

서버 동작시키기 Up and Running

우리는 이제 3개의 포트에서 동작하는 3개의 컴포넌트를 갖췄다. 브라우저의 http://localhost:8080/ui/ 를 열면 당신은 이제 HTTP Basic challenge를 받아야 하며 "user/password"로 (게이트웨이에 당신의 credential로서) 인증할 수 있다. 그리고 일단 인증받으면, 리소스 서버에 프록시를 거친 백앤드 호출을 경유하여, UI에서 greeting을 볼 수 있게 된다.

당신이 개발자툴을 사용중이라면 (보통 크롬에서 F12로 열수 있고 파이어폭스는 플러그인을 설치해야한다) 브라우저와 백엔드간의 연동과 당신의 브라우저에서 확인할 수 있다.  여기 정리해두었다:

VerbPathStatusResponse

GET

/ui/

401

Browser prompts for authentication

GET

/ui/

200

index.html

GET

/ui/css/angular-bootstrap.css

200

Twitter bootstrap CSS

GET

/ui/js/angular-bootstrap.js

200

Bootstrap and Angular JS

GET

/ui/js/hello.js

200

Application logic

GET

/ui/user

200

authentication

GET

/resource/

200

JSON greeting

브라우저가 홈페이지를 단일연동으로서 불러온다고 취급하기 때문에 401을 볼 수 없을 것이다. 모든 요청은 프록시되었다. ( 관리를 위한 Actuator endpoint상에는 아직 게이트웨이에 컨텐트가 없다)

만세, 이제 동작한다!. 당신은 두개의 백엔드 서버를 가졌다. 하나는 독립적인 수용능력과 고립테스트가 가능한 UI인데 이들은 당신이 인증을 위해 설정해둔 제어를 한 보안이 설정된 게이트웨이와 함께 연결되었다. 만일 백엔드가 브라우저에서 접근가능하지 않다해도 상관없다 (사실 아마도 당신이 물리적인 보안을 더욱 제어할 수 있는 점에서 장점이 될 것이다)

로그인 폼 추가하기 Adding a Login Form

첫번째 섹션의 "기본" 샘플에서 처럼, 우리는 이제 예를 들면 두번째 섹션으로부터 복사함으로서, 게이트웨이에 로그인폼을 추가할 수 있다. 우리가 이것을 할때 또한 기본 네비게이션 엘리면트를 게이트웨이에 추가해줄 수 있다. 그래서 사용자가 프록시의 UI 백엔드 경로를 알 수 없게 한다. 게이트웨이안에 "단일"UI로부터의 정적인 에셋assets을 먼저 복사하고 (어딘가의 <body/>안에 위치한) 홈페이지안의 로그인 폼을 삽입하고 메세지 랜더링을 삭제하자:

index.html
<body ng-app="hello" ng-controller="navigation" ng-cloak
	class="ng-cloak">
  ...
  <div class="container" ng-show="!authenticated">
    <form role="form" ng-submit="login()">
      <div class="form-group">
        <label for="username">Username:</label> <input type="text"
          class="form-control" id="username" name="username"
          ng-model="credentials.username" />
      </div>
      <div class="form-group">
        <label for="password">Password:</label> <input type="password"
          class="form-control" id="password" name="password"
          ng-model="credentials.password" />
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </div>
</body>

메세지 랜더링 대신, 우리는 더 멋진 커다란 네비게이션 버튼을 만들 것이다:

index.html
<div class="container" ng-show="authenticated">
  <a class="btn btn-primary" href="/ui/">Go To User Interface</a>
</div>

github의 샘플을 보면, "로그아웃" 버튼을 가진 작은 네비게이션 바 또한 볼 수 있을 것이다. 여기 로그인 폼의 스크린샷을 보자:

Login Page

로그인 폼을 지원하려면, 우리는 <form/>안에 선언한  login() 함수를 구현한 navigation" 컨트롤러를 가진 자바스크립트가 필요하다. 그리고 authenticated 플래그를 설정해서 홈페이지가 사용자가 인증을 받았는지 안받았는지에 따라 다르게 랜더할 것이다. 예를 들어:

gateway.js
angular.module('gateway', []).controller('navigation',
function($scope, $http) {

  ...

  authenticate();

  $scope.credentials = {};

$scope.login = function() {
    authenticate($scope.credentials, function() {
      if ($scope.authenticated) {
        console.log("Login succeeded")
        $scope.error = false;
        $scope.authenticated = true;
      } else {
        console.log("Login failed")
        $scope.error = true;
        $scope.authenticated = false;
      }
    })
  };

}

authenticate() 함수의 구현은 두번째 섹션과 유사하다:

gateway.js
var authenticate = function(credentials, callback) {

  var headers = credentials ? {
    authorization : "Basic "
        + btoa(credentials.username + ":"
            + credentials.password)
  } : {};

  $http.get('user', {
    headers : headers
  }).success(function(data) {
    if (data.name) {
      $scope.authenticated = true;
    } else {
      $scope.authenticated = false;
    }
    callback && callback();
  }).error(function() {
    $scope.authenticated = false;
    callback && callback();
  });

}

authenticated 플래그럴 저장하기 위해 $scope을 사용할 것이다. 이 간단한 어플리케이션은 오직 하나의 컨트롤러만 가지고 있기 때문이다.

이제 이 향상된 게이트웨이를 동작해보면 UI를 위해 URL을 기억하는 대신, 우리는 그냥 홈페이리를 부른 다음 다음의 링크를 통하면된다. 인증된 사용자를 위한 홈페이지를 보자:

Home Page

백엔드의 접근결정 단위 Granular Access Decisions in the Backend

이제 우리의 어플리케이션이 기능적으로 두번째 섹션과 네번째 섹션에 있는 것과 유사해졌지만, 추가적인 전담의 게이트웨이를 가지고 있다. 이 추가 레이어의 장점은 아직 명확하진 않지만 우리가 시스템을 확장할때 좀 더 강점을 가질 것이다. 우리가 서로 다른 백엔드 UI에 노출하기하기 위해 이 게이트웨이를 사용한다고 가정하보자, 메인UI에서 컨텐트를 "감시감독하기 위한" 사용자를 위해, 그리고 특별한 롤을 가진 사용자에만 이 기능의 접근을 제한하고자 원할때 우리는 프록시뒤에 "어드민" 어플리케이션을 추가 할것이다. 그리고 그 시스템은 다음과 같다:

Components of the System

application.yml에 다음과 같이 새 컴포넌트(어드민)와 게이트웨이에 새 라우트가 있다.

application.yml

zuul:
  routes:
    ui:
      url: http://localhost:8081
    admin:
      url: http://localhost:8082
    resource:
      url: http://localhost:9000

"USER" 롤을 가진 사용자들에 이용가능한 지금의 UI는 위의 다이아그램의 게이트웨이 박스(녹색글씨)에 표시되어있다. "ADMIN"롤의 사용자는 어드민 어플리케이션으로 가야한다. 이 "ADMIN"을 위한 접근 결정은WebSecurityConfigurerAdapter설정을 통해서 게이트웨이에서 적용하거나 어드민 어플리케이션 자체에 적용할 수 있다.(밑에서 어떻게 하는지 살펴볼것이다)

뿐만 아니라 어드민 어플리케이션에서 "읽기권한READER"과 "쓰기권한WRITER" 를 구분해줘야한다면, 우리는 주요 관리자가 사용자의 역할을 조정할 수 있게 해주어야 한다. 이것이 해당 역할이 백엔드서버가 어디있는지 알 수 있는지에 대한 접근결정단위(granular access decision)이다. 게이트웨이에서 우리는 단지 사용자 계정에 역할이 필요한지 그리고 이 정보가 이용가능한지를 확실히 해주기만 하면 된다. 그러나 게이트웨이는 이것을 어떻게 해셕하는지에 대해 알필요가 없다. 게이스웨이에서 우리는 샘플어플리케이션에 스스로 사용자계정을 갖추도록 만들어준다: 

SecurityConfiguration.class

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Autowired
  public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
      .withUser("user").password("password").roles("USER")
    .and()
      .withUser("admin").password("admin").roles("USER", "ADMIN", "READER", "WRITER")
    .and()
      .withUser("audit").password("audit").roles("USER", "ADMIN", "READER");
  }

}

"어드민" 사용자는 3가지 새로운 롤을 가질수 있다 ("관리자ADMIN","읽기권한READER",'쓰기권한WRITER") 그리고 또한  "관리자ADMIN"권한을 가지지만 "쓰기권한WRITER"은 없는 청강audit사용자를 추가했다.

여담: 제품화단계의 시스템에서 사용자 계정 데이터는 백앤드 데이터베이스에서 관리(대게, 디렉토리 서비스)되지 스프링 설정에 하드코드되지 않는다. 그러한 데이터베이스 접속하는 예제 어플리케이션은 인터넷에서 쉽게 찾을 수 있다 예를 들면 스프링 시큐리티 예제.


접근 권한은 어드민 어플리케이션으로 옮겨졌다. (이 백엔드의 어디에서나 요구되는) "관리자admin"역할을 위해, 스프링 시큐리티에 다음과같이해보자:

SecurityConfiguration.java
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
  protected void configure(HttpSecurity http) throws Exception {
    http
    ...
      .authorizeRequests()
        .antMatchers("/index.html", "/login", "/").permitAll()
        .antMatchers("/admin/**").hasRole("ADMIN")
        .anyRequest().authenticated()
    ...
  }

}

"읽기권한reader"와 "쓰기권한writer"를 위해 어플리케이션 자체를 나누었다. 어플리케이션이 자바스크립트로 구현되었으므로 우리는 접근결정을 만들어줘야한다. 이것을 하는 하나의 방법은 이것을 내장한 뷰를 가진 홈페이지를 만드는 것이다:

index.html
<div class="container">
  <h1>Admin</h1>
  <div ng-show="authenticated" ng-include="template"></div>
  <div ng-show="!authenticated" ng-include="'unauthenticated.html'"></div>
</div>

앵귤러JS는 하나의 표현식으로 "ng-include"속성값을 검토하여 템플릿을 불러올것이다.

 더 복잡한 어플리케이션은 아마 자체로 모듈화한 메카니즘을 사용할 것이다. 예를 들어 이 시리즈의 거의 모든 다른 어플리케이션에 사용했던 $routeProvider 서비스와 같이.

 template 변수는 우리의 컨트롤러에서 초기화된다. 먼저 유틸리티 함수를 정의하자:

admin.js
var computeDefaultTemplate = function(user) {
  $scope.template = user && user.roles
      && user.roles.indexOf("ROLE_WRITER")>0 ? "write.html" : "read.html";
}

그 후 컨트롤러가 로드될때 유틸리티 함수를 사용하자:

admin.js
angular.module('admin', []).controller('home',

function($scope, $http) {

  $http.get('user').success(function(data) {
    if (data.name) {
      $scope.authenticated = true;
      $scope.user = data;
      computeDefaultTemplate(data);
    } else {
      $scope.authenticated = false;
    }
    $scope.error = null
  })
  ...

})

어플리케이션은 먼저 (이 시리즈에서 해왔던) 일반적인 "/user" endpoint를 살펴본다. 그다음 데이터를 추출하고 authenticated 플래그를 설정한다. 만일 사용자가 인증받았다면 사용자 데이터를 검토함으로서 템플릿을 산출한다.

백엔드에서 이 함수를 지원하기 위해, 우리는 endpoint가 필요하다. 예를 들면 메인 어플리케이션 클래스에서:

AdminApplication.java

@SpringBootApplication
@RestController
@EnableRedisHttpSession
public class AdminApplication {

  @RequestMapping("/user")
  public Map<String, Object> user(Principal user) {
    Map<String, Object> map = new LinkedHashMap<String, Object>();
    map.put("name", user.getName());
    map.put("roles", AuthorityUtils.authorityListToSet(((Authentication) user)
        .getAuthorities()));
    return map;
  }

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

}

"/user" endpoint에서 "ROLE_"접두사가 붙은 역할 이름을 받아옴으로서 우리는 인증의 종류를 구별할 수 있다.(이것은 스프링 시큐리티가 하는 일이다) 그러므로 "ROLE_" 접두사가 붙은 역할은 스프링 시큐리티 설정에서가 아니라 명확한 "역햘"의 메소드이름으로 작업에 초점을 맞추기위해 자바스크립트에서 필요하다.

왜 우리가 여기에 있지? Why are we Here?

이제 우리는 2개의 독립적인 UI와 백엔드 리소스 서버를 가진 작고 멋진 시스템을 갖추었다. 모든 것이 게이트웨이에서 똑같은 인증에 의해 보호되고있다. 게이트웨이가 마이크로 프록시(micro-proxy)로서 역할을 하고 있다는 사실은 백엔드 시큐리티에 대한 구현을 극도록 쉽게 만들어주었다. 그리고 그들은 자기들의 비지니스 관심사에만 집중할 수 있게 되었다. 스프링 세션의 사용은 또다시 수많은 번거로운 작업과 잠재적인 에러를 피할수 있게 해주었다.

백앤드가 선호하는 어떤 종류의 인증법을 독립적으로 가진다는 것(예를들어 당신이 물리적 주소와 로컬 credential의 셋트를 알고 있다면 UI로 직접 갈수 있다)은 매우 강력한 기능이다. 게이트웨이는 사용자를 인증하고 백엔드의 접근 규약을 만족시키는 사용자에 메타데이터를 할당할 수 있는한 관련이 없는 제약사항의 집합을 강제한다.  이는 백엔드 컴포넌트를 독립적으로 테스트하고 개발할수 있게 해주는 훌륭한 디자인이다. 만일 우리가 필요하다면 게이트웨이의 인증을 위해 외부 OAuth2 서버로 회귀할 수 있다 (다섯번째 섹션처럼 또는 완전히 다른 어떤한 것도 가능하다). 이경우도 백엔드는 손댈 필요가 없다.

이 아키텍쳐(인증을 제어하는 단일의 게이트웨이, 모든 컴포넌트에서 공유하는 세션 토) 의 보너스 기능은 "단일 로그아웃signgle logout"이다. 다섯번째 섹션에서 구현하기 복잡했던 기능으로 여기선 그냥 된다. 더 정확하게, 단일 로그아웃에 대한 사용자 경험에 대한 하나의 특별한 접근법이 우리의 완성된 시스템에 자동적으로 이용가능하다: 각각 개별의 UI가 같은 방식으로 "로그아웃"을 구현했다는 가정하에, 만일 사용자가 UI들중 아무 한군데에서( 게이트웨이, UI백엔드 또는 어드민 백엔드) 로그아웃을 하면, 그 사용자는 모든 곳에서 로그아웃이 된다(세션 무효화함으로서)

감사의 말: 이 시리즈를 만드는데 도움을 준 모든분께 감사드리고 싶다. 특히 각각의 섹션과 소스코드를 주의깊게 리뷰를 해주고, 내가 잘 알고 있다고 생각한 부분에서 조차 알지못했던 몇가지 트릭을 알게 해준 Rob Winch 와 Thorsten Spaeth.에게 감사하다. 첫번째 섹션이 발행된 이후 많은 수정을 하지 못했지만 다른 섹션들은 읽은 사람들의 통찰력과 답글들과 진화를 거듭했다. 그러므로 포럼에 참여하여 수고를 아끼지않아준 사람들과 이 섹션을 읽어준 사람들께도 감사하다.


반응형

반응형

스프링 시큐리티와 앵귤러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)


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


OAuth2를 활용한 싱글사인온 Single Sign On with OAuth2

이 섹션에서 우리는 어떻게 스프링 시큐티리와 앵귤러JS로 단일 페이지 어플리케이션을 만드는지 계속 얘기해볼 것이다. 이제 우리는 API게이트웨이가 백엔드 리소스에 OAuth2 토큰 인증과 싱글사인온을 지원하도록 스프링 클라우드와 함께 스프링 시큐리티 OAuth를 어떻게 사용하는지 보여줄것이다. 이 글을 시리즈의 5번째 섹션으로 당신이 어플리케이션의 기본구성을 이해하거나 처음부터 빌드해보려면 첫번째 섹션부터 읽도록 하자 또는 그냥 Github의 소스코드로 바로 가도 된다. 이전 섹션에서 우리는 UI서버에 내장된 API 게이트웨이를 구현하기 위해 스프링 클라우드를, 백엔드 리소스의 인증을 위해 스프링 세션을 사용하여 배포가능한 작은 어플리케이션을 만들었다. 이번 섹션에서 우리의 UI서버를 인증서버로 잠재적으로 많은 싱글사인온 어플리케이션의 하나로 만들 것이다. 이것은 엔터프라이즈나 쇼셜 스타트업에서나 요즘 수많은 어플리케이션에서 흔한 패턴이다. 우리는 OAuth2 서버를 인증자로서 사용할 것이다. 또한 백엔드 서버를 위한 토큰 인증을 위해 사용할 것이다. 스프링 클라우드는 자동으로 우리의 백엔드에서 억세스 토큰access token을 중계하여 우리로 하여금 UI와 리소스 서버 양쪽의 구현을 더욱 간단하게 만들어준다.

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

OAuth2 인증서버 만들기 Creating an OAuth2 Authorization Server

인증과 토근관리를 처리하는 새 서버를 만드는 첫 걸음은, Spring Boot Initializr로 시작하는 첫번째 섹션의 절차를 따르는 것이다. 유닉스계열 시스템에서는 다음과 같이 curl을 사용할 수 있다: 

$ curl https://start.spring.io/starter.tgz -d style=web \
-d style=security -d name=authserver | tar -xzvf -

그 후에 당신이 선호하는 IDE에서 프로젝트를 import하면 된다. 또는 커맨드라인에서 "mvn" 명령어를 써서 파일로 동작해도 된다.

OAuth2 의존성 추가하기 Adding the OAuth2 Dependencies

Spring OAuth 의존성을 추가해줘야한다 POM에 다음과 같이 추가하자:

pom.xml
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
  <version>2.0.5.RELEASE</version>
</dependency>

인증서버의 구현은 매우 쉽다. 최소한의 구현은 다음과 같다:

AuthserverApplication.java

@SpringBootApplication
public class AuthserverApplication extends WebMvcConfigurerAdapter {

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

  @Configuration
  @EnableAuthorizationServer
  protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
      endpoints.authenticationManager(authenticationManager);
    }

@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      clients.inMemory()
          .withClient("acme")
          .secret("acmesecret")
          .authorizedGrantTypes("authorization_code", "refresh_token",
              "password").scopes("openid");
    }

}

다음의 2가지 일만 해주면 된다. (@EnableAuthorizationServer를 추가한 후):

    • 클라이언트 "acme"를 secret과 "authorization_code"를 포함하는 인증타입을 가지도록 등록한다. 

    • 스프링 부트 자동설정으로 부터  기본 AuthenticationManager를 주입하고 이것을 OAuth2 endpoint와 연동한다.

이제 테스트를 위해 패스워드를 명시하고 9999포트에서 돌려보자:

application.properties
server.port=9999
security.user.password=password
server.contextPath=/uaa

또한 기본값("/")을 사용하지않게 하기 위해 context path를 명시한다. 이것은 이렇게 하지않으면 로컬호스트의 다른 서버로부터 얻은 쿠키를 잘못된 서버로 보낼 수 있기 때문이다. 다음과 같이 서버가 동작하는 것을 확인할 수 있다:

$ mvn spring-boot:run

또는 당신의 IDE에서 main() 메소드에서 시작하자.

인증서버 테스트하기 Testing the Authorization Server

스프링 기본 시큐리티 설정을 사용하는 우리 서버는 HTTP Basic 인증에 의해 보호되는 첫번째 섹션의 서버와 같다. authorization code token grant 를 초기화 하기위해, 당신은 인증endpoint를 거쳐야한다. 이를테면 http://localhost:9999/uaa/oauth/authorize? response_type=code&client_id=acme&redirect_uri=http://example.com. 일단 인증을 받았으면 인증코드가 첨부되어 example.com으로 리다이렉트될 것이다. 예를 들면 http://example.com/?code=jYWioI.

 샘플 어플리케이션에서 등록을 위한 리다이렉트없이 "acme"클라이언트를 만든 목적은, 우리가 example.com의 리다이렉트를 얻을 수 있게 하기위함이다. 제품화수준의 어플리케이션에셔는 항상 리다이렉트를 등록해줘야한다 (HTTPS를 사용해서)

토큰 endpoint의 "acme"클라이언트 credential을 사용하는 억세스토큰을 위해 코드를 맞교환한다:

$ curl acme:acmesecret@localhost:9999/uaa/oauth/token  \
-d grant_type=authorization_code -d client_id=acme     \
-d redirect_uri=http://example.com -d code=jYWioI
{"access_token":"2219199c-966e-4466-8b7e-12bb9038c9bb","token_type":"bearer","refresh_token":"d193caf4-5643-4988-9a4a-1c03c9d657aa","expires_in":43199,"scope":"openid"}

억세스 토큰은 서버의 메모리상주 토큰 저장소에 의해 만들어진 UUID ("2219199c…")이다. 우리는 또한 현재의 토큰의 유효기간이 끝날때 새로운 억세스토큰을 받는데 사용하는 리프레시 토큰을 받는다.

 우리가 "acme" 클라이언트에 "password" 승인을 허용하였으므로, curl과 인증코드 대신 user credential을 사용하여 토큰 endpoint로부터 직접 토큰을 얻을 수 있다. 이는 테스트용으로는 유용하지만 브라우저 기반의 클라이언트에서 적합하지않다.

위의 링크를 따라가보면, 스프링 OAuth가 제공하는 whitelabel UI를 볼 수 있다. 우리가 이것을 사용하려면 self-contained서버를 위해 곧 두번째 섹션에서 했던 것을 다시 보강해보자 

리소스 서버 바꾸기 Changing the Resource Server

네번째 섹션에서 우리의 리소스 서버는 인증을 위해 스프링 세션을 사용했다. 우리는 이것을 스프링 OAuth으로 교체할 것이다. 또한 스프링 세션과 레디스 의존성을 제거할 필요가 있다. 다음과 같이 바꿔보자:

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

위의 의존성을 아래로 바꾸자:

pom.xml
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
</dependency>

그 후 메인 어플리케이션 클래서에서 Filter 세션을 지우자, 이것을 스프링 클라우드 시큐리티의  @EnableOAuth2Resource 어노테이션으로 바꾼다:

ResourceApplication.groovy
@SpringBootApplication
@RestController
@EnableOAuth2Resource
class ResourceApplication {

  @RequestMapping('/')
  def home() {
    [id: UUID.randomUUID().toString(), content: 'Hello World']
  }

  static void main(String[] args) {
    SpringApplication.run ResourceApplication, args
  }
}

이 수정으로 어플리케이션은 HTTP Basic 대신 억세스 토큰을 검수하도록 준비가 되었다. 그러나 우리는 이 프로세스를 실제로 끝마치려면 설정을 수정해줘야한다. "application.properties"에 작은 외부 설정을 추가하여 리소스 서버가 사용자를 인증하고 토큰을 디코드할 수 있게 할 것이다:

application.properties

...
spring.oauth2.resource.userInfoUri: http://localhost:9999/uaa/user

이것은 서버가 "/user" endpoint에 접근하기위해 토큰을 사용하며, 인증 정보를 얻어내는데 사용할 것이라고 알려주는 것이다. (페이스북 API의  "/me" endpoint 와 유사하다). 이는 스프링 OAuth2의 ResourceServerTokenServices 인터페이스에 의해 표출됨으로서 리소스 서버가 토큰을 디코드하는 방법을 효율적으로 제공해준다.

어플리케이션을 시작하고 클라이언트의 커맨드라인에서 홈페이지를 쳐보자:

$ curl -v localhost:9000
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:9000
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
...
< WWW-Authenticate: Bearer realm="null", error="unauthorized", error_description="An Authentication object was not found in the SecurityContext"
< Content-Type: application/json;charset=UTF-8
{"error":"unauthorized","error_description":"An Authentication object was not found in the SecurityContext"}

bearer 토큰을 가르키는 "WWW-Authenticate" 헤더를 가지는 401 을 볼 수 있을 것이다.

userInfoUri는 리소스서버가 토큰을 디코드하는 방법을 연결할 뿐 아니라 사실, 최소 공통분모의 한 종류이다 (그리고 스펙의 일부가 아니다.) 그러나 종종 (페이스북, 클라우드 파운더리, Github과 같은) OAuth2 프로바이더에서 이용가능하다. 그리고 또 다른 선택을 사용할 수 있는데 예를 들면, (JWT와 같은 예로사용자는 토큰의 유저인증 자체를 인코드할 수 있다. 또는 공유된 백엔드 저정소를 사용할 수도 있다. 클라우드 파운더리에는 /token_info endpoint가 있어 사용자 정보 endpoint보다 더 자세한 정보를 제공해준다. 그러나 더 구체적인 인증을 요구한다. 또다른 (자연스러운) 옵션으로 trade-off와 다른 종유의 혜택을 제공할 수 있지만 이들에 대한 자세한 논의는 이 섹션의 범주에서 벗어난다.

사용자 endpoint 구현하기 Implementing the User Endpoint

인증서버에 다음의 endpoint를 간단히 추가해보자:

AuthserverApplication.java
@SpringBootApplication
@RestController
@EnableResourceServer
public class AuthserverApplication {

  @RequestMapping("/user")
  public Principal user(Principal user) {
    return user;
  }

  ...

}

두번째 섹션에서 했던데로 똑같이 @RequestMapping 를 추가했다. 또한 "/oauth/* endpoint를 제외한 인증서버의 모든 곳에 기본 보안을 적용하는 스프링 OAuth의 @EnableResourceServer 어노테이션도 추가했다.

이 endpoint를 가지고 우리는 greeting 리소스를 테스트할 수 있다. 그들 둘다 이제 인증서버에 생성한 bearer 토큰을 허용하기 때문이다:

$ TOKEN=2219199c-966e-4466-8b7e-12bb9038c9bb
$ curl -H "Authorization: Bearer $TOKEN" localhost:9000
{"id":"03af8be3-2fc3-4d75-acf7-c484d9cf32b1","content":"Hello World"}
$ curl -H "Authorization: Bearer $TOKEN" localhost:9999/uaa/user
{"details":...,"principal":{"username":"user",...},"name":"user"}

(당신이 스스로 동작을 확인하기 위해 당신의 인증서버로 부터 획득한 억세스 토큰의 값으로 대체하자)

UI 서버 The UI Server

이 어플리케이션의 마지막 부분으로, 우리는 인증서버로 위임하고 인증 파트를 추출하는 UI서버를 완성시켜야한다. 이것을 위해 리소스 서버로부터 우리는 먼저 스프링 세션과  레디스 의존성을 제거하고 그들을 스프링 OAuth로 대체해줘야한다.

이것이 끝나면, 우리는 세션 필터와 "/user" endpoint 또한 지워준다. (@EnableOAuth2Sso어노테이션을 사용하여) 인증서버로 리다이렉스하기 위해 어플리케이션을 설정하자:

UiApplication.java

@SpringBootApplication
@EnableZuulProxy
@EnableOAuth2Sso
public class UiApplication {

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

  @Configuration
  @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
  protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

네번째 섹션을 기억하자 @EnableZuulProxy의 혜택을 본 UI서버는 이제 API 게이트웨이로서 동작한다. 우리는 YAML에 라우트 매핑route mapping을 선언할 수 있다. 이제 "/user" endpoint는 인증서버로 프록시되어진다:

application.yml
zuul:
  routes:
    resource:
      path: /resource/**
      url: http://localhost:9000
    user:
      path: /user/**
      url: http://localhost:9999/uaa/user

마침내 우리는WebSecurityConfigurerAdapterOAuth2SsoConfigurerAdapter로 바꿔줘야 한다. 이제부터 @EnableOAuth2Sso에 의해 설정된 SSO filter chain이 기본값으로 바뀔것이다:

SecurityConfiguration.java

@Configuration
protected static class SecurityConfiguration extends OAuth2SsoConfigurerAdapter {

  @Override
  public void match(RequestMatchers matchers) {
    matchers.anyRequest();
  }

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/index.html", "/home.html", "/")
        .permitAll().anyRequest().authenticated().and().csrf()
        .csrfTokenRepository(csrfTokenRepository()).and()
        .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
  }

  ... // the csrf*() methods are the same as the old WebSecurityConfigurerAdapter
}

기본 클래스 이름과 상관없이 주요한 변경사항은 자기들 스스로의 메소드로 가게 하는 matcher이다. 이제 formLogin() 은 더이상 필요없다.

또한 @EnableOAuth2Sso 어노테이션을 위한 몇개의 의무적인 외부설정 프로퍼티가 있어 올바른 인증서버에 연결하고 인증할 수 있다. 이것을 application.yml에 넣어주자:

application.yml

spring:
  oauth2:
    sso:
      home:
        secure: false
        path: /,/**/*.html
    client:
      accessTokenUri: http://localhost:9999/uaa/oauth/token
      userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
      clientId: acme
      clientSecret: acmesecret
    resource:
      userInfoUri: http://localhost:9999/uaa/user

OAuth2 클라이언트 ("acme")와 인증서버 위치를 넣어주자. 또한 (리소스서버에 있는 것과 같은) userInfoUri가 있어 사용자가 UI 앱 자체에서 인증받을 수 있다. "home"관련 것들은 우리의 단일 페이지 어플리케이션에서 정적 리소스에 익명의 접근을 허용한다.

클라이언트에선 In the Client

UI어플리케이션의 프론트 엔드에 약간의 수정을 해주어 우리가 여전히 인증서버로 리다이트되도록 만들어줘야한다. 먼저 앵귤러 라우트로부터 "로그인" 링크를 바꿔줘야하는 네비게이션 바의 "index.html":

index.html
<div ng-controller="navigation" class="container">
  <ul class="nav nav-pills" role="tablist">
    ...
    <li><a href="#/login">login</a></li>
    ...
  </ul>
</div>

을 그냥 평범한 HTML링크로 바꿔주자:

index.html
<div ng-controller="navigation" class="container">
  <ul class="nav nav-pills" role="tablist">
    ...
    <li><a href="login">login</a></li>
    ...
  </ul>
</div>

"/login" endpoint는 스프링 시큐리티에 의해 처리된다. 만일 사용자가 인증받지 못하면 인증서버로 리다이렉트 될 것이다.

우리는 또한 "네비게이션" 컨트롤러의 login()함수와 앵귤러 설정에서 "/login" 라우트를 지울 것이다.

hello.js

angular.module('hello', [ 'ngRoute' ]).config(function($routeProvider) {

  $routeProvider.when('/', {
    templateUrl : 'home.html',
    controller : 'home'
  }).otherwise('/');

}). // ...
.controller('navigation',

function($rootScope, $scope, $http, $location, $route) {

  $http.get('user').success(function(data) {
    if (data.name) {
      $rootScope.authenticated = true;
    } else {
      $rootScope.authenticated = false;
    }
  }).error(function() {
    $rootScope.authenticated = false;
  });

  $scope.credentials = {};

  $scope.logout = function() {
    $http.post('logout', {}).success(function() {
      $rootScope.authenticated = false;
      $location.path("/");
    }).error(function(data) {
      $rootScope.authenticated = false;
    });
  }

});

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

이제 모든 서버를 다같이 동작시키자. 브라우저를 열고 http://localhost:8080에 방문하여 "로그인" 링크를 클릭하면 UI로부터 인증한 같은 토큰을 사용해서 OAuth2리소스 서버로부터 불러오는 greeting을 가지는 UI의 홈페이지로 리다이렉트 되기전에 ,인증(HTTP Basic 팝업)과 토큰 승인(Whitelable HTML)을 위해 인증서버로 리다이트 될것이다.

브라우저와 백엔드간의 연동은 브라우저에서 확인할 수 있다. 만일 당신이 개발자툴을 쓴다면 (보통 크롬에서 F12로 열수 있고 파이어폭스에서는 플러그인이 필요할 것이다) 여기 요약본을 보자:

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

/home.html

200

HTML partial for home page

GET

/user

302

Redirect to login page

GET

/login

302

Redirect to auth server

GET

(uaa)/oauth/authorize

401

(ignored)

GET

/resource

302

Redirect to login page

GET

/login

302

Redirect to auth server

GET

(uaa)/oauth/authorize

401

(ignored)

GET

/login

302

Redirect to auth server

GET

(uaa)/oauth/authorize

200

HTTP Basic auth happens here

POST

(uaa)/oauth/authorize

302

User approves grant, redirect to /login

GET

/login

302

Redirect to home page

GET

/user

200

(Proxied) JSON authenticated user

GET

/home.html

200

HTML partial for home page

GET

/resource

200

(Proxied) JSON greeting

(uaa) 접두사가 붙은 요청들은 인증서버로 간다. "ignored"로 마크된 응답들은 우리는 그들이 던져주는 데이터를 따로 처리하지 않지 때문에  XHR호출로 앵귤러가 받게되는 응답이다.  "/user"리소스의 경우 우리는 인증된 사용자를 요구한다. 첫번째 호출에 인증이 없기때문에 응답은 드랍된다.

UI의 "/trace" endpoint (마지막까지 스크롤을 내려라)에서 당신은 remote:true 설정으로  "/user"와 "/resource"의 프록시된 백엔드 요청을 볼 수 있을 것이다. 네번째 섹션에서 언급한대로 쿠키대신 bearer토큰이 인증에 사용된다. 스프링 클라우드 시큐리티는 우리를 위해 이것을 알아서 해준다: @EnableOAuth2Sso 와@EnableZuulProxy의 설정을 인식함으로서, (기본값으로) 우리가 토큰을 프록시된 백엔드에 중계하기 원한다고 이해한다.

 이전 섹션에서, 인증이 섞이는것을 막기위해 "/trace"를 다른 브라우저로 사용하라고 언급했다.

로그아웃 경험 The Logout Experience

로그아웃 링크를 클릭하면 홈페이지가 바뀌는 것을 볼 수 있다 (greeting이 더이상 보이지않는다). 따라서 사용자는 더이상 UI서버에 인증되어있지않다. 로그인을 다시 클릭하면 인증서버에 승인절차를 다시 거칠 필요가 없다. (왜냐하면 당신은 그곳에 로그아웃하지않았으므로). 어떤것이 올바른 사용자 경험인지 의견이 나뉠테지만, 이는 악명높은 속임수 문제가 있다.(싱글사인아웃 Single Sign Out:Science Direct article and Shibboleth docs). 이상적인 사용자 경험은 기술적으로 취약하지 않아야한다. 또한 사용자가 원하는게 무엇인가 심사숙고해야한다. "나는 '로그아웃'하길 원한다"는 말은 충분히 쉽게 들리지만 명확한 응답은 "무엇을 로그아웃할것인가?"이다. 이 싱글사인온서버에 의해 제어되는 모든 시스템에서 로그아웃하길 원하는지 아니면 그냥 "로그아웃"링크를 클릭하는 것을 원하는가? 여기는 이 주제를 포괄적으로 논의할 곳은 아니다. 그러나 주목해야할 가치가 있다. 만일 이 논제에 관심이 있다면 몇몇의 군침이 도는 아이디어를 Open ID Connect 사양에서 토론을 할 수 있다. 

결 론 Conclusion

이제 스프링 시큐리티와 앵귤러JS 스택을 거치는 겉핥기식 여행의 끝무렵에 다다랐다. 우리는 이제 UI/API 게이트웨이, 리소스 서버, 인증서버/토큰승인자 의 명확한 책임을 가지는 각각 3개의 분리된 컴포넌트로 구성된 멋진 아키텍쳐를 가지게 되었다. 이들은 모든 레이어에서 비지니스 로직이 아닌 코드의 양을 최소화하였고 비지니스 로직을 더욱 향상시키고 확장할 수 있도록 알아보기 쉽다. 다음 단계에서 우리는 우리의 인증서버의 UI를 깔끔하게 정리할 것이다. 아마 자바스크립트 클라이언트상의 테스트를 포함하는 약간의 테스트를 추가할 것이다. 또 하나의 흥미로은 작업은 이 표준공정(boilerplate)의 코드를 모두 추출하여 앵귤러 쪽의 네비게이션 컨트롤러를 위해 스프링 시큐리티와 스프링 세션 자동설정과 몇개의 webjars리소스를 포함하는 라이브러리 (예를 들면 "spring-security-angular")안에 넣는 것이다. 앵귤러JS나 스프링 시큐리티의 내부 동작에 대해 배우길 원하는 누구나 이 시리즈의 섹션에 아마도 실망하게 될것이다. 그러나 당신이 어떻게 모두다 함께 잘 동작하는지, 최소한의 설정으로 먼 길을 떠날 수 있는지 알기원한다면 당신은 희망적으로 좋은 경험을 얻게 될것이다. Spring Cloud 는 이제 막 릴리즈 되었고 그들을 써야할 때 스냇샷으로 가져와야한다. 그러나 Release candidate이 이용가능하고 GA가 곧 나올것이다.  Github 이나 gitter.im에 피드백을 보내거나 확인할 수 있다.

이 시리즈의 다음섹션은 (인증과정에서) 접근 결정에 관한 것이다. 같은 프록시내에서 다중의 UI어플리케이션을 사용할 것이다.

부록: 인증서버를 위한 부트스트랩 UI와 JWT 토큰 Addendum: Bootstrap UI and JWT Tokens for the Authorization Server

Github의 소스코드에서 두번째 섹션에서 우리가 만들었던 로그인페이지와 유사한 방식으로 구현한 보기좋은 로그인페이지와 유저 승인 페이지를 가지는  이 어플리케이션의 또다른 버전을 찾아볼 수 있다. 또한 JWT 인코딩된 토큰을 사용한다. 그래서 "/user" endpoint를 사용하는 대신, 리소스 서버는 간단한 인증을 수행하기 위해 토큰 자체에서 충분한 정보를 뽑아올 수 있다. 브라우저 클라이언트는 UI서버를 통해 프록시되어진 이것을 사용자가 인증되었는지 결정하는데 여전히 사용한다 (실제 어플리케이션에서 리소스서버를 호출하는 숫자에 비교하면 자주 호출될 필요가 없다.)


반응형

+ Recent posts