스프링부트는 사랑입니다

Spring Security and AngularJS Part II 본문

Tutorials

Spring Security and AngularJS Part II

얼바인 2015. 12. 2. 10:44
728x90

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


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

로그인 페이지 The Login Page

이 섹션에서 "단일페이지 어플리케이션"을 스프링 시큐리티와 앵귤러JS를 어떻게 사용할지에 대한 얘기를 계속하려고 한다. 이 장에서 어떻게 앵귤러JS가 폼을 통해 유저를 인증하는지 그리고 UI에서 랜더하기위해 보안된 리소스를 가지고 오는지를 보여줄 것이다. 시리즈의 두번째 섹션으로, 당신은 어플리케이션의 기본 구성단위를 배워 나가도 되고, 첫번째 섹션에서부터 하나씩 만들어온 것을 빌드해봐도 되며, 아니면 그냥 Github의 소스코드.로 바로 가봐도 된다. 첫번째 섹션에서 우리는 백엔드 리소스를 보호하기 위해 HTTP Basic 인증을 사용하는 간단한 어플리케이션을 만들었다. 여기에 로그인 폼을 추가하고, 사용자가 인증을 할지 말지 정하할수 있게해주고 첫번째 섹션에서 다른 문제(CSRF 보호가 원칙적으로 결여됨)를 해결할 것이다.

기억하기: 샘플 어플리케이션으로 이 섹션 동안 돌릴 거면, 쿠키와 HTTP Basic credentials의 브라우저 캐시를 꼭 지워야한다. 크롬에서는 새 익명창incognito window로 여는게 최선의 방법이다.

홈페이지에 네이게이션 추가하기 Add Navigation to the Home Page

단일 페이지 어플리케이션의 핵심은 정적인 "index.html" 파일이다. 우리는 이미 정말 간단한 것 가지고 있므로 이제 이 어플리케이션에 약간의 네이게이션 기능(로그인, 로그아웃, 홈으로)을 제공해보자. 이제 ("src/main/resources/static"에서) 수정해보자:

index.html

<!doctype html>
<html>
<head>
<title>Hello AngularJS</title>
<link
	href="css/angular-bootstrap.css"
	rel="stylesheet">
<style type="text/css">
[ng\:cloak], [ng-cloak], .ng-cloak {
	display: none !important;
}
</style>
</head>

<body ng-app="hello" ng-cloak class="ng-cloak">
	<div ng-controller="navigation" class="container">
		<ul class="nav nav-pills" role="tablist">
			<li class="active"><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>
	<div ng-view class="container"></div>
	<script src="js/angular-bootstrap.js" type="text/javascript"></script>
	<script src="js/hello.js"></script>
</body>
</html>

사실 원래 소스와 크게 다르지않다. 아주 사소한 기능으로:

  • 네비게이션 바를 위해 <ul> 를 사용했다. 모든 링크는 홈페이지로 바로 되돌아올테지만, 일단 "라우트"설정을 했다면 앵귤러는 경로를 잘 인식할 것이다.

  • 모든 컨탠트는 "ng-view"가 있는 <div>에 부분적으로 추가될것이다.

  • "ng-cloak"은 body로 이동되었다. 우리는 앵귤러가 몇몇의 랜더링을 끝마칠때까지 전체 페이지를 숨길 것이기 때문이다. 그렇지 않으면 메뉴나 컨텐트가 페이지가 로드되는 동안 깜빡깜빡 움직일 수 있다.

  • 첫번째 섹션에서 했던대로, 프론트엔드 에셋인 "angular-bootstrap.css" 와 "angular-bootstrap.js"는 빌드시 JAR 라이브러리로부터 만들어질 것이다.


앵귤러 어플리케이션에 네비게이션 추가하기 Add Navigation to the Angular Application

이제 ("src/main/resources/public/js/hello.js" 경로에 있는) "hello" 어플리케이션을 수정하여 네비게이션 기능을 추가해보자. 일단  홈페이지의 링크를 실제로 작동하게 하는 등의 라우트를 위한 추가설정부터 하자. 

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('/');

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

  })
  .controller('home', function($scope, $http) {
    $http.get('/resource/').success(function(data) {
      $scope.greeting = data;
    })
  })
  .controller('navigation', function() {});

우리는 "ngRoute" 라 불리는 앵귤러 모듈의 의존성을 추가했다. 이는 마법과 같은 $routeProvider를 설정함수에 주입할수 있게 해준다 (앵귤러는 명명규칙naming convention에 의해 의존성 주입을 하고 당신의 함수 파라미터의 이름을 인식한다). $routeProvider는 링크들을 "/" ("home" 컨트롤러)과 "/login" ("login" 컨트롤러)에 설정하는 함수의 내부에서 사용된다. "templateUrls"은 ("/"와 같은) 라우트의 루트부터의 각각의 컨트롤러에 의해 만들어지는 모델의 랜더에 사용되는 "부분적partial" 뷰들에의 상대경로relative paths이다.

커스텀 "X-Requested-With" 는 브라우저 클라이언트가 보내는 관습적인 헤더이다. 이는 앵귤러에서 기본으로 사용되었지만 1.3.0에서 떼어냈다. 스프링 시큐리티는 401 응답에 "WWW-Authenticate" 헤더를 보내지 않음으로서 응답한다. 그러므로 브라우저는 (우리는 인증하기 원하므로 우리의 앱에 바람직한) 인증창을 띄우지 않을 것이다.

"ngRoute" 모듈을 사용하기위해선, 우리는 ("src/main/wro" 경로의) 정적 에셋을 빌드하는 "wro.xml" 설정하는 라인을 추가해줘야한다:

wro.xml
<groups xmlns="http://www.isdc.ro/wro">
  <group name="angular-bootstrap">
    ...
    <js>webjar:angularjs/1.3.8/angular-route.min.js</js>
   </group>
</groups>


환영메세지 The Greeting

("src/main/resources/static"에 있는 "index.html" 바로 옆에 있는) "home.html"에 위치한 예전의 홈페이지로부터의 환영메세지 컨탠트:

home.html
<h1>Greeting</h1>
<div ng-show="authenticated">
	<p>The ID is {{greeting.id}}</p>
	<p>The content is {{greeting.content}}</p>
</div>
<div  ng-show="!authenticated">
	<p>Login to see your greeting</p>
</div>

이제 사용자가 로그인을 할지 안할지 선택할 수 있으므로 (브라우저에 의해 제어되기 전에), 우리는 보호되야하는 것과 안해도 되는 컨탠트 사이에 UI를 구별해줘야한다. (아직 없지만) authenticated변수의 참조를 추가함으로서 실현할 수 있을것이다.


로그인 폼 The Login Form

"login.html"에 있는 로그인 폼:

login.html

<div class="alert alert-danger" ng-show="error">
	There was a problem logging in. Please try again.
</div>
<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>

위의 코드는 사용자명과 패스워드를 입력하는 두개의 입력폼이 있고 ng-submit을 통해 제출할 수 있는 버튼이 있는 아주 표준화된 로그인폼이다. 사용자는 폼태그에 아무것도 할 필요가 없다. 아마 아무것도 입력하지 않는 편에 더 낫을 것이다. 또한 앵귤러의 $scopeerror값을 가지고 있을 때만 보이는 에러메시지가 있다. 폼 제어는 앵귤러와 HTML 컨트롤러간에 데이터를 보내기 위해 ng-model을 사용한다. 이 경우 우리는 credentials 객체에 사용자명과 패스워드를 보관할 것이다. 라우트에 의해, 우리는 아직 비어있는 "navigation" 컨트롤러를 통해 로그인폼을 링크하도록 정의했다. 이제 하나씩 채워보도록 하자.


인증 프로세스 The Authentication Process

우리가 추가한 로그인폼을 작동시키려면 약간의 기능을 더 추가해줘야 한다. 클라이언트 쪽에선 "navigation" 컨트롤러를 구현할 것이다. 서버쪽에선 스프링 시큐리티 설정을 할 것이다.


로그인 폼 제출하기 Submitting the Login Form

폼을 제출하려면, 우리가 이미 ng-submit를 통해 폼안에 이미 참조해둔 login() 함수를 정의해줘야한다. credentials객체는 ng-model을 통해 참조될 것이다. 이제 "hello.js"안에 위치한 "navigation" 컨트롤러에 살을 붙여보자 (라우트 설정과 "home" 컨트롤러는 생각한다):

hello.js
angular.module('hello', [ 'ngRoute' ]) // ... omitted code
.controller('navigation',

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

  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) {
        $rootScope.authenticated = true;
      } else {
        $rootScope.authenticated = false;
      }
      callback && callback();
    }).error(function() {
      $rootScope.authenticated = false;
      callback && callback();
    });

  }

  authenticate();
  $scope.credentials = {};
  $scope.login = function() {
      authenticate($scope.credentials, function() {
        if ($rootScope.authenticated) {
          $location.path("/");
          $scope.error = false;
        } else {
          $location.path("/login");
          $scope.error = true;
        }
      });
  };
});

메뉴바에 있는 <div>는 visible과 ng-controller="navigation"로 코드되어 있으므로, 페이지가 로드될때 "navigation" 컨트롤러내의 모든 소스코드가 실행될 것이다. 이 컨트롤러는 credentials객체를 초기화하는 것 뿐만 아니라 두개의 함수를 정의하고 있다. login()함수는 폼에서 필요하고,  authenticate()는 로컬 헬퍼local helper 함수로서 백엔드에서 "user" 리소스를 불러오는데 쓰인다. authenticate()함수는 (이를테면, 만일 사용자가 세션을 설정하는 중간에 브라우저를 새로고치는 등으로) 컨트롤러가 로드될때 유저가 이미 인증을 받았는지를 확인할때 호출된다. 서버에서 이미 인증을 받았을 때 원격호출을 하기위해 authenticate()가 필요하다 그리고 우리는 이 인증을 유지하는 것을 브라우저에 맡기길 원치않는다.

authenticate()함수는 authenticated이라 부르는 어플리케이션 차원의 플래그로서 설정되는데 이는 우리가 "home.html"에서 페이지의 어느 부분을 렌더할지 제어하기 위해 이미 사용했었다. "navigation"과 "home" 컨트롤러간에 authenticated 플래그를 공유하는 목적으로 $rootScope를 사용하는 데 이는 편안하고 쉽게 따라할 수 있기 때문이다. 앵귤러 전문가라면 아마 공유되는 사용자 정의 서비스를 통해 데이터를 공유하는 편을 선호할 것이다 (하지만 이것도 결국 따지고보면 같은 메카니즘이다)

authenticate()는 (당신의 어플리케이션의 배포루트에 상대적인) 상대적 리소스 "/user" 에 GET 호출을 한다.  login() 함수가 호출될때 Base64 인코딩된 credential을 헤더에 추가한다. 서버에서 이걸로 인증을하고 돌아오는 쿠키를 되돌려준다. login() 함수 역시 인증의 결과를 받아올때 로그인폼에 에러메세지를 표시하는데 쓰이는 로컬 $scope.error 플래그를 설정한다.


현재 인증된 사용자 The Currently Authenticated User

authenticate() 함수를 서비스하려면 우리는 백엔드에 새로운 종단endpoint을 추가해야한다:

UiApplication.java
@SpringBootApplication
@RestController
public class UiApplication {

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

  ...

}

스프링 시큐리티 어플리케이션에서 이렇게 설정하는 것은 유용한 전략이다. "/user" 리소스에 접근할 권한이 있다면, 현재 인증된 유저(Authentication)를 되돌려줄것이다. 그렇지 않다면 스프링 시큐리티는 요청을 가로챈후 AuthenticationEntryPoint를 거쳐 401 응답을 보낸다.

서버에서 로그인 요청을 처리하기 Handling the Login Request on the Server

스프링 시큐리티로 로그인 요청을 쉽게 처리할 수 있다. 그냥 우리의 메인 어플리케이션 클래스 (inner클래스로서)에 약간의 설정만 추가해주면 된다:

UiApplication.java

@SpringBootApplication
@RestController
public class UiApplication {

  ...

  @Configuration
  @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
  protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http
        .httpBasic()
      .and()
        .authorizeRequests()
          .antMatchers("/index.html", "/home.html", "/login.html", "/").permitAll()
          .anyRequest().authenticated();
    }
  }

}

위의 코드는 단지 정적인 리소스(HTML)에의 익명의 접근을 허용하도록 커스터마이즈한 (CSS와 JS리소스는 이미 기본값으로 접근가능하다) 스프링 시큐리티를 가지고 표준화된 스프링 어플리케이션이다, HTML리소스는 스프링 시큐리티가 무시하도록 설정함으로서 익명의 사용자도 이용가능해야한다.


로그아웃 Logout

어플리케이션이 기능적으로 거의 완성되었다 마지막으로 우리는 홈페이지에서 밑그림을 그려둔 로그아웃 기능을 구현하려고한다. 다음과 같이 만들어둔 네이게이션 바를 상기해보자:

index.html

<div ng-controller="navigation" class="container">
  <ul class="nav nav-pills" role="tablist">
    <li class="active"><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>

사용자가 인증을 받은 상태면, 로그아웃 링크를 볼 수 있다. 이는 "navigation" 컨트롤러안의 logout()함수와 묶여있다. 이 함수의 구현은 상대적으로 간단하다:

hello.js
angular.module('hello', [ 'ngRoute' ]).
// ...
.controller('navigation', function(...) {

...

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

...

});

이제 우리가 서버쪽을 구현해야할 "/logout"으로 HTTP POST를 보낸다.  스프링 시큐리티에 의해 이미 추가되었으므로 매우 직관적이다.(한마디로, 우리는 이 간단한 시나리오에 아무것도 할 필요없다.). 하지만 로그아웃 이후의 비지니스 로직을 돌리려고 하는 등으로 로그아웃의 동작을 좀 더 제어해야한다면 WebSecurityAdapter에 있는 HttpSecurity 콜백을 사용할 수 있다. 


CSRF 보안 CSRF Protection

이제 어플리케이션이 거의 준비되었다. 사실 실행해보면 우리가 로그아웃 링크를 제외한 지금까지 실제로 작업한 모든 것을 확인해볼 수 있다. 한번 테스트하고 브라우저의 응답을 둘러보면 왜 그런지 알 수 있다:

POST /logout HTTP/1.1
...
Content-Type: application/x-www-form-urlencoded

username=user&password=password

HTTP/1.1 403 Forbidden
Set-Cookie: JSESSIONID=3941352C51ABB941781E1DF312DA474E; Path=/; HttpOnly
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
...

{"timestamp":1420467113764,"status":403,"error":"Forbidden","message":"Expected CSRF token not found. Has your session expired?","path":"/login"}

보다시피 스프링 시큐리티의 내장 CSRF protection가 우리 스스로의 요청도 막아버리고 있으므로 잘 동작한다고 할 수 있다. CSRF protection이 원하는건 헤더에 "X-CSRF"라는 토큰이 들어있는 것이다. CSRF 토큰의 값은 홈페이지가 불러지는 첫번째 요청의 HttpRequest속성값을 가지고 서버에서 이용한다. 클라이언트가 이 값을 받으려면, 서버에서 동적 HTML 를 사용해서 랜더하거나 커스텀 종단endpoint를 통해 노출시키거나 또는 우리가 쿠키로서 보내거나 해야한다. 앵귤러가 쿠키에 기반한 ("XSRF"라 부르는) 내장된 CSRF지원을 가지고 있기 때문에 마지막 옵션이 최고의 선택이 될것이다.

서버에서는 쿠키를 보내는 커스텀 필터가 필요하다. 앵귤러는 쿠키이름을 "XSRF-TOKEN"라고 받길 원하므로 스프링 시큐리티는 요청 속성값으로 제공해줘야 한다. 요청속성값에 쿠키를 다음과 같이 전송하면 된다:

CsrfHeaderFilter.java

public class CsrfHeaderFilter extends OncePerRequestFilter {
  @Override
  protected void doFilterInternal(HttpServletRequest request,
      HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
    CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class
        .getName());
    if (csrf != null) {
      Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
      String token = csrf.getToken();
      if (cookie==null || token!=null && !token.equals(cookie.getValue())) {
        cookie = new Cookie("XSRF-TOKEN", token);
        cookie.setPath("/");
        response.addCookie(cookie);
      }
    }
    filterChain.doFilter(request, response);
  }
}

이것을 완전하게 제네릭하게 만들어 완성하려면,  ("/"를 하드코드하는 대신) 어플리케이션의 컨텍스트 경로에 쿠키 경로를 신중하게 설정해야한다. 하지만 우리가 만드는 이 어플리케이션수준에선 충분하다.

어플리케이션의 어딘가에 이 필터를 설치해야하는데 이 요청의 속성값을 사용하려면 스프링 시큐리티의 CsrfFilter이후에 불려져야한다. 이들 리소스를 보호하기 위해 스프링 시큐리티가 이미 사용중이므로 스프링 시큐리티의 Spring Security filter chain이 최적의 장소이다. 예를 들면, 위의 SecurityConfiguration를 다음과 같이 확장해보자:

SecurityConfiguration.java

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .httpBasic().and()
      .authorizeRequests()
        .antMatchers("/index.html", "/home.html", "/login.html", "/").permitAll().anyRequest()
        .authenticated().and()
      .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);
  }
}

서버단에서 해줘야하는 또 다른 작업은 스프링 시큐리티에 앵귤러가 받기를 원하는 포맷 (앵귤러는 스프링의 기본값인 "X-CSRF-TOKEN" 대신 "X-XRSF-TOKEN" 이름을 헤더에 쓴다)의 CSRF토큰을 알려줘야한다.

SecurityConfiguration.java

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .httpBasic().and()
    ...
    .csrf().csrfTokenRepository(csrfTokenRepository());
}

private CsrfTokenRepository csrfTokenRepository() {
  HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
  repository.setHeaderName("X-XSRF-TOKEN");
  return repository;
}

위의 수정으로 이제 클라이언트 쪽의 아무런 수정없이 로그인폼이 잘 작동한다.


어떻게 작동하나? How Does it Work?

개발자 툴 (크롬에선 기본값으로 F12를 누르면 된다. 파이이폭스는 플러그인을 설치해야한다)을 통해, 브라우저와 백엔드간의 연동을 브라우저에서 확인할 수 있다. 아래 정리해두었다:

VerbPathStatusResponse

GET

/

200

index.html

GET

/css/angular-bootstrap.css

200

트위터 부트스트랩 CSS

GET

/js/angular-bootstrap.js

200

부트스트랩과 앵귤러JS

GET

/js/hello.js

200

어플리케이션 로직

GET

/user

401

Unauthorized

GET

/home.html

200

홈페이지

GET

/resource

401

Unauthorized

GET

/login.html

200

앵귤러 로그인 폼 일부

GET

/user

401

Unauthorized

GET

/user

200

인증credentials보내고 JSON받기

GET

/resource

200

JSON greeting

위에 "ignore"로 표시된 응답은 앵귤러가 XHR호출로 받은 HTML응답이다. 우리는 HTML로 떨어진 데이터를 처리하지 않으므로 "/user"리소스의 경우 인증된 유저만 볼 수 있다. 하지만 첫번째 호출에는 없기때문에 응답이 받아진다.

요청을 더 자세히 들여다보면 그들이 모두 쿠키값을 가지고 있는 것을 확인할 수 있을 것이다. (크롬의 incognito와 같은) 클린 브라우저로 시작을 하면, 맨 처음 요청에는 서버에서 보내진 쿠키가 없지만 서버는 JSESSIONID" (일반적으로HttpSession)와 X-XSRF-TOKEN" (우리가 위에서 설정한 CRSF 쿠키)를 위해 "Set-Cookie"를 돌려준다. 이 값들은 매우 중요한데 어플리케이션은 이들없이 동작하지않는다. 이 값들은 매우 기본적인 보안기능(인증과 CSRF보호)을 제공해주고 있다. 쿠키의 값은 유저가 (POST 호출로) 인증받으면 바뀐다. 이는 (session fixation attacks을 방지하는) 또 하나의 중요한 보안기능이다.

CSRF보호를 위해 서버에서 받은 쿠키에 의존하는 것은 적절하지 않다. 브라우저는 당신이 어플리케이션에서 불러진 페이지에 있지않을 때도 자동적으로 이것을 보내기 때문이다.(XSS라고 알려진 Cross Site Scripting Attack에 의해). 헤더는 자동적으로 보내지지 않으므로 원본은 지켜진다. 사용자는 어플리케이션에서 CSRF토큰이 클라이언트에 쿠키값으로 보내지는 것을 확인할 수 있다. 따라서 우리는 브라우저에 의해 자동적으로 되돌려질 것이다. 그러나 이것은 보호되고 있는 헤더이다.

도와줘,, 내 어플리케이션을 어떻게 스케일하지?

"그런데 잠시만…싱글페이지 어플리케이션에서 세션 상태을 사용하는건 안좋은거 아닌가요?" 라고 물어본다면, 대답은 "보통은 그렇다" 이다. CSRF보안과 인증에 세션상태을 사용하는 것은 확실히 매우 좋다. 이 상태는 어딘가에 저장되어야한다 그리고 사용자가 세션을 떼어내면 어딘가에 넣어두고 그것을 사용자 스스로 관리해줘야할 것이다. 서버와 클라이언트 양쪽에서 이것은 더 많은 코드와 유지보스를 필요로 하며 이것은 완전히 불필요한 일을 하는 것이다.

"하지만,, 하지만… 그럼 이제 어떻게하면 내 어플리케이션의 규모를 크게 스케일할 수 있죠?". 이 "진짜" 질문은 당신이 위에서 했던 것이지만, 세션은 나쁘다 -> 나는 상태없는stateless 상태로 가야한다고 생각한다로 수렴되는 경향이 있다. 놀랄 것없다. 여기서 받아들여지는 중요한 점은 보안은 stateful이라는 점이다. 당신은 안전한 stateless한 어플리케이션을 가질 수 없다. 그럼 어디에 state를 저장할 것인가? 이게 여기서 얘기할 수 있는 전부다. Rob Winch 는 Spring Exchange 2014 에서 state가 왜 필요한지 (또 이는 어디에서나 쓰인다. - TCP와 SSL은 statfule이고 따라서 당신이 알았던 몰랐던 당신의 시스템도 stateful 이다) 설명한 매우 유용하고 통찰력있는 (이 주제에 대해 더 깊이 파고 들고 싶어하다고 느낄만한) 얘기를 했었다

좋은 소식은 당신은 선택할 수 있다는 것이다. 가장 쉬운 선택은 세션데이터를 메모리(in-memory)에 저장하고 당신의 로드밸러서에 세션를 전적으로 맡겨서 같은 JVM에서 같은 세션을 돌려주도록 요청을 라우트하는 것이다.  이 방법은 당신이 맨땅에서 시작하기에 충분히 좋을 뿐만아니라 실제 수많은 사용예에서 잘 작동할 것이다. 또 다른 선택은 어플리케이션 인스턴스간에 세션 데이터를 공유하는 것이다. 사용자가 엄격히 보안데이터만을 저장하고 이 데이터가 작으며 드물게 바뀌는 것이라면 (유저가 로그인, 로그아웃하거나 사용자 세션이 타임아웃됬을때만) 그래서 주요 기반(infrastructure)의 문제가 없다면, 스프링 세션으로 아주 쉽게 해결할 수 있다. 우리는 이 시리즈의 다음 섹션에서 스프링 세션을 사용할 것이므로 여기서 어떻게 설정하는 지 등에 대해 자세하게 들어갈 필요는 없다. 하지만 말그대로 코드 몇줄과 레디스 서버면 된다.

 또다른 손쉬운 방법은 세션 상태를 공유하도록 설정하고 클라우드 파운더리의 Pivotal Web Services에 WAR파일로 어플리케이션을 배포한뒤 레디스 서비스에 바인드하는 것이다.

하지만 내 커스텀 토큰 구현은 어떻게 되지? (이건 Stateless인데?)

이게 이 마지막 섹션에의 당신의 질문이라면, 아마도 당신은 처음부터 알아듣지 못한 것같으니 위의 글을 다시 읽어보자. 당신이 어딘가에 토큰을 저장한다면 그것은 stateless가 아닐 것이다. 그러나 당신이 (JWT 인코드된 토큰을 사용함으로서) 따로 저장하지않는다면, 어떻게 CSRF보호를 할 것인가? 이것은 중요한 문제다. 여기 (Rob Winch가 결론지은) 경험의 법칙이 있다: 당신의 어플리케이션이나 API를 브라우저로 접근한다면, 당신은 CSRF 보호가 필요하다. 이것은 세션이 없다고 못하는 것은 아니다. 그냥 당신 스스로 코드를 만들면된다. 요점은 이미 HttpSession 기반으로 아주 완벽히 동작하게 구현되어있는게 있다는 것이다. 당신이 CSRF가 필요없고 완벽히 (세션을 사용하지않는) "stateless" 토큰 구현체를 쓰도록 결심했다 하더라도, 당신은 여전히 클라이언트에 이를 (당신이 브라우저에 위임하도록 한 것) 사용하도록 그리고 서버의 스스로 내장된 기능(기본값을 끄지않으면 브라우저는 항상 쿠키를 보내고 서버는 항상 세션을 가진다)을 가지도록 추가의 코드를 만들어야만 한다. 그 코드는 비지니스 로직도 아니고 그걸로 돈을 벌수 있는 것도 아니다. 그럴만한 가치가 있다하더라도 추가적인 비용이 들어가는 그냥 간접비용일 뿐이다. 

결 론 Conclusion

어플리케이션은 이제 사용자가 기대하는 "실제" 환경에서 돌아가는 "실전" 어플리케이션에 더 가까워졌다. 이 아키텍쳐을 기반으로 더 풍부한 기능을 더할 수 있는 템플릿으로서 사용할 수 있을 것이다. (정적 컨텐트와 JSON응답을 가지는 단일 서버). 우리는 보안데이터를 저장하기위해 클라이언트에 우리가 보낸 쿠키를 사용하도록 하여 HttpSession를 사용하고 있다. 우리는 이제 아주 편안하다 왜냐하면 우리 스스로의 비지니스 도메인에 집중하면 되기 때문이다. 다음 섹션에서 우리는 아키텍쳐를 확장하여 인증과 UI서버를 분리할 것이다. 또 JSON을 위해 독립 리소스 서버를 갖출 것이다. 이 다중 리소스 서버는 손쉽게 만들 수 있다. 또한 스프링 세션을 소개하여 어떻게 인증데이터를 공유하는데 활용하는지 보여줄 것이다.


728x90