반응형

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

이 섹션에서 우리는 어떻게 스프링 시큐티리와 앵귤러JS로 단일 페이지 어플리케이션을 만드는지 계속 얘기해볼 것이다. 우리의 어플리케이션에서 동적 컨텐트로서 사용하고 있는 "greeting" 리소스를 별도의 서버 (먼저 보호하지않아도 되는 리소스, 그다음 토큰에 의해 보호되는 리소스)로 빼내는 걸로 시작하자. 이 글은 시리즈의 3번째 섹션이다. 당신이 어플리케이션의 기본구성을 이해하거나 처음부터 빌드해보려고 하면 첫번째 섹션부터 읽도록 하자 또는 보호하지않아도 되는 리소스토큰에 의해 보호되는 리소스 두 파트로 나누어진 Github의 소스코드를 직접 받아도 된다. 


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

분리된 리소스 서버 A Separate Resource Server

클라이언트쪽 수정 Client Side Changes

클라이언트 쪽은 리소스를 서로 다른 백엔드로 이동하기위해 해줘야 할게 거의 없다. 여기 "home" 컨트롤러의 마지막 섹션을 보자:

hello.js

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

우리가 해줘야하는 건 URL을 바꿔주는 게 전부다. 예를 들면, 로컬호스트의 새 리소스를 돌리려면 아래와 같이 하면 된다:

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

서버쪽 수정 Server Side Changes

 UI 서버의 수정은 아주 경미하다. 그냥 (기존에 "/resource" 였던) greeting 리소스를 위한 @RequestMapping를 지워주면 된다. 그 다음 우리가 Spring Boot Initializr를 사용한 첫번째 섹션에서 했던 대로 새로운 리소스 서버를 만들어줘야한다. 예를 들면 유닉스 계열의 시스템에선 다음과 같이 curl을 사용해보자: [주: 그냥 sts씁시다]

$ mkdir resource && cd resource
$ curl https://start.spring.io/starter.tgz -d style=web \
-d name=resource -d language=groovy | tar -xzvf -

그후 프로젝트를 당신이 선호하는 IDE에 import 하자 (기본설정은 보통 메이븐 자바 프로젝트다), 또는 커맨드라인에서 "mvn"을 써도 된다. 우리는 여기서 그루비를 사용할테지만 자바가 편하다면 자바를 써도 된다, 어쨋든 코드가 많지 않을 것이다.

예전 UI에 구현해둔 코드를 복사해서 main application class에 @RequestMapping 를 추가하자:

ResourceApplication.groovy
@SpringBootApplication
@RestController
class ResourceApplication {

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

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

}

일단 이걸로 다음의 커맨드라인을 타입함으로서 어플리케이션을 브라우저에서 불러올 수 있다. 

$ mvn spring-boot:run --server.port=9000

브라우저에서 http://localhost:9000 를 열고 geeting을 가진 JSON을 확인해보자. ("src/main/resources"에 위치한) application.properties에서 포트넘버를 바꿀 수 있다.

application.properties
server.port: 9000

브라우저를 열고  8080 포트에서 작동중인) UI로 부터 리소스를 불러보면 제대로 동작하지않는 것을 볼 수 있다. 브라우저가 XHR요청을 허용하지 않기 때문이다.

CORS 협상 CORS Negotiation

브라우저는 Cross Origin Resource Sharing 프로토콜에 따라 접근이 허용되는지를 알기위해 우리의 리소스 서버에 협상을 한다. 이는 앵귤러JS의 소관이 아니다. 쿠키와 같이 브라우저에서 그냥 모든 자바스크립트와 같이 작동할 것이다.  이 두 서버는 자기들이 공통의 기원origin을 가지고 있다고 선언하지 않았기 때문에 브라우저는 요청을 보내는 것을 거부하고 UI가 깨지게 된다.

이것을 고치려면 우리는 "pre-flight" 옵션 요청과 호출자의 허용된 행동을 리스트하기위한 몇몇의 헤더들을 포함하는 CORS 프로토콜을 지원해야한다. 스프링 4.2는 잘 정제된 CORS 지원을 한다고 했지만 릴리즈 전까지는 (주, 이미 릴리즈됨Filter.를 써서 모든 요청에 같은 CORS응답을 보내는 어플리케이션의 목적에 맞춰주는 작업을 해야한다. 리소스 서버 어플리케이션의 같은 디렉토리에 클래스를 하나 만들고 @Component 어노테이션을 한다 (스프링 어플리케이션 컨텍스트가 스캔할 수 있도록). 예를 들면:

CorsFilter.groovy

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class CorsFilter implements Filter {

  void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
    HttpServletResponse response = (HttpServletResponse) res
    HttpServletRequest request = (HttpServletRequest) req
    response.setHeader("Access-Control-Allow-Origin", "*")
    response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE")
    response.setHeader("Access-Control-Allow-Headers", "x-requested-with")
    response.setHeader("Access-Control-Max-Age", "3600")
    if (request.getMethod()!='OPTIONS') {
      chain.doFilter(req, res)
    } else {
    }
  }

  void init(FilterConfig filterConfig) {}

  void destroy() {}

}

Filter는 주요 스프링 시큐리티 필터보다 전에 적용되어야 하므로 @Order를 정의했다. 이 리소스 서버의 수정후, 재시작하면 UI로 greeting을 받아올 수 있다.

 태평스럽게  Access-Control-Allow-Origin=*를 사용하는건 빠르고 지저분하지만 잘 동작한다. 이 방식은 안전하지 않으므로 추천되는 방법은 아니다.

리소스 서버 보호하기 Securing the Resource Server

훌륭하게 새 아키텍처의 어플리케이션이 잘 동작하게 만들었다. 문제는 리소스서버가 보호되고 있지않다는 점 뿐이다.

스프링 시큐리티 추가하기 Adding Spring Security

UI서버와 같이 필터를 사용해서 리소스 서버에 시큐리티를 추가하는 법을 보자. 이것은 아마 더 기본의 방식대로 일것이고 대부분의 PaaS환경에서 최고의 선택이다. (보통 어플리케이션을 위해 private network를 만들지 않기때문에). 첫걸음은 정말 쉽다: 그냥 스프링 시큐리티를 메이븐 POM의 클래스패스에 추가해주면 된다:

pom.xml

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

리소스 서버를 재시작해보자. 유후! 이제 보안이 적용되었다:

$ curl -v localhost:9000
< HTTP/1.1 302 Found
< Location: http://localhost:9000/login
...

로그인 페이지로 리다이렉트되었다. curl은 우리의 앵귤러 클라이언트처럼 헤더를 보내지 않기 때문이다. 더 유사한 헤더를 보내도록 이 명령어를 수정해보자:

$ curl -v -H "Accept: application/json" \
    -H "X-Requested-With: XMLHttpRequest" localhost:9000
< HTTP/1.1 401 Unauthorized
...

우리는 단지 모든 요청에 credential을 보내도록 클라이언트에 알려주기만 하면 된다.

토큰 인증 Token Authentication

인터넷과 사람들의 스프링 백엔드 프로젝트는 커스텀 토큰 기반 인증 솔루션들로 어지럽혀져있다. 스프링 시큐리티는 사용자 스스로 시작할 수 있는 그 근간의 Filter 구현체를 제공한다.( 예로, AbstractPreAuthenticatedProcessingFilter 와TokenService를 보자). 그렇지만 스프링 시큐리티는 쓰라고 정해놓은 구현체가 없다. 이 이유중 하나는 아마 더 손쉬운 방법이 있기때문일 것이다.

이 시리지의 두번째 섹션을 기억해보자. 스프링 시큐리티는 기본값으로 인증데이터를 저장하기위해 HttpSession 을 사용한다. 비록 이것은 세션과 직접적으로 연관이 없지만: 사용자가 뒷단의 저장장소를 변경하는 중에 사용할 수 있는 추상 레이어(SecurityContextRepository)가 있다. UI에 의해 검증된 인증을 저장하기 위해 우리의 리소스 서버에 이 repository를 설정해두면, 우리는 두 서버간에 인증을 공유하는데 쓸 수 있다. UI서버는 이미 (HttpSession를 통해) 그러한 저장을 하고 있으므로 이 저장된 것을 배포하고 리소스 서버에 따라 열면 우리는 대부분의 해결책을 갖추게된다. 

스프링 세션 Spring Session

스프링 세션으로 이 부분을 손쉽게 해결할 수 있다. 우리는 그저 공유데이터 저장소가 필요할 뿐이다. (레디스는 바로 사용가능도록 지원되고 있다). Filter를 설정하기위해 서버에 몇줄의 설정만 해주면 될뿐이다.

UI 어플리케이션에선 POM에 몇개의 의존성만 추가해주면 된다:

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
@RestController
@EnableRedisHttpSession
public class UiApplication {

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

  ...

}

@EnableRedisHttpSession 어노테이션은 스프링 세션에서 온 것으로 스프링 부트의 레디스 연결을 지원한다. (URL과 credential은 환경변수 또는 설정 파일을 통해 설정할 수 있다)

위의 한줄의 코드로 레디스 서버는 UI어플리케이션을 실행하는 로컬호스트에서 동작하여 유효한 사용자 credential과 세션 데이터(인증과 CSRF토큰)를 가지고 로그인정보가 레디스에 저장될 것이다.


로컬에서 동작하는 레디스 서버가 없다면, Docker(윈도우와 맥에서는 VM이 필요하다)로 쉽게 돌릴 수 있다. Github의 소스코드에서 docker-compose.yml 파일이 있어 docker-compose up명령어로 커맨드라인에서 손쉽게 동작시킬 수 있다. 

UI로부터 커스텀 토큰 보내기 Sending a Custom Token from the UI

이제 하나만 빼고 완성되었는데 저장소에서 데이터에의 키값을 위한 전송 메카니즘이다. 키값은 HttpSession ID 이므로, 우리가 UI클라이언트에서 그 키값을 유지하고 있다 리소스 서버에 커스텀 헤더로서 이것을 보낼 수 있다. 따라서 "home" 컨트롤러는 greeting 리소스를 위해 HTTP 요청의 일부로서 이 헤더를 보낼 수 있도록 수정되어야 한다. 예를 들면:

hello.js

angular.module('hello', [ 'ngRoute' ])
...
.controller('home', function($scope, $http) {
  $http.get('token').success(function(token) {
    $http({
      url : 'http://localhost:9000',
      method : 'GET',
      headers : {
        'X-Auth-Token' : token.token
      }
    }).success(function(data) {
      $scope.greeting = data;
    });
  })
});

(더 우아한 해결책은 아마 필요할 때 토큰을 가져오는 것일 것이다. 앵귤러의 interceptor 를 사용하여 리소스 서버로의 모든 요청에 헤더를 추가 할 수 있다. 인터셉터의 정의는 한 장소에서 모든것을 하고 비지니스 로직을 마구 채워가는 대신 추상화하는 것이다.

"http://localhost:9000[http://localhost:9000]"에 직접적으로 가는 대신, 우리는 하나의 호출의 성공적인 콜백안에 UI서버의 "/token"이라는 새로운 커스텀 종단을 부르도록 씌우는 것(wrap)이다. 이것의 구현은 이렇게 사소하다:

UiApplication.java
@SpringBootApplication
@RestController
@EnableRedisHttpSession
public class UiApplication {

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

  ...

  @RequestMapping("/token")
  @ResponseBody
  public Map<String,String> token(HttpSession session) {
    return Collections.singletonMap("token", session.getId());
  }

}

이렇게 UI어플리케이션은 준비가 되었고 모든 백엔드로의 호출에 "X-Auth-Token"이 헤더에 들어간 세션ID를 포함할게 될 것이다.

리소스 서버의 인증 Authentication in the Resource Server

커스텀 헤더를 받아들이려면 리소스 서버에 아주 작은 수정만 해주면 된다. CORS 필터에 원격 클라이언트로부터 허용되는 헤더를 정해줘야한다

예를 들면:

CorsFilter.groovy

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {

  void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    ...
    response.setHeader("Access-Control-Allow-Headers", "x-auth-token, x-requested-with")
    ...
  }

  ...
}

이제 남은 작업은 리소스 서버에서 커스텀 토큰을 가져와 사용자를 인증하는데 이를 사용하는 것이다. 이 작업은 매우 직관적인데, 우리가 해줄 건 단지 어디에 세션 repository가 있는가, 그리고 들어오는 요청의 어디에서서 토큰(세션ID)를 찾을 수 있는가를 스프링 시큐리티에 알려주는 것 뿐이다. 먼저 우리는 스프링 세션과 레디스 의존성을 추가해줘야하고 그 후 Filter를 설정할 수 있다:

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

  ...

  @Bean
  HeaderHttpSessionStrategy sessionStrategy() {
    new HeaderHttpSessionStrategy();
  }

}

이 생성된 필터는 UI서버의 하나를 미러링해서 세션저장소로서 레디스를 설정한다. 유일한 차이점은 기본값("JSESSIONID"라 부르는 쿠키) 대신 이 헤더안에 위치한 커스텀 HttpSessionStrategy를 사용하는 것이다. 우리는 또한 인증되지 않은 사용자에게 보여지는 팝업창을 막아줘야한다. - 어플리케이션은 보호되고 있기때문에 기본값으로 WWW-Authenticate: Basic 를 가진 401를 보내 브라우저가 사용자명과 패스워드를 타입할 수 있는 팝업창으로 응답한다. 여러 가지 방법이 있지만 우리는 이미 앵귤러가 "X-Requested-With" 헤더를 보내도록 만들어뒀기때문에 스프링 시큐리티는 기본적으로 이를 처리해준다.

리소스 서버를 새로운 인증스키마에서 작동하게 만드는 마지막 수정이 남았다. 스프링 부트의 기본 시큐리티는 stateless이고 우리는 이를 세션안에서 인증을 저장하기를 원하므로 application.yml (또는 application.properties)에서 다음과 같이 명시해줘야한다:

application.yml

security:
  sessions: NEVER

이것은 스프링 시큐리티에게 "세션을 절대 생성하지 말되 거기에 이미 있다면 그것을 사용하라"는 뜻이다 (UI에서의 인증때문에 이미 거기에 있을 것이다.)

리소스 서버를 재시작하고 새 브라우저 창에서 UI를 열어보자

왜 쿠키와는 작동하지 않나요? Why Doesn’t it All Work With Cookies?

우리는 커스텀 헤더를 사용하고 헤더를 넣기위해 클라이언트의 코드를 만들었다. 이것은 끔찍하게 복잡하진 않으나 가능한한 쿠키와 세션을 사용하라는 두번째 섹션의 내용과 모순되는 것처럼 보인다. 불필요한 복잡성을 추가하려고 이걸 언급한게 아니다 우리가 지금 가지고 있는 구현체는 이미 지금까지 봐온 최고의 복잡성을 가졌다는 걸 확인하려는거다: 우리 솔루션의 기술적인 부분은 (명확히 아주 작고 사소한) 비지니스 로직보다 훨씬 더 가치가 있다. 이는 명백히 정당한 비판이다. (그리고 우리가 이 시리즈의 다음 섹션에서 언급할 것이다.) 그러나 왜 모든 것에 쿠키와 세션을 사용하는 것만큼 간단하지 않은지 잠시 둘러보자.

적어도 우리는 여전히 세션을 사용한다. 스프링 시큐리티와 서블릿 컨테이너가 우리의 파트에 어떠한 노력없이 어떻게 이것을 하는지 알고 있기 때문이다. 하지만, 우리가 인증토큰을 전송하기위해 쿠키를 사용해야하는 것 아닌가? 멋진 생각이지만 이것이 제대로 동작하지않는 이유가 있다. 그리고 브라우저도 이렇게 쓰는 것을 허용하지 않는다. 당신이 자바스크립트 라이브러리를 써서 브라우저의 쿠키저장소에서 값을 가지고 올 수 있지만 약간의 제약사항이 있다. 당신은 특히 서버에서 (기본적으로 세션 쿠키의 경우로 볼 수 있는) "HttpOnly"로 보내진 쿠키값에 접근할 수 없다는 것이 하나의 좋은 이유다. 또한 발신(outgoing) 요청안에 쿠키를 설정할 수 없기 때문에  우리는 (스프링세션의 기본 쿠키이름인) "세션" 쿠키를 설정할 수 없다. 따라서 커스텀 "X-Session" 헤더를 사용해야했다. 이 둘 다 사용자 스스로의 보호를 위한 제약사항들도 악의적인 스크립트가 올바른 인증없이 당신의 리소스에 접근할 수 없도록 해준다.

UI와 리소스 서버는 공통의 원본origin을 가지지 않는다. 따라서 그들은 (우리가 그들을 스프링세션을 사용하여 세션을 공유하도록 강제함에도 불구하고) 쿠키를 공유할 수 없다.

결 론 Conclusion

우리는 이 시리즈의 두번째 섹션에서 만든 어플리케이션의 기능을 복제하였다: 원격의 백엔드로부터 불러온 greeing을 가지는 홈페이지, 네비게이션바안에 로그인과 로그아웃 링크들. 리소스 서버로 부터 오는 greeing의 차이는 UI서버안에 내장된게 아니라 독립실행상태라는 점이다. 이는 구현에 중대한 복잡성을 추가해야 했지만, 좋은 점은 우리는 대부분 설정기반configuration-based (그리고 실제론 100% 선언적) 솔루션을 가지고 있다는 점이다. 모든 새 코드들을 라이브러리 (스프링 설정과 앵귤러 커스텀 디렉티브안에서 뽑아옴으로서 솔루션을 100% 선언적으로 만들수도 있다. 몇가지 인스톨후로 이 흥미로운 작업을 잠시 뒤로 미룰 것이다. 다음 섹션에서 우리는 현재의 구현의 복잡성을 줄이는 또다른 정말 훌륭한 방법을 살펴볼 것이다: API 게이트웨이 패턴 (클라이언트는 모든 요청을 한군데로 보내고 인증은 그곳에서 처리된다)

 우리는 여기에서 스프링 세션을 논리적으로 같은 어플리케이션이 아닌 두 서버간에 세션을 공유하기 위해 사용하였다. 이는 깔끔한 트릭이다. "정식"의 JEE 배포 세션에서는 가능하지않다.


반응형

반응형

스프링 시큐리티와 앵귤러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을 위해 독립 리소스 서버를 갖출 것이다. 이 다중 리소스 서버는 손쉽게 만들 수 있다. 또한 스프링 세션을 소개하여 어떻게 인증데이터를 공유하는데 활용하는지 보여줄 것이다.


반응형

반응형

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


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

단일페이지 보안어플리케이션 A Secure Single Page Application

이 섹션에서 스프링 시큐리티, 스프링 부트와 앵귤러JS의 멋진 기능을 같이 조합하여 쾌적하고 시큐어한 사용자 경험을 제공하려고 한다. 스프링과 앵귤러JS를 막 시작하려는 사람도 충분히 따라할수 있는 수준이지만, 많은 디테일한 정보를 제공하여 경력자들 또한 이롭게 할 것이다. 이 글은 스프링 시큐리티와 앵귤러JS의 새 기능을 순차적으로 연재하는 시리즈의 첫번째 섹션이다. 연재가 되면서 어플리케이션을 차츰 더 낫게 손보겠지만 이후의 주요 변화는 기능적인 것보다는 아키텍쳐 관점이 될 것이다.


스프링과 단일페이지 어플리케이션 Spring and the Single Page Application

HTML5과 "단일페이지어플리케이션"은 요즘 개발자들에게 극도의 가치가 있는 툴이지만 정적인 컨텐츠(HTML,CSS와 자바스크립트)뿐만 아니라 의미가 되는 상호작용은 백엔드서버에 들어있으므로 우리는 백엔드 서버가 필요하다. 백엔드서버는 모든 역할의 갯수 - 정적인 컨텐츠를 제공하고, 가끔 (요즘엔 많지않지만) 동적인 HTML를 랜더링하며, 보호하는 자원의 안전한 접근을 보장하며, 자바스크립트와 브라우저내에서 HTTP와 JSON을 통해 상호작동 (REST API라 언급되기도함)을 하는 - 만큼의 역할을 할 수 있다.

스프링은 백엔드 기능(특히 엔터프라이즈 환경에서)을 만드는데 있어 언제나 인기있는 기술이었고,  스프링 부트의 출현과 함께 이러한 기능을 제공하기에 이보다 더 쉬울 순 없게 되었다. 오직 스프링부트와 앵귤러JS 그리고 트위터 부트스트랩만 사용해서 어떻게 새 단일페이지 어플리케이션을 살펴보도록 하자. 이 기술세트를 선택하게 된 특별한 이유가 있는건 아니고 그냥 엔터프라이즈 자바 바닥에서 아주 인기가 많기 때문에, 이렇게 시작할 충분한 가치가 있기 때문이다. 


새 프로젝트 만들기 Create a New Project

이 어플리케이션을 만들기위한 단계마다 스프링과 앵귤러에 익숙하지않은 사람들이 어떻게 되어가는지 따라올 수 있게 약간의 부연설명을 덧붙일 것이다. 부연설명이 필요없는 사람들은 이 단락을 넘기고 어플리케이션을 동작하는 마지막 섹션으로 바로 가도 된다. 새 프로젝트를 생성할 수 있는 몇가지 옵션들이 있다:


우리가 빌드할 완성된 프로젝트의 소스코드는 여기 Github에서 볼 수 있다. 필요하다면 프로젝트를 클론하여 돌려보고 바로 다음 단계로 건너뛰어도 된다.


Spring Tool Suite 이용하기

Spring Tool Suite (이클립스 플러그인의 한종류)의 메뉴 File->New->Spring Starter Project에서 프로젝트를 만들거나 불러올 수 있다.


홈페이지 추가하기 Add a Home Page

단일페이지 어플리케이션의 핵심은 하나의 정적인 "index.html"이다. 바로 프로젝트의 "src/main/resources/static" 또는 "src/main/resources/public"의 경로에 다음과 같이 하나 만들어보자:

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">
  <div class="container">
    <h1>Greeting</h1>
    <div ng-controller="home" ng-cloak class="ng-cloak">
      <p>The ID is {{greeting.id}}</p>
      <p>The content is {{greeting.content}}</p>
    </div>
  </div>
  <script src="js/angular-bootstrap.js" type="text/javascript"></script>
  <script src="js/hello.js"></script>
</body>
</html>

단지 "Hello World"를 출력하는 코드이므로 매우 간결하고 쉽다.


홈페이지의 특징 Features of the Home Page

이 홈페이지의 가장 주요한 기능은 다음과 같다:

  • <head>에서 CSS를 불러온다. 하나의 엘리먼트에는 실제로 아직 존재하지는 않지만 ("angular-bootstrap.css")와 같이 임시적으로 파일 이름을 채우며, 인라인 스타일시트가 "ng-cloak" 클래스를 정의 하고 있다.

  • "ng-cloak" 클래스는 <div> 컨탠트에 적용되어 해당 동적 컨텐트를 앵귤러JS가 처리할때까지 보지않게(hidden) 한다 - 초기 페이지 로딩중에 페이지가 깜빡거리는 현상을 막기 위해서다.

  • <body>에 마크된 ng-app="hello"는 앵귤러가 "hello"라 불리는 어플리케이션을 인식할 수 있게 만들어주는 하는 자바스크립트 모듈을 정의해야 한다는 의미다.

  • "ng-cloak"을 제외한 모든 CSS 클래스는 트위터 부트스트랩에서 불러온다. 스타일시트를 올바르게 설정해두면 페이지를 이쁘게 만들어줄것이다.

  • greeting안의 컨탠트는 {{greeting.content}}와 같이 핸들바를 이용하여 마크업되었다. 이것은 후에 앵귤러에 의해 실제 값으로 채워질 것이다. (<div>)로 둘러쌓인 ng-controller directive에 의해 "home" 이라 불리는 controller에 의해). 

  • 앵귤러JS (와 트위터 부트스트랩)은 <body>의 하단에 포함되어 브라우저가 모든 HTML을 처리한 이후 읽혀질것이다.

  • 또한 어플리케이션의 행동을 정의해둘 별도의 "hello.js"이 있다. 

곧 스크립트와 스타일시트 파일들을 만들테니 지금은 그들이 없는 파일이라는걸 무시하기로 하자.


어플리케이션 동작하기 Running the Application

일단 홈페이지 파일이 추가되면 (아직 많은 것 하지않았음에도 불구하고) 어플리케이션을 브라우저에서 실행할 수 있다. 커맨드라인상에 다음과 같이 타입해보자:

$ mvn spring-boot:run

그리고 브라우저를 열고 주소창에 http://localhost:8080를 타입한다. 홈페이지가 실행되면 브라우저 팝업창이 뜨며 아이디와 패스워드를 물어볼것이다. (아이디는 "user"이고 패스워드는 어플리케이션 시작시 콘솔로그에 출력된다). 아직 컨탠트가 없지만 인증에 성공하면 "greeting"헤더를 가지는 빈페이지를 볼 수 있을 것이다.

 패스워드 확인을 위해 콘솔창을 찾는게 싫다면 프로젝트의 "src/main/resources"의 위치에 application.properties 파일을 만들어 다음과 같이 추가하면된다: security.user.password=password (패스워드는 원하는대로 변경가능하다). "application.yml"파일을 만들어 넣어도 똑같이 동작한다.

IDE에선 그냥 어클리케이션 클래스의 main() 메소드를 실행하면 된다. 지금은 UiApplication이라는 하나의 클래스만 있다.

패키지 빌드하여 standalone JAR을 만들어 다음과 같이 실행해도 된다:

$ mvn package
$ java -jar target/*.jar

프론트엔드 에셋 Front End Assets

앵귤러JS나 다른 프론트엔드 기술의 초급 튜토리얼들은 주로 라이브러리 에셋을 직접 라이브러리와 연관된 여러개의 파일들을 다운받아 리소스 경로에 직접 넣는 것 대신 인터텟을 통해 바로 가져오라고 가르친다. (이를테면 앵귤러 JS 웹사이트에서는 Google CDN를 통해 다운받는것을 추천한다). 이것은 어플리케이션을 동작하는데 반드시 따라야하는 것은 아니지만 실제 제품화 단계에서 브라우저와 서버같의 불필요한 통신을 피할수 있는 최고의 실전 팁이다. 우리가 CSS 스타일시트를 변경하거나 커스터마이즈하지 않는다면 "angular-bootstrap.css"파일을 프로젝트에 만들어두는 것도 불필요하므로 Google CDN를 통해 정적인 에셋을 사용하면 된다. 하지만 실제 어플리케이션은 거의 대부분 스타일시트를 수정하면서도 CSS소스 자체에는 손대고 싶지않기 때문에 Less 나 Sass와 같은 상위레벨의 툴을 사용한다. 따라서 우리 또한 그렇게 해볼 것이다.

이것을 위한 많은 방법들이 있지만 이 섹션의 목적을 위해 우리는 자바기반의 프론트엔드 에센의 선처리 및 패키징 툴체인인 wro4j을 사용할 것이다. 그냥  서블릿 어플리케이션에서 JIT (Just in Time) Filter로서 사용할 수 있지만 메이븐이나 이클립스와 같은 빌드툴을 또한 매우 잘 지원한다. 그리고 우리가 이것을 쓰려는 이유다. 따라서 우리는 정적 리소스 파일들을 빌드하여 번들화시켜 우리의 어플리케이션 JAR에 넣을 것이다.

여담: Wro4j 는 아마도 하드코어 프론트엔드 개발자가 선택할 만한 툴은 아닐것이다. 그들은 통상 bower 와/또는 grunt와 같은 노드기반의 툴체인을 쓴다. 그러한 툴은 명백하게 아주 훌륭하고 인터넷상에 수많은 튜토리얼을 구할수 있다. 당신이 그것을 선호한다면 이 프로젝트내에서 자유롭게 쓰길 바란다. 그냥 프로젝트의 "src/main/resources/static" 경로에 그 툴체인으로 만들어진 결과파일/폴더를 넣으면 바로 잘 동작할것이다. 당신이 하드코어 프론트엔드 개발자가 아니라면 이 자바기반의 툴인 wro4j를 나름 편하게 활용할 수 있을 것이다.

빌드타임에 정적 리소스를 만드려면 메이븐 pom.xml에 약간의 마법을 뿌려야한다. (좀 장황하지만 boilerplate로서 메이븐의 parent pom안에서 또는 그래들의 shared task나 plugin으로 추출된다):

pom.xml

<build>
  <resources>
    <resource>
      <directory>${project.basedir}/src/main/resources</directory>
    </resource>
    <resource>
      <directory>${project.build.directory}/generated-resources</directory>
    </resource>
  </resources>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <plugin>
      <artifactId>maven-resources-plugin</artifactId>
      <executions>
        <execution>
          <!-- Serves *only* to filter the wro.xml so it can get an absolute
            path for the project -->
          <id>copy-resources</id>
          <phase>validate</phase>
          <goals>
            <goal>copy-resources</goal>
          </goals>
          <configuration>
            <outputDirectory>${basedir}/target/wro</outputDirectory>
            <resources>
              <resource>
                <directory>src/main/wro</directory>
                <filtering>true</filtering>
              </resource>
            </resources>
          </configuration>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <groupId>ro.isdc.wro4j</groupId>
      <artifactId>wro4j-maven-plugin</artifactId>
      <version>1.7.6</version>
      <executions>
        <execution>
          <phase>generate-resources</phase>
          <goals>
            <goal>run</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <wroManagerFactory>ro.isdc.wro.maven.plugin.manager.factory.ConfigurableWroManagerFactory</wroManagerFactory>
        <cssDestinationFolder>${project.build.directory}/generated-resources/static/css</cssDestinationFolder>
        <jsDestinationFolder>${project.build.directory}/generated-resources/static/js</jsDestinationFolder>
        <wroFile>${project.build.directory}/wro/wro.xml</wroFile>
        <extraConfigFile>${basedir}/src/main/wro/wro.properties</extraConfigFile>
        <contextFolder>${basedir}/src/main/wro</contextFolder>
      </configuration>
      <dependencies>
        <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>jquery</artifactId>
          <version>2.1.1</version>
        </dependency>
        <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>angularjs</artifactId>
          <version>1.3.8</version>
        </dependency>
        <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>bootstrap</artifactId>
          <version>3.2.0</version>
        </dependency>
      </dependencies>
    </plugin>
  </plugins>
</build>

위의 소스 그대로 당신의 POM에 붙여넣거나 Github에 있는 소스를 가져다 쓰면된다.

주요 사항은:

  • 우리는 (CSS와 스타일링을 위한 jquery와 부트스트랩, 그리고 비지니스 로직을 위한 앵귤러JS의) 의존성으로서 몇몇 webjars 라이브러리를 추가했다. 이러한 jar 파일안의 정적 리소스들의 일부는 우리가 만든 angular-bootstarp.* 같은 파일들을 포함할 것이다. 그러나 jar 자체는 어플리케이션에 같이 패키지 될 필요없다.

  • 트위터 부트스트랩은 JQuery에 의존성을 가지므로 같이 넣을 것이다. 부트스트랩을 쓸 필요없는 앵귤러JS 어플리케이션은 이것이 필요없다. 왜냐하면 앵귤러는 자체적으로 JQuery로 쓰려는 기능을 가지고 있기때문이다.

  • <resources/> 섹션에서 선언되었기 때문에 생성된 리소스들은 "target/generated-resources"폴더에 위치되며, 프로젝트를 빌드한 JAR안에 패키지될 것이다. (이클립스 m2e같은 메이븐 툴링을 쓴다는 가정하에) IDE에서는 클래스패스를 통해 쓸 수 있다.

  • wro4j-maven-plugin은 약간 이플립스 통합기능을 가지고 있어 이클립스 마켓플레이스에서 인스톨할 수 있다. (어플리케이션이 완성되면 필요없으므로 처음이면 나중에 인스톨하라). 인스톨하면 이클립스는 이후 리소스 파일들을 주시하고 변경사항이 있으면 결과물을 자동으로 재생성한다. 디버그 모드로 동작중이라면 브라우저에 변경사항을 즉시 반영한다.

  • Wro4j는 당신의 빌드 클래스패스를 알지 못하는 XML설정을 통해 제어하며 오직 파일의 절대경로만 인식한다. 그러므로 우리는 파일의 절대 경로를 만들어 wro.xml에 추가해야한다. 이것을 위해 메이븐 리소스 필터링을 사용할 것이며 이것이 왜 명시적으로 "maven-resources-plugin" 선언을 해줘야 하는가에 대한 이유다.

필요한 POM 수정은 이게 전부이며 "src/main/wro"에 위치할 wro4j 빌드파일을 추가할일만 남았다.


Wroj4j 소스파일들 Wro4j Source Files

Github의 소스파일엔 오직 3개 파일만 볼 수 있다. (그중 차후 커스터마이징을 위해 하나는 빈파일이다)

  • wro.properties는 wro4j 엔진의 선처리preprocessing와 랜더링을 위한 설정configuration 파일이다. 이 설정파일로 이 툴체인의 다양한 기능을 켜거나 끌수있다. Less로 부터 CSS를 컴파일하고 자바스크립트를 minify하려는 우리의 경우, 결과적으로 필요한 모든 라이브러리들의 소스를 섞어 2개의 파일로 합칠것이다.

    wro.properties
    preProcessors=lessCssImport
    postProcessors=less4j,jsMin
  • wro.xml는 "angular-bootstarp"이라는 단일 "그룹"을 선언하며 생성되는 정적 리소스의 기본명으로 종료한다. 우리가 추가한 webjars의 <css> 와 <js> 엘리먼트의 참조형 및 로컬 리소스파일인 main.less를 또한 포함하고있다.

    wro.xml
    <groups xmlns="http://www.isdc.ro/wro">
      <group name="angular-bootstrap">
      <css>webjar:bootstrap/3.2.0/less/bootstrap.less</css>
        <css>file:${project.basedir}/src/main/wro/main.less</css>
        <js>webjar:jquery/2.1.1/jquery.min.js</js>
        <js>webjar:angularjs/1.3.8/angular.min.js</js>
      </group>
    </groups>
  • main.less는 예제코드로서 빈 파일이지만 UI의 Look & Feel이나 트위터 부트스트랩의 (밑의 한줄로) 기본 파란색을 옅은 핑크색으로 바꾸는 등의 기본설정을 바꾸거나, 커스터마이즈하는데 사용된다.

    main.less
    @brand-primary: #de8579;
  • 위의 파일들을 프로젝트로 복사해넣고 "mvn package"를 타입하면 JAR파일안에 "bootstarp-angular.*" 리소스 파일들을 볼 수 있을 것이다. 이제 앱을 실행하면, CSS가 작동하는것을 확인할수 있다. 하지만 아직 비지니스 로직과 페이지 이동기능이 여전히 빠져있다.


    앵귤러 어플리케이션 만들기 Create the Angular Application

    이제 "hello" 어플리케이션을 만들어보자 ("src/main/resources/static/js/hello.js" 경로에 만들어야 "index.html"의 하단의 <script/> 엘리먼트가 올바른 경로를 찾을수 있다)

    최소사양의 앵귤러JS 어플리케이션은 다음과 같다.

    hello.js
    angular.module('hello', [])
      .controller('home', function($scope) {
        $scope.greeting = {id: 'xxx', content: 'Hello World!'}
    })

    어플리케이션의 이름은 "hello"이고 빈 "config" 와 "home"이라는 이름의 "controller"를 가지고 있다. "home" controller는 "index.html"에 ng-controller="home"을 선언한 <div> 컨텐트때문에 "index.html"가 로드될때  같이 호출된다

    controller 함수안에 마법과도 같은 $scope을 주입했고 (앵귤러는 명명규칙naming convention에 따른 의존성주입 dependency injection by naming convention을 한다), 당신의 함수변수의 이름을 인식하고 있다는걸 명심하자. $scope은 컨트롤러가 책임지는 함수내에서 UI엘리먼트를 위한 컨텐트와 행동규약등을 설정해준다.

    당신이 이 파일을 "src/main/resources/static/js" 경로밑에 추가했다면 어플리케이션은 이제 안전하고 기능적이되었으며 "Hello World!"를 보여줄것이다. greeting은 HTML내에{{greeting.id}} 와 {{greeting.content}} 와 같은 placeholder들은 핸들바를 사용하는 앵귤러에 의해 랜더되었다. 


    동적 컨텐트 추가하기 Adding Dynamic Content

    지금까지 우리는 하드코드된 greeting을 가진 어플리케이션을 만들었다. 어떻게 서로가 맞추어가는지 배우면 유용할것이다. 그러나 실제로 우리가 기대한 컨텐트는 서버로부터 오는것이므로 이제 HTML endpoint를 만들어 greeting을 받아올 것이다. "src/main/java/demo"에 있는 어플리케이션의 메인클래스에  @RestController 어노테이션과 새 @RequestMapping을 정의하자:

    UiApplication.java
    @SpringBootApplication
    @RestController
    public class UiApplication {
    
      @RequestMapping("/resource")
      public Map<String,Object> home() {
        Map<String,Object> model = new HashMap<String,Object>();
        model.put("id", UUID.randomUUID().toString());
        model.put("content", "Hello World");
        return model;
      }
    
      public static void main(String[] args) {
        SpringApplication.run(UiApplication.class, args);
      }
    
    }

    새프로젝트를 어떻게 만들었냐에 따라 UiApplication이 아닐 수도 있 다. @SpringBootApplication 대신 @EnableAutoConfiguration @ComponentScan @Configuration이 있을 수도 있다.

    어플리케이션을 실행하고 curl에서 "/resource" endpoint를 타입해보면 기본적으로 보안이 작동하여 다음과 같은 메세지를 볼 수 있다:

    $ curl localhost:8080/resource
    {"timestamp":1420442772928,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/resource"}

    앵귤러에서 동적리소스 불러오기 Loading a Dynamic Resource from Angular

    이제 브라우저에서 메세지를 받아봐보자. "home" controller를 XHR을 사용하여 보호된 리소스를 불러올수있게 수정하자:

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

    core기능으로 앵귤러가 제공해주는 $http서비스를 주입하고 GET메소드로 resource에 접근한다. 성공적으로 요청되면 앵귤러는 콜백으로 Response body로부터 JSON 타입을 돌려받는다.

    다시 어플리케이션을 실행하면 (또는 그냥 브라우저의 홈페이지를 다시 로드하면) Unique ID를 가진 동적 메세지를 볼수 있을것이다. 리소스가 보호된 상태라 직접적으로 curl을 사용할 순 없지만 브라우저는 컨텐트에 접근할 수 있다. 우리 이제 백줄도 안되는 코드로 단일페이지 보안 어플리케이션을 가지게 되었다!

    당신은 아마 파일 수정후 브라우저에 정적 리소스의 재실행을 강제하고 싶을 수 있다. 크롬(파이어폭스에선 플러그인을 통해)에서 "developer tools" 메뉴 (단축키 F12)를 통해 가능하며 또는 CTRL+F5를 사용하면 된다.


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

    브라우저와 백엔드간의 연동은 사용자의 브라우저에서 확인할 수 있다. 만일 사용자가 개발자 툴을 사용한다면 (보통 크롬에서는 기본값으로 F12를 누르면 열리고 파이어폭스에서는 플러그인을 설치해야할 것이다) 정리하자면:

    VerbPathStatusResponse

    GET

    /

    401

    인증을 위한 브라우저 팝업 프롬프트

    GET

    /

    200

    index.html

    GET

    /css/angular-bootstrap.css

    200

    트위터 부트스트랩 CSS

    GET

    /js/angular-bootstrap.js

    200

    부트스트랩과 앵귤러JS

    GET

    /js/hello.js

    200

    어플리케이션 로직

    GET

    /resource

    200

    JSON greeting

    브라우저가 단일 연동으로서 홈페이지를 로드한다고 간주하기때문에 사용자는 아마 401 응답을 볼 수 없을것이다. 그리고 /resource를 위한 두번의 요청을 볼수 있는데, 이는 CORS negotiation때문이다.

    요청들을 더 자세히 들여다 보면, 모든 요청에 다음과 같은 "인증Authorization"헤더가 있는 것을 볼 수 있다:

    Authorization: Basic dXNlcjpwYXNzd29yZA==

    브라우저는 모든 요청에 사용자명과 패스워드를 보내고 있는데 ( 그러므로 제품화단계에선 HTTPS으로만 써야하는걸 기억하자). 이런한 단계는 "앵귤러"와 무관하므로 사용자가 자바스크립트 프레임워크를 쓰던 안쓰던 잘 동작한다.


    무엇이 문제인가? What’s Wrong with That?

    표면적으로는 매우 잘 작동하는 것처럼 보인다. 간결하고 구현이 간단하며, 모든 데이터는 패스워드에 의해 안전하다. 그리고 사용자가 프로트엔드 또는 백엔드 기술세트를 바꾸더라도 여전히 잘 작동할 것이다. 하지만 약간의 이슈가 있는데:

    • 기본 인증은 사용자명과 패스워드 인증으로 제한되어있다.

    • 인증 UI는 흔하지만 매우 추하다 ( 브라우저 다이얼로그).

    • Cross Site Request Forgery (CSRF)로부터 보호되지않는다.

    CSRF는 실제로 백엔드 리소스에 GET만 사용하는 지금의 어플리케이션에는 문제가 안될 수도 있다 (예를 들면 서버의 상태변화가 없을때). 일단 사용자가 어플리케이션에 POST, PUT 또는 DELETE 를 사용한다면, 요즘의 기술로는 더 이상 안전하지 않다.

    이 시리즈의 다음 섹션에서, 우리는 HTTP Basic보다 훨씬 유연한 폼 기반의 인증을 사용하도록 어플리케이션을 확장할 것이다. 일단 우리가 폼을 사용하면 CSRF 보호를 할 필요가 있다. 스프링 시큐리티와 앵귤러 둘다 이것을 만드는데 도움을 주는 즉시 활용가능한 멋진 기능들을 가지고 있다. 약간 스포일로해보면, 우리는 HttpSession을 사용할 것이다.

    감사의 말: 이 시리즈를 만드는데 도움을 준 모든분께 감사드리고 싶다. 특히 각각의 섹션과 소스코드를 주의깊게 리뷰를 해주고, 내가 잘 알고 있다고 생각한 부분에서 조차 알지못했던 몇가지 트릭을 알게 해준 Rob Winch 와 Thorsten Spaeth.에게 감사하다.



    반응형

    반응형

    이 가이드는 어떻게 스프링 세션을 스프링 시큐리티와 함께 사용하는지 설명할 것이다. 당신의 어플리케이션에 스프링 시큐리티를 이미 적용했다는 가정하에 진행된다.

    완전한 가이드는 다음의 시큐리티 샘플 어플리케이션링크에서 확인할 수 있다.

    의존성 업데이트 하기 Updating Dependencies

    스프링 세션을 사용하기전, 의존성 업데이트를 해야한다. 메이븐을 쓴다면 다음의 의존성을 추가해주자:

    pom.xml
    <dependencies>
            <!-- ... -->
            <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
            <version>1.0.2.RELEASE</version>
            <type>pom</type>
            </dependency>
            <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>4.1.6.RELEASE</version>
            </dependency>
    </dependencies>

    스프링 설정 Spring Configuration

    필수 의존성을 추가하면 스프링 설정을 생성할 수 있다. 스프링 설정으로 Servlet 필터를 만들어 HttpSession 구현체를 스프링 세션의 엄호하에 사용되는 구현체로 바꿔줘야한다. 다음의 스프링 설정을 추가하자:

    @Configuration
    @EnableRedisHttpSession 1
    public class Config {
    
            @Bean
            public JedisConnectionFactory connectionFactory() {
                    return new JedisConnectionFactory(); 2
            }
    }
    1@EnableRedisHttpSession 어노테이션은 Filter를 구현한 springSessionRepositoryFilter라는 이름의 스프링 빈을 만들어준다. 이 필터는 HttpSession를 스프링 세션의 엄호하에 사용되는 구현체로 대체해준다. 이 인스턴스에서 스프링 세션은 레디스에 엄호하에 있다.
    2

    스프링 세션을 레디스 서버에 연결하는 RedisConnectionFactory를 만들었다.RedisConnectionFactory that connects Spring Session to the Redis Server. 기본포트인 6379로 연결하도록 설정한다. 스프링 데이터 레디스의 자세한 설정정보는 레퍼런스 문서를 참고하자.

    서블릿 컨테이너 초기화 Servlet Container Initialization

    우리는 스프링 설정으로 Filter를 구현한 springSessionRepositoryFilter라는 이름의 스프링 빈을 만들어 스프링 세션에 엄호하에 사용되는 커스텀 구현체로 HttpSession을 대체하는데 쓴다.

    우리의 Filter를 동작하게 하기 위해, 스프링은 우리가 설정한 Config 클래스를 불러와야한다. 어플리케이션이 이미 스프링 설정을 SecurityInitializer 클래스를 통해 실행되고 있기때문에 간단히 다음의 Config 클래스를 추가해주면된다:

    src/main/java/sample/SecurityInitializer.java
    public class SecurityInitializer extends
                    AbstractSecurityWebApplicationInitializer {
    
            public SecurityInitializer() {
                    super(SecurityConfig.class, Config.class);
            }
    }

    마지막으로 서블릿 컨테이너 (톰캣같은)가 모든 요청에 우리의springSessionRepositoryFilter 를 사용하게 만들야한다. 스프링세션의 springSessionRepositoryFilter가 스프링 시큐리티의 springSecurityFilterChain이 불러지기전에 호출되어야한다는 것은 극도로 중요하다. 스프링 시큐리티에서 사용하는 HttpSession은 반드시 스프링 세션의 엄호하에 있어야 한다. 다행스럽게도 스프링 세션은 이것을 극도로 쉽게 만들어주는 AbstractHttpSessionApplicationInitializer 이라는 유틸리티 클래스를 제공해준다. 밑의 예제를 보자:

    src/main/java/sample/Initializer.java
    public class Initializer extends AbstractHttpSessionApplicationInitializer {
    
    }

    클래스 이름 (Initializer)은 무엇이되도 상관없다. 중요한 것은 우리가 AbstractHttpSessionApplicationInitializer를 확장했다는 것이다.

    AbstractHttpSessionApplicationInitializer 를 확장함으로서, springSessionRepositoryFilter라는 이름의 스프링 빈이 스프링 시큐리티의 Security’sspringSecurityFilterChain이전에 모든 요청을 처리하도록 서블릿 컨테이너와 함께 등록될 것이다.

    시큐리티 샘플 어플리케이션 security Sample Application

    시큐리티 샘플 어플리케이션 실행하기 Running the security Sample Application

    소스 코드를 받아서 다음의 명령어를 호출함으로서 샘플을 실행할 수 있다:

    샘플을 동작하려면 먼저 localhost에 레디스 2.8 또는 그 이후버전을 인스톨하고 기본 포트(6379)로 실행하자. 설치 설정이 다르면 JedisConnectionFactory에 레디스 서버를 해주면 된다.

    $ ./gradlew :samples:security:tomcatRun

    이제 http://localhost:8080/를 통해 어플리케이션에 접근할 수 있다.

    시큐리티 샘플 어플리케이션 둘러보기 Exploring the security Sample Application

    어플리케이션을 사용해보자. 다음의 로그인 정보를 입력하자:

    • Username user

    • Password password

    Login 버튼을 누르면, 방금 입력한 사용자로 로그인됬다로 알려주는 메세지를 볼수 있다. 이 사용자 정보는 톰캣의 HttpSession 구현체가 아닌 레디스에 저장되어있다.

    어떻게 동작하는가? How does it work?

    톰캣의 HttpSession대신, 우리는 실제 레디스에 값을 저장하고 있다. 스프링 세션은 레디스의 엄호하에 있는 구현체로 HttpSession을 대체한다. 스프링 시큐리티의SecurityContextPersistenceFilter SecurityContextHttpSession 에 저장할때, 이는 레디스안에 저장된다.

    새로운 HttpSession이 만들어질때, 스프링 세션은 사용자의 브라우저에 사용자의 세션ID를 가지고 있는 SESSION이라는 쿠키를 만든다. 계속 진행해서 쿠키를 둘러보자. (크롬이나 파이어폭스에서 쿠키값을 확인해볼수 있다)

    redis-cli를 쓰면 세션값을 언제든지 레디스에서 손쉽게 삭제할 수 있다.예를 들면 리눅스 기반의 시스템에서는 다음과 같이 타입하면된다:

    $ redis-cli keys '*' | xargs redis-cli del
     redis-cli 설치는 레디스 문서를 참고하자.

    또는, 명시적인 키explicit key값을 삭제해도 된다. 7e8383a4-082c-4ffe-a4bc-c40fd3363c5e의 값을 가지는 세션 쿠키를 삭제하려면 다음을 터미널에 타입하자:

    $ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e

    이제 다시 http://localhost:8080/ 를 방문해보면 더이상 인증되어있지 않은 걸 확인할 수 있다.


    반응형

    + Recent posts