<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>
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 스프링 세션
- spring
- spring boot
- rest api
- 앵귤러JS
- spring security oauth2
- angular
- 스웨거
- 스프링 시큐리티
- angularjs
- 스프링부트
- 스프링 부트
- oauth2
- 스프링 시큐리티와 앵귤러 js
- Spring Framework
- 앵귤러
- Exception Handling
- controlleradvice
- 예외처리
- AUthorization
- spring security and angularjs
- Authentication
- swagger-ui
- 레디스
- spring session
- api documentation
- api 문서화
- 스프링
- spring framwork
- Spring Security
- Today
- Total
스프링부트는 사랑입니다
Spring Security and AngularJS Part VI 본문
스프링 시큐리티와 앵귤러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 창을 쓰는게 최선의 방법이다.
이 섹션에서 우리는 어떻게 스프링 시큐티리와 앵귤러JS로 단일 페이지 어플리케이션을 만드는지 계속 얘기해볼 것이다. 이제 두번째 섹션과 네번째 섹션에서 만들었던 시스템의 기능을 섞기위해 어떻게 스프링 클라우드와 스프링 세션을 함께 사용하는지 보여줄 것이다. 그리고 서로 다른 책임을 가지는 3개의 단일 페이지 어플리케이션을 만들어볼것이다. (네번째 섹션과 같은) 게이트웨이를 만드는 목표는 API리소스를 사용하는 것뿐만 아니라 백엔드로부터 UI를 불러오는데 사용된다. 우리는 백엔드에 인증을 통과하기 위해 게이트웨이를 사용함으로서 두번째 섹션의 토큰 논쟁을 간소화할 수 있다. 그럼 다음 우리는 게이트웨이에서 인증과 식별을 제어하고 있는 동안 백엔드에서 로컬 접근 결정 입자를 어떻게 만드는가 보여주기위해 시스템을 확장할 것이다. 이것은 일반적으로 분산 시스템을 만드는 수많은 혜택을 가진 강력한 모델이다:
기억하기: 만일 당신이 이섹션의 샘플을 동작시키려면 브라우저의 쿠키와 HTTP Basic credential에 대한 캐시값을 지워주어야 한다. 크롬에선 새 incognito 창을 쓰는게 최선의 방법이다.
타겟 아키텍쳐 Target Architecture
우리가 이제부터 만드려는 시스템의 기본 그림이다:
이 시리즈의 다른 샘플 어플리케이션과 같이 UI(HTML과 자바스크립트)와 리소스 서버를 가지고 있다. 네번째 섹션의 샘플같이 게이트웨이를 가지고 있으나 여기에선 UI의 일부가 아닌 별도로 분리되었다. UI는 효율적으로 백엔드의 일부가 되었다. 이것은 우리에게 재설정과 재구현의 기능에 더 많은 선택을 가져다 준다. 또한 우리가 앞으로 볼수 있듯 더 많은 혜택을 가져다 줄것이다.
브라우저의 모든 요청이 게이트웨이로 간다. 이것은 백엔드의 아키텍쳐에 대해 알 필요가 없다(기본적로 백엔드에 대해 아무것도 모른다.) 브라우저가 이 게이웨이에서 하는 일중 하나는 인증이다. 예를 들어 두번째 섹션과 같이 사용자명과 패스워드를 보내고 쿠키를 돌려받는다. 이어지는 요청에서 자동적으로 쿠키가 들어있고 게이트웨이는 이것으로 백엔드를 통과한다. 쿠키를 통과시키기위해 클라이언트에서 어떠한 코드도 필요하지않다. 백엔드는 쿠키를 사용하여 인증을 하고 모든 컴포넌트가 해당 사용자에 대한 같은 정보를 공유하는 세션을 공유하기 때문이다. 게이트웨이에서 쿠키가 억세스 토큰으로 변환되었던 다섯번째 섹션과는 반대로, 엑세스 토큰은 모든 백엔드 컴포넌트에 의해 각각 독립적으로 디코드되어져야 한다.
네번째 섹션에서 처럼, 게이트웨이는 클라이언트와 서버간의 연동을 간소화한다. 이는 보안을 다루는 작고 잘 정의된 표면으로 표현된다. 예를 들면, 우리는 Cross Origin Resource Sharing를 걱정할 필요가 없다. 이는 아주 쉽게 잘못될 수 있기 때문에 우리에게 안도감을 줄 것이다.
완전한 프로젝트의 소스코드는 이곳 Github에서 받을 수 있다. 따라서 그냥 프로젝트를 클론하여 당신이 원하는 곳에서 직접 돌려볼 수 있다. 이 시스템의 마지막 상태에 ("double-admin"이라는) 추가적인 컴포넌트가 있지만 지금은 당장 무시하도록하자
우리가 이제부터 만드려는 시스템의 기본 그림이다:
이 시리즈의 다른 샘플 어플리케이션과 같이 UI(HTML과 자바스크립트)와 리소스 서버를 가지고 있다. 네번째 섹션의 샘플같이 게이트웨이를 가지고 있으나 여기에선 UI의 일부가 아닌 별도로 분리되었다. UI는 효율적으로 백엔드의 일부가 되었다. 이것은 우리에게 재설정과 재구현의 기능에 더 많은 선택을 가져다 준다. 또한 우리가 앞으로 볼수 있듯 더 많은 혜택을 가져다 줄것이다.
브라우저의 모든 요청이 게이트웨이로 간다. 이것은 백엔드의 아키텍쳐에 대해 알 필요가 없다(기본적로 백엔드에 대해 아무것도 모른다.) 브라우저가 이 게이웨이에서 하는 일중 하나는 인증이다. 예를 들어 두번째 섹션과 같이 사용자명과 패스워드를 보내고 쿠키를 돌려받는다. 이어지는 요청에서 자동적으로 쿠키가 들어있고 게이트웨이는 이것으로 백엔드를 통과한다. 쿠키를 통과시키기위해 클라이언트에서 어떠한 코드도 필요하지않다. 백엔드는 쿠키를 사용하여 인증을 하고 모든 컴포넌트가 해당 사용자에 대한 같은 정보를 공유하는 세션을 공유하기 때문이다. 게이트웨이에서 쿠키가 억세스 토큰으로 변환되었던 다섯번째 섹션과는 반대로, 엑세스 토큰은 모든 백엔드 컴포넌트에 의해 각각 독립적으로 디코드되어져야 한다.
네번째 섹션에서 처럼, 게이트웨이는 클라이언트와 서버간의 연동을 간소화한다. 이는 보안을 다루는 작고 잘 정의된 표면으로 표현된다. 예를 들면, 우리는 Cross Origin Resource Sharing를 걱정할 필요가 없다. 이는 아주 쉽게 잘못될 수 있기 때문에 우리에게 안도감을 줄 것이다.
완전한 프로젝트의 소스코드는 이곳 Github에서 받을 수 있다. 따라서 그냥 프로젝트를 클론하여 당신이 원하는 곳에서 직접 돌려볼 수 있다. 이 시스템의 마지막 상태에 ("double-admin"이라는) 추가적인 컴포넌트가 있지만 지금은 당장 무시하도록하자
백엔드 만들기 Building the Backend
이 아키텍쳐에서 백앤드는 세번째 섹션에서 만들었던 이례적으로 실제 로그인페이지가 필요하지 않았던 스프링 세션 샘플과 매우 유사하다. 우리가 여기서 원하는 것을 얻는 가장 쉬운 방법은 아마도 첫번째 섹션의 "기초" 샘플에서 UI를 떼어내고, 세번째 섹션으로부터의 "리소스"서버를 복사하는 것이다. "기본" UI를 얻기위해 몇가지 의존성을 추가해주면 된다.(세번째섹션에서 스프링 세션을 처음 사용했을 때와 같이):
pom.xml그리고 메인 어플리케이션 클래스에 @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" 설정은 스프링 시큐리티가 인증 토큰으로 쿠키를 받아들인다는 의미로 만일 이미 존재하지 않는다면 그들을 만들지 않을 것이다.
이 아키텍쳐에서 백앤드는 세번째 섹션에서 만들었던 이례적으로 실제 로그인페이지가 필요하지 않았던 스프링 세션 샘플과 매우 유사하다. 우리가 여기서 원하는 것을 얻는 가장 쉬운 방법은 아마도 첫번째 섹션의 "기초" 샘플에서 UI를 떼어내고, 세번째 섹션으로부터의 "리소스"서버를 복사하는 것이다. "기본" UI를 얻기위해 몇가지 의존성을 추가해주면 된다.(세번째섹션에서 스프링 세션을 처음 사용했을 때와 같이):
그리고 메인 어플리케이션 클래스에 @EnableRedisHttpSession
어노테이션을 추가하자:
@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.propertiesserver.port: 9000
security.sessions: NEVER
여기 github에 필요시 둘러볼 수 있는 완전한 샘플이 있다.
리소스 서버는 우리가 이미 만들어놓은 샘플들중하나로 쉽게 만들수 있다. 두번째 섹션의 "spring-session" 리소스 서버와 동일하다: 단지 "/resource" endpoint과 분산 세션 데이터를 얻기 위한 @EnableRedisHttpSession
. 우리는 이 서버를 기본 포트값이 아닌 특정 값을 설정해주어 세션에서 인증을 찾아볼 때 쓸 수 있길 원한다. 이것을 위해서 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.ymlzuul:
routes:
ui:
url: http://localhost:8081
resource:
url: http://localhost:9000
security:
user:
password:
password
sessions: ALWAYS
프록시에는 2개의 라우트가 있다, 하나는 각각 UI와 리소스 서버를 위한 것으로 우리는 기본 패스워드와 세션 저장 전략session persistence strategy을 설정하였다. (스프링 시큐리티에 항상 인증을 위해 세션을 만들라고 알려줌으로서). 이 마지막 단계는 우리가 인증하기를 원하기 때문에 세션이 게이트웨이에서 관리되기 때문에 중요하다.
(작동하는 가장 간단한) 게이트웨이의 초기 구현을 위해, 우리는 아무것도 없는 스프링 부트 껍데기 어플리케이션을 가지고 @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
에서 이름을 바꿔주자):
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로 열수 있고 파이어폭스는 플러그인을 설치해야한다) 브라우저와 백엔드간의 연동과 당신의 브라우저에서 확인할 수 있다. 여기 정리해두었다:
Verb Path Status Response 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인데 이들은 당신이 인증을 위해 설정해둔 제어를 한 보안이 설정된 게이트웨이와 함께 연결되었다. 만일 백엔드가 브라우저에서 접근가능하지 않다해도 상관없다 (사실 아마도 당신이 물리적인 보안을 더욱 제어할 수 있는 점에서 장점이 될 것이다)
우리는 이제 3개의 포트에서 동작하는 3개의 컴포넌트를 갖췄다. 브라우저의 http://localhost:8080/ui/ 를 열면 당신은 이제 HTTP Basic challenge를 받아야 하며 "user/password"로 (게이트웨이에 당신의 credential로서) 인증할 수 있다. 그리고 일단 인증받으면, 리소스 서버에 프록시를 거친 백앤드 호출을 경유하여, UI에서 greeting을 볼 수 있게 된다.
당신이 개발자툴을 사용중이라면 (보통 크롬에서 F12로 열수 있고 파이어폭스는 플러그인을 설치해야한다) 브라우저와 백엔드간의 연동과 당신의 브라우저에서 확인할 수 있다. 여기 정리해두었다:
Verb | Path | Status | Response |
---|---|---|---|
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의 샘플을 보면, "로그아웃" 버튼을 가진 작은 네비게이션 바 또한 볼 수 있을 것이다. 여기 로그인 폼의 스크린샷을 보자:
로그인 폼을 지원하려면, 우리는 <form/>
안에 선언한 login()
함수를 구현한 navigation" 컨트롤러를 가진 자바스크립트가 필요하다. 그리고 authenticated
플래그를 설정해서 홈페이지가 사용자가 인증을 받았는지 안받았는지에 따라 다르게 랜더할 것이다. 예를 들어:
gateway.jsangular.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.jsvar 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을 기억하는 대신, 우리는 그냥 홈페이리를 부른 다음 다음의 링크를 통하면된다. 인증된 사용자를 위한 홈페이지를 보자:
첫번째 섹션의 "기본" 샘플에서 처럼, 우리는 이제 예를 들면 두번째 섹션으로부터 복사함으로서, 게이트웨이에 로그인폼을 추가할 수 있다. 우리가 이것을 할때 또한 기본 네비게이션 엘리면트를 게이트웨이에 추가해줄 수 있다. 그래서 사용자가 프록시의 UI 백엔드 경로를 알 수 없게 한다. 게이트웨이안에 "단일"UI로부터의 정적인 에셋assets을 먼저 복사하고 (어딘가의 <body/>
안에 위치한) 홈페이지안의 로그인 폼을 삽입하고 메세지 랜더링을 삭제하자:
<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>
메세지 랜더링 대신, 우리는 더 멋진 커다란 네비게이션 버튼을 만들 것이다:
<div class="container" ng-show="authenticated">
<a class="btn btn-primary" href="/ui/">Go To User Interface</a>
</div>
github의 샘플을 보면, "로그아웃" 버튼을 가진 작은 네비게이션 바 또한 볼 수 있을 것이다. 여기 로그인 폼의 스크린샷을 보자:
로그인 폼을 지원하려면, 우리는 <form/>
안에 선언한 login()
함수를 구현한 navigation" 컨트롤러를 가진 자바스크립트가 필요하다. 그리고 authenticated
플래그를 설정해서 홈페이지가 사용자가 인증을 받았는지 안받았는지에 따라 다르게 랜더할 것이다. 예를 들어:
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()
함수의 구현은 두번째 섹션과 유사하다:
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을 기억하는 대신, 우리는 그냥 홈페이리를 부른 다음 다음의 링크를 통하면된다. 인증된 사용자를 위한 홈페이지를 보자:
백엔드의 접근결정 단위 Granular Access Decisions in the Backend
이제 우리의 어플리케이션이 기능적으로 두번째 섹션과 네번째 섹션에 있는 것과 유사해졌지만, 추가적인 전담의 게이트웨이를 가지고 있다. 이 추가 레이어의 장점은 아직 명확하진 않지만 우리가 시스템을 확장할때 좀 더 강점을 가질 것이다. 우리가 서로 다른 백엔드 UI에 노출하기하기 위해 이 게이트웨이를 사용한다고 가정하보자, 메인UI에서 컨텐트를 "감시감독하기 위한" 사용자를 위해, 그리고 특별한 롤을 가진 사용자에만 이 기능의 접근을 제한하고자 원할때 우리는 프록시뒤에 "어드민" 어플리케이션을 추가 할것이다. 그리고 그 시스템은 다음과 같다:
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.jsvar computeDefaultTemplate = function(user) {
$scope.template = user && user.roles
&& user.roles.indexOf("ROLE_WRITER")>0 ? "write.html" : "read.html";
}
그 후 컨트롤러가 로드될때 유틸리티 함수를 사용하자:
admin.jsangular.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_" 접두사가 붙은 역할은 스프링 시큐리티 설정에서가 아니라 명확한 "역햘"의 메소드이름으로 작업에 초점을 맞추기위해 자바스크립트에서 필요하다.
이제 우리의 어플리케이션이 기능적으로 두번째 섹션과 네번째 섹션에 있는 것과 유사해졌지만, 추가적인 전담의 게이트웨이를 가지고 있다. 이 추가 레이어의 장점은 아직 명확하진 않지만 우리가 시스템을 확장할때 좀 더 강점을 가질 것이다. 우리가 서로 다른 백엔드 UI에 노출하기하기 위해 이 게이트웨이를 사용한다고 가정하보자, 메인UI에서 컨텐트를 "감시감독하기 위한" 사용자를 위해, 그리고 특별한 롤을 가진 사용자에만 이 기능의 접근을 제한하고자 원할때 우리는 프록시뒤에 "어드민" 어플리케이션을 추가 할것이다. 그리고 그 시스템은 다음과 같다:
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)이다. 게이트웨이에서 우리는 단지 사용자 계정에 역할이 필요한지 그리고 이 정보가 이용가능한지를 확실히 해주기만 하면 된다. 그러나 게이트웨이는 이것을 어떻게 해셕하는지에 대해 알필요가 없다. 게이스웨이에서 우리는 샘플어플리케이션에 스스로 사용자계정을 갖추도록 만들어준다:
@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"역할을 위해, 스프링 시큐리티에 다음과같이해보자:
@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"를 위해 어플리케이션 자체를 나누었다. 어플리케이션이 자바스크립트로 구현되었으므로 우리는 접근결정을 만들어줘야한다. 이것을 하는 하나의 방법은 이것을 내장한 뷰를 가진 홈페이지를 만드는 것이다:
<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"속성값을 검토하여 템플릿을 불러올것이다.
더 복잡한 어플리케이션은 아마 자체로 모듈화한 메카니즘을 사용할 것이다. 예를 들어 이 시리즈의 거의 모든 다른 어플리케이션에 사용했던 |
template
변수는 우리의 컨트롤러에서 초기화된다. 먼저 유틸리티 함수를 정의하자:
var computeDefaultTemplate = function(user) {
$scope.template = user && user.roles
&& user.roles.indexOf("ROLE_WRITER")>0 ? "write.html" : "read.html";
}
그 후 컨트롤러가 로드될때 유틸리티 함수를 사용하자:
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가 필요하다. 예를 들면 메인 어플리케이션 클래스에서:
@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.에게 감사하다. 첫번째 섹션이 발행된 이후 많은 수정을 하지 못했지만 다른 섹션들은 읽은 사람들의 통찰력과 답글들과 진화를 거듭했다. 그러므로 포럼에 참여하여 수고를 아끼지않아준 사람들과 이 섹션을 읽어준 사람들께도 감사하다.
이제 우리는 2개의 독립적인 UI와 백엔드 리소스 서버를 가진 작고 멋진 시스템을 갖추었다. 모든 것이 게이트웨이에서 똑같은 인증에 의해 보호되고있다. 게이트웨이가 마이크로 프록시(micro-proxy)로서 역할을 하고 있다는 사실은 백엔드 시큐리티에 대한 구현을 극도록 쉽게 만들어주었다. 그리고 그들은 자기들의 비지니스 관심사에만 집중할 수 있게 되었다. 스프링 세션의 사용은 또다시 수많은 번거로운 작업과 잠재적인 에러를 피할수 있게 해주었다.
백앤드가 선호하는 어떤 종류의 인증법을 독립적으로 가진다는 것(예를들어 당신이 물리적 주소와 로컬 credential의 셋트를 알고 있다면 UI로 직접 갈수 있다)은 매우 강력한 기능이다. 게이트웨이는 사용자를 인증하고 백엔드의 접근 규약을 만족시키는 사용자에 메타데이터를 할당할 수 있는한 관련이 없는 제약사항의 집합을 강제한다. 이는 백엔드 컴포넌트를 독립적으로 테스트하고 개발할수 있게 해주는 훌륭한 디자인이다. 만일 우리가 필요하다면 게이트웨이의 인증을 위해 외부 OAuth2 서버로 회귀할 수 있다 (다섯번째 섹션처럼 또는 완전히 다른 어떤한 것도 가능하다). 이경우도 백엔드는 손댈 필요가 없다.
이 아키텍쳐(인증을 제어하는 단일의 게이트웨이, 모든 컴포넌트에서 공유하는 세션 토) 의 보너스 기능은 "단일 로그아웃signgle logout"이다. 다섯번째 섹션에서 구현하기 복잡했던 기능으로 여기선 그냥 된다. 더 정확하게, 단일 로그아웃에 대한 사용자 경험에 대한 하나의 특별한 접근법이 우리의 완성된 시스템에 자동적으로 이용가능하다: 각각 개별의 UI가 같은 방식으로 "로그아웃"을 구현했다는 가정하에, 만일 사용자가 UI들중 아무 한군데에서( 게이트웨이, UI백엔드 또는 어드민 백엔드) 로그아웃을 하면, 그 사용자는 모든 곳에서 로그아웃이 된다(세션 무효화함으로서)
감사의 말: 이 시리즈를 만드는데 도움을 준 모든분께 감사드리고 싶다. 특히 각각의 섹션과 소스코드를 주의깊게 리뷰를 해주고, 내가 잘 알고 있다고 생각한 부분에서 조차 알지못했던 몇가지 트릭을 알게 해준 Rob Winch 와 Thorsten Spaeth.에게 감사하다. 첫번째 섹션이 발행된 이후 많은 수정을 하지 못했지만 다른 섹션들은 읽은 사람들의 통찰력과 답글들과 진화를 거듭했다. 그러므로 포럼에 참여하여 수고를 아끼지않아준 사람들과 이 섹션을 읽어준 사람들께도 감사하다.
'Tutorials' 카테고리의 다른 글
스프링부트와 OAuth2 - (1/4) (0) | 2016.03.19 |
---|---|
첫번째 스프링 부트 어플리케이션 개발하기 (레퍼런스 11장) (2) | 2015.12.03 |
Spring Security and AngularJS Part V (0) | 2015.12.03 |
Spring Security and AngularJS Part IV (0) | 2015.12.02 |
Spring Security and AngularJS Part III (0) | 2015.12.02 |