반응형

스프링 부트와 OAuth2

Spring Boot And OAuth2

(원문소스: https://spring.io/guides/tutorials/spring-boot-oauth2/)

Authorization 서버 돌리기 Hosting an Authorization Server

이 섹션에서 우리는 우리가 만들 앱을 여전히 페이스북과 Github을 사용하여 인증이 가능하지만 스스로 억세스 토큰을 만들 수 있는 완전무결fully-fledged한 OAuth2 인가서버Authorization Server로 만들도록 수정할 것이다. 이들 토큰은 이후 보호된secure 백엔드 리소스에서 사용되거나 똑같은 방식으로 보호되고 있는 다른 어플리케이션과의 SSO를 하는데 쓸 수 있다.

인증 설정을 정리하기 Tidying up the Authentication Configuration

인가서버Authorization server 기능을 시작하기 전에, 우리는 두개의 외부 제공자를 위한 설정코드를 깔끔하게 정리해보자. ssoFilter() 메소드안의 몇몇 코드가 중복되어있으므로 이를 공유 메소드로 가져올 것이다:

SocialApplication.java
private Filter ssoFilter() {
  CompositeFilter filter = new CompositeFilter();
  List<Filter> filters = new ArrayList<>();
  filters.add(ssoFilter(facebook(), "/login/facebook"));
  filters.add(ssoFilter(github(), "/login/github"));
  filter.setFilters(filters);
  return filter;
}

예전 메소드의 중복된 코드를 가지는 새로운 편리한 메소드이다:

SocialApplication.java
private Filter ssoFilter(ClientResources client, String path) {
  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter(
      path);
  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(client.getClient(),
      oauth2ClientContext);
  facebookFilter.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(new UserInfoTokenServices(
      client.getResource().getUserInfoUri(), client.getClient().getClientId()));
  return facebookFilter;
}

앱의 최종 버전에서 별도의 @Beans으로 선언된OAuth2ProtectedResourceDetails 그리고 ResourceServerProperties를 합친  새로운 wrapper 객체 ClientResources를 사용하였다:

SocialApplication.java
class ClientResources {

  private OAuth2ProtectedResourceDetails client = new AuthorizationCodeResourceDetails();
  private ResourceServerProperties resource = new ResourceServerProperties();

  public OAuth2ProtectedResourceDetails getClient() {
    return client;
  }

  public ResourceServerProperties getResource() {
    return resource;
  }
}

이 Wrapper와 전에 사용한 같은 YAML 설정을 함께 사용할 수 있지만, 각각 제공자를 위한 단일 메소드를 가지게 되었다:

SocialApplication.java
@Bean
@ConfigurationProperties("github")
ClientResources github() {
  return new ClientResources();
}

@Bean
@ConfigurationProperties("facebook")
ClientResources facebook() {
  return new ClientResources();
}

인가서버 돌리기 Enabling the Authorization Server

우리가 우리의 앱에 OAuth2 인가서버를 돌리려는데, 최소한 몇몇 기본기능(하나의 클라이언트와 억세스토큰을 만드는 능력)으로 시작하려고 수많은 삽질을 할 필요없다, 하나의 인가서버Authorization Server는 종단의 묶음이상이하도 아니다. 이들은 스프링 MVC 핸들러로서 스프링 OAuth2에서 구현되었다. 우리는 이미 보호된secure 어플리케이션을 가지고 있기때문에, 실제로 @EnableAuthorizationServer 어노테이션을 추가해주기만 하면 된다:

SocialApplication.java
@SpringBootApplication
@RestController
@EnableOAuth2Client
@EnableAuthorizationServer
public class SocialApplication extends WebSecurityConfigurerAdapter {

   ...

}

이 곳에 새 어노테이션을 추가함으로서, 스프링 부트는 우리가 지원하고 싶은 OAuth2 클라이언트에 약간의 디테일만 제공해주면, 그에 필요한 종단을 설치하고 그들에 시큐리티 설정을 해줄 것이다:

application.yml
security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
      scope: read,write
      auto-approve-scopes: '.*'

이 클라이언트는 우리가 외부제공자로서 필요한 facebook.client* and github.client* 와 동일하다. 외부제공자를 우리 앱에서 사용하기 위해 우리는 클라이언트 id 와 secret을 받아서 등록해 주어야 한다. 우리의 경우, 이미 스스로 똑같은 기능을 제공하고 있으기 때문에 작동확인을 위한 (최소한 하나의) 클라이언트가 필요하다.

우리는 모든 스쿠프와 정규식 매칭되는 auto-approve-scopes설정을 하였다. 이는 우리가 이 앱을 실제 시스템에 돌릴때는 필요하지않지만,  스프링 OAuth2가 우리의 사용자가 억세스 토큰을 요구할 때 설정된게 없다면 사용자에게 팝업을 띄울 대체자나 whitelabel 승인페이지 없이 빠르게 무언가를 동작할 수 있게 해준다. 토큰을 승인하는 명시적 승인절차를 추가하려면 UI를 제공하여 (/oauth/confirm_access위치의) whitelabel 버전을 대체해주어야 한다.

억세스토큰 받는 법 How to Get an Access Token

억세스 토큰은 이제 우리의 새 인가서버에서 사용이 가능하다. 토큰을 얻는 가장 간단한 방법은 "acme" 클라이언트로서 하나를 긁어오는 것이다. 앱을 실행하고 다음과 같이 curl을 보내면 이것을 확인할 수 있다:

$ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=client_credentials
{"access_token":"370592fd-b9f8-452d-816a-4fd5c6b4b8a6","token_type":"bearer","expires_in":43199,"scope":"read write"}

클라이언트 credential 토큰은 어떤한 환경하에서(이를테면 토큰 종단이 동작하는지 테스트하는 등) 유용하다. 하지만, 우리 서버의 모든 기능을 장점을 취하려면 사용자들이 토큰을 만들수 있게 해줘야한다. 우리 앱에 보통의 사용자 행동을 통해 토큰을 받으려면, 사용자를 인증할 수 있어야 한다. 앱이 시작할 때 로그를 주의깊게 살펴보면, 아마 기본 스프링부트 사용자 (Spring Boot User Guide 참고)를 위한 임의 패스워드에 대한 로그를 볼 수 있을 것이다. 당신은 이 패스워드를 "사용자" ID와 같이 사용하여 사용자 방식으로 토큰을 받을 수 있다:

$ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=password -d username=user -d password=...
{"access_token":"aa49e025-c4fe-4892-86af-15af2e6b72a2","token_type":"bearer","refresh_token":"97a9f978-7aad-4af7-9329-78ff2ce9962d","expires_in":43199,"scope":"read write"}

"…​"이 있는 곳을 실제 패스워드로 바꿔줘야한다. 이것을 "패스워드 승인password grant"라고 부르는데 억세스 토큰을 위해 사용자아이디와 패스워드를 교환하는 것이다. 패스워드 승인은 당신이 credential을 저장하거나 유효한지 확인할수 있는 로컬 사용자 데이터베이스를 가지고 있는 네이티브 또는 모바일 어플리케이션에 적합하다. 웹앱 또는 어떤 "소셜"로그인을 가진 앱에서는 "인가코드 승인authorization code grant"이 필요하다. 이는 리다이렉트를 처리하기 위해 그리고 외부제공자로부터 사용자 인터페이스를 랜더할수 있는 브라우저가 필요하다는 의미이다.

클라이언트 어플리케이션 만들기 Creating a Client Application

우리 인가서버를 위한 클라이언트 어플리케이션은 그 자체로 하나의 웹어플리케이션이다. 스프링 부트로 손쉽게 만들수 있는데 다음 예제를 보자:

ClientApplication.java
@EnableAutoConfiguration
@Configuration
@EnableOAuth2Sso
@RestController
public class ClientApplication {

	@RequestMapping("/")
	public String home(Principal user) {
		return "Hello " + user.getName();
	}

	public static void main(String[] args) {
		new SpringApplicationBuilder(ClientApplication.class)
				.properties("spring.config.name=client").run(args);
	}

}

클라이언트를 위한 요소들은 (단지 사용자의 이름을 출력하는) 하나의 홈페이지와 (spring.config.name=client를 통한) 설정파일을 위한 명시적 이름이다. 우리가 이 앱을 돌리면 우리가 다음과 같이 제공한 설정파일을 찾을 것이다:

client.yml
server:
  port: 9999
security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
      access-token-uri: http://localhost:8080/oauth/token
      user-authorization-uri: http://localhost:8080/oauth/authorize
    resource:
      user-info-uri: http://localhost:8080/me

이 설정은 우리가 메인 앱에서 사용한 값들과 거의 비슷한 것 같지만, 페이스북이나 Github 대신에 "acme" 클라이언트를 가진다. 이 앱은 메인 앱과의 충돌을 피하기 위해 9999포트를 통해 동작되며 우리가 아직 구현하지 않은 사용자 정보 종단인 "/me"를 참조한다.

사용자 정보 종단 보호하기 Protecting the User Info Endpoint

우리가 페이스북과 Github을 사용한 것과 같이 새 인가서버에서 싱글사인온을 사용하려면, 생성된 억세스 토큰에 의해 보호되는 /user 종단을 만들어줘야한다. 지금까지 우리는 /user 종단을 가지고 있었고 사용자가 인증할 때 만들어진 쿠키에 의해 보호되고 있었다. 이를 추가적으로 로컬에서 승인된 억세스 토큰으로 보호하려면, 현존의 종단을 재사용하여 새로운 경로로 alias를 만들어주면 된다:

SocialApplication.java
@RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
  Map<String, String> map = new LinkedHashMap<>();
  map.put("name", principal.getName());
  return map;
}

 우리는 브라우저에 노출하고 싶지 않는 정보를 숨기기 위해, 또한 두개의 외부 인증 제공자사이의 종단의 행동을 구별하기 위해 Principal을 Map 으로 변환하였다. 원칙적으로 우리는 여기에 제공자의 구제적인 고유식별자 또는 만일 이용가능하다면 이메일과 같더 자세한 정보를 추가 할 수 있다.

이제 우리의 앱에 의해 만들어진 억세스 토큰에 보호되는 "/me" 경로는 (인가서버일 뿐아니라) 하나의 리소스 서버이다. 우리는 새 설정클래스를 만들었다. (메인앱의 n 내부 클래스로서, 하지만 이는 또한 별도의 독립실행가능한standalone 클래스로 나눌 수 있다.):

SocialApplication.java
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration
    extends ResourceServerConfigurerAdapter {
  @Override
  public void configure(HttpSecurity http) throws Exception {
    http
      .antMatcher("/me")
      .authorizeRequests().anyRequest().authenticated();
  }
}

추가적으로 우리는 메인 어플리케이션의 시큐티리를 위해 @Order를 명시해줘야한다:

SocialApplication.java
@SpringBootApplication
...
@Order(6)
public class SocialApplication extends WebSecurityConfigurerAdapter {
  ...
}

@EnableResourceServer 어노테이션은 기본값으로 @Order(3)를 가지는 시큐리티 필터를 만든다. 메인 어플리케이션 시큐리티를 @Order(6)로 옮김으로서 우리는 "/me"이 우선순위를 가지도록 보장해줄 수 있다.

OAuth2 클라이언트 테스트하기 Testing the OAuth2 Client

두개의 앱을 모두 동작시키고 브라우저오 127.0.0.1:9999를 방문하여 새 기능을 테스트해보자. 클라이언트 앱을 로컬 인가서버로 리다이렉트될 것이고 이후 사용자에게 페이스북이나 Github으로 인증을 선택하도록 할 것이다.  일단 테스트 클라이언트로 제어가 완전히 되돌아오면, 로컬 억세스 토큰은 승인되고 인증이 완료된다. (브라우저에서 "Hello" 메시지를 볼 수 있을 것이다). 만인 이미 Github이나 페이스북으로 인증이 되어있다면 이 원격 인증을 아마 알아차리지 못했을 것이다.

 테스트 클라이언트 앱에서 "localhost"를 사용하지 말자. 이는 메인 앱으로부터 쿠키를 빼앗아오기 때문에 인증이 엉망이 되어버린다. 만일 127.0.0.1이 "localhost"와 매핑되어 있지 않으면, 당신은 당신의 OS에서 이를 설정해주어야한다. (이를테면 "/etc/hosts") 또는 만일 있다면 다른 로컬 주소를 사용하자.

비인증 사용자를 위한 에러페이지 추가하기 Adding an Error Page for Unauthenticated Users

이 섹션에서 우리는 이전에 만든 Github 인증으로 전환하는 logout 앱을 수정하여 인증받지 못한 사용자를 위한 약간의 처리를 추가해볼 것이다. 동시에 우리는 특정 Github 조직에 속해있는 사용자들만 인증하도록 허용하는 규칙을 포함하도록 인증로직을 확장해 볼 것이다. "조직organization"은 Github만의 특정 개념이지만 유사한 규칙을 이를테면 구글같은 당신이 특정 도메인으로부터의 사용자만을 인증하도록 다른 제공자에게 적용할 수 있다

Github으로 전환하기 Switching to Github

logout 예제는 OAuth2 제공자로서 페이스북을 사용했다. 우리는 이를 로컬 설정을 수정함으로서 손쉽게 Github으로 전환할 수 있다:

application.yml
security:
  oauth2:
    client:
      clientId: bd1c0a783ccdd1c9b9e4
      clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
      accessTokenUri: https://github.com/login/oauth/access_token
      userAuthorizationUri: https://github.com/login/oauth/authorize
      clientAuthenticationScheme: form
    resource:
      userInfoUri: https://api.github.com/user

클라이언트에서 인증실패 감지하기 Detecting an Authentication Failure in the Client

클라이언트에서 우리는 인증하지 못한 사용자들에게 약간의 피드백을 제공해주려고 한다. 이를 제공해주려고 우리는 정보 메시지를 가진 div를 추가하였다:

index.html
<div class="container text-danger" ng-show="home.error">
There was an error (bad credentials).
</div>

이 텍스트는 컨트롤러에서 "error'플래그가 설정되었을때만 보여진다. 이를 위해 다음과 같이 약간의 코드가 필요하다:

index.html
angular
  .module("app", [])
  .controller("home", function($http, $location) {
    var self = this;
    self.logout = function() {
      if ($location.absUrl().indexOf("error=true") >= 0) {
        self.authenticated = false;
        self.error = true;
      }
      ...
    };
  });

"home" 컨트롤러는 브라우저가 로드될때 그 위치를 체크한다. 만일 URL에 "error=true"가 있다면 플래그가 설정된다.

에러페이지 추가하기 Adding an Error Page

클라이언트에서 플래그설정을 지원하기 위해 인증에러를 잡아내서 쿼리 파라메터안에 설정된 이 플래그를 가진 홈페이지로 리다이렉트할 수 있어야한다. 따라서 우리는 다음과 같은 보통의 @Controller내에 하나의 종단이 필요하다:

SocialApplication.java
@RequestMapping("/unauthenticated")
public String unauthenticated() {
  return "redirect:/?error=true";
}

이 예제 앱에서 우리는 메인 어플리케이션 클래스에 이것을 넣어주었다. 이제 이는 @RestController가 아니라 @Controller로 바꿔줌으로서 이제 리다이렉트를 처리할 수 있다. 우리가 마지막으로 해주어야할 것은 비인증 응답(UNAUTHORIZED로 알려진 HTTP 401)을 "/unauthenticated" 종단으로매핑해주는 것이다. 그냥 다음과 같이 추가해주면 된다:

ServletCustomizer.java
@Configuration
public class ServletCustomizer {
  @Bean
  public EmbeddedServletContainerCustomizer customizer() {
    return container -> {
      container.addErrorPages(
          new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthenticated"));
    };
  }
}

(이 예제에선, 단지 간결함을 위해 이는 메인 클래스 안에 내장된 클래스nested class로서 추가된다)

서버에서 401 만들기 Generating a 401 in the Server

401 응답은 사용자가 Github으로 로그인 안하거나 못할 경우 이미 스프링 시큐리티로 부터 보내지고 있다. 따라서 앱은 사용자가 인증에 실패할 경우 이미 동작하고 있는 것이다. (예를 들어, 토큰승인을 거절함으로서)

여기에 양념을 조금 더 쳐서, 우리는 인증 규칙을 확장하여 올바른 "조직"에 속해있지않은 사용자의 인증을 거부할 것이다. 사용자에 대해 더 많은 정보는 Github API를 사용하면 쉽다. 우리는 단지 인증 처리의 올바를 부분에 이것을 플러그해주기만 하면 된다. 운좋게도 이러한 간단한 사용예를 위해 스프링 부트는 손쉬운 확장점을 제공해주고 있다: 만일 우리가 타입 AuthoritiesExtractor @Bean을 선언해주면, 이는 인증된 사용자의 인가Authorities (보통은 "역할roles")를 구성하는데 사용될 것이다. 우리는 사용자가 올바른 조직에 속해있는지 확인하는데 사용할 수 있으면, 아닐 경우 예외가 발생한다:

SocialApplication.java
@Bean
public AuthoritiesExtractor authoritiesExtractor(OAuth2RestOperations template) {
  return map -> {
    String url = (String) map.get("organizations_url");
    @SuppressWarnings("unchecked")
    List<Map<String, Object>> orgs = template.getForObject(url, List.class);
    if (orgs.stream()
        .anyMatch(org -> "spring-projects".equals(org.get("login")))) {
      return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
    }
    throw new BadCredentialsException("Not in Spring Projects origanization");
  };
}

우리가 이 메소드안에 OAuth2RestOperations을 autowired했다는걸 기억하자, 우리는 이를 인증받은 사용자의 행동을 위한 Github API에 접근하는데 사용할 수 있다. 우리는 이렇게 하여 (스프링 오픈소스 프로젝트를 저장하는데 사용되는 조직인) "spring-projects"와 매치되는 조직들중 하나를 찾는다. 당신이 스프링 엔지니어링 팀의 일원이 아니라면 성공적으로 인증을 받기 위해 당신 자신의 값으로 대체하면 된다. 매치되는 값이 없다면 BadCredentialsException 예외가 발생하여 스프링 시큐리티가 이를 받아서 401 응답을 낸다.

 명맥하게 위의 코드는 어떤것은 Github에, 어떤것은 다른 OAuth2제공자에게 적용하도록 다른 인증 규칙로 일반화할 수 있다. 당신이 필요한건 제공자의 API에 대한 약간의 지식과 OAuth2RestOperations이 전부다.

결 론 Conclusion

우리는 스프링 부트와 스프링 시큐리티를 사용하여 최소한의 노력으로 수많은 스타일을 가진 앱을 만드는 법을 보아왔다. 모든 예제를 통해 작동하는 주요 테마는 외부 OAuth2 제공자를 사용하는 "소셜"로그인이다. 마지막 예제조차 "내부적으로" 그러한 서비스를 제공하는데 사용된다. 왜냐하면 이는 외부 제공자들이 가지고 있는 동일한 기본기능을 가지기 때문이다. 모든 예제 앱들은 더욱 구체적인 사용예를 위해 손쉽게 확장하고 재설정이 가능한데 보통은 하나의 설정파일을 수정하는것 이상도 이하도 아니다. 만일 당신이 페이스북이나 Github (또는 유사한) 제공자를 통해 등록하기하고 자신의 호스트 주소에 클라이언트 credentials을 얻으려면 자신만의 서버 버전을 사용해야한다는 것을 기억하자 또한 이들 credential을 소스컨트롤에 올리는 일이 없도록 주의하자!


반응형

반응형

스프링 부트와 OAuth2

Spring Boot And OAuth2

(원문소스: https://spring.io/guides/tutorials/spring-boot-oauth2/)

OAuth2 클라이언트의 수동설정 Manual Configuration of OAuth2 Client

이 섹션에서 우리는 @EnableOAuth2Sso 어노테이션의 '마법'으로 이미 만들어본  logout 앱을 모든 설정을 명시적으로 직접 설정하도록 수정해볼 것이다.

클라이언트와 인증 Clients and Authentication

 @EnableOAuth2Sso에는 OAuth2 클라이언트와 인증의 2가지 기능이 있다. 클라이언트는 재사용이 가능하고, 또한 당신의 인가서버Authorization Server (우리의 경우 페이스북)가 제공하는(우리의 경우 Graph API) OAuth2 리소스들과 상호작동하는데 사용할 수 있다. 이 인증 기능은 당신의 앱을 스프링시큐리티와 맞도록 맞춰준다. 일단 페이스북과 소통하는게 끝나면, 당신의 앱은 정확히 다른 보호된 스프링 앱secure Spring app과 똑같이 동작할 것이다.

클라이언트 

클라이언트 부분은 스프링 시큐리티 OAuth2에 의해 제공되며 @EnableOAuth2Client라는 다른 어노테이션을 쓴다. 따라서 이 수정의 첫 걸음은 @EnableOAuth2Sso를 삭제하고 더 낮은 레벨의 어노테이션으로 변경하는 것이다:

SocialApplication
@SpringBootApplication
@EnableOAuthClient
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {
  ...
}

일단 이렇게 하면 우리는 우리를 위해 만들어진 유용한 몇가지 기능을 가지게 된다. 먼저 OAuth2ClientContext를 주입할 수 있어, 우리의 시큐리티 설정에 추가할 인증 필터를 만드는데 쓸 수 있다:

SocialApplication
@SpringBootApplication
@EnableOAuthClient
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {

  @Autowired
  OAuth2ClientContext oauth2ClientContext;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/**")
      ...
      .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
  }

  ...

}

이 필터는 우리가 OAuth2ClientContext를 사용하는 새 메소드에서 생성된다:

SocialApplication
private Filter ssoFilter() {
  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
  facebookFilter.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()));
  return facebookFilter;
}

이 필터 역시 페이스북에 등록된 클라이언트 설정에 대해 알아야한다:

SocialApplication
  @Bean
  @ConfigurationProperties("facebook.client")
  OAuth2ProtectedResourceDetails facebook() {
    return new AuthorizationCodeResourceDetails();
  }

그리고 인증을 완료하려면 페이스북의 사용자 정보 종단user info endpoint이 어딘지 알아야 한다:

SocialApplication
  @Bean
  @ConfigurationProperties("facebook.resource")
  ResourceServerProperties facebookResource() {
    return new ResourceServerProperties();
  }

이 두개의 "정적인" 데이터 객체(facebook()과 facebookResource())에 우리가  @ConfigurationProperties. 로 설정된 @Bean 을 사용했다는 것을 알아두자. 이것은 우리가 application.yml를, 설정의 접두어를 security.oauth2대신 facebook로 사용하는 약간 새로운 포멧으로 전환할 수 있다는 것을 의미한다:

application.yml
facebook:
  client:
    clientId: 233668646673605
    clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
    accessTokenUri: https://graph.facebook.com/oauth/access_token
    userAuthorizationUri: https://www.facebook.com/dialog/oauth
    tokenName: oauth_token
    authenticationScheme: query
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://graph.facebook.com/me

리다이렉트 처리하기 Handling the Redirects

마지막 수정은 우리의 앱에서 페이스북으로 리다이렉트를 명시적으로 지원하게 만들어주는 것이다. 이는 서블릿 Filter를 가진 Spring OAuth2에서 처리되어진다. 이 필터는 어플리케이션 컨텍스트application context에서 이미 사용가능한데 우리가 @EnableOAuthClient선언을 해두었기 때문이다. 우리가 이 필터를 엮어서 사용하려면 그냥 스프링부트 어플리케이션의 올바른 순서right order안에서 호출해 주기만 하면 된다. 이것을 위해 우리는 FilterRegistrationBean이 필요하다:

SocialApplication.java
@Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(
    OAuth2ClientContextFilter filter) {
  FilterRegistrationBean registration = new FilterRegistrationBean();
  registration.setFilter(filter);
  registration.setOrder(-100);
  return registration;
}

우리는 이미 사용가능할 필터를 Autowire해두었고, 이 필터를 메인 스프링 시큐리티 필터가 불러지기 전에 호출되도록 충분히 낮은 순서로 등록하였다. 이 방법으로 우리는 인증요청의 예외exception발생을 통해 리다이렉트를 처리할 수 있다.

이 단계까지의 수정을 통해 앱은 동작이 가능하며, 실행시 지난 섹션에서 만드 logout예제와 동일하게 동작한다. 설정을 단계별로 쪼개고 몇시적으로 우리에게 가르쳐줌으로서 스프링 부트가 해주는 마법과 같은 자동화는 더이상 없다. (이는 단지 설정의 뼈대일 뿐이다). 그리고 이는 막바로 사용가능하게 자동으로 제공되었던 기능을 확장하기 위한, 우리 자신의 의사과 비지니스 요구사항을 추가하기 위한 준비가 된다.

Github으로 로그인하기 Login with Github

이 섹션에서 우리가 이미 페이스북 링크를 통해 로그인이 가능했던 앱을 Github 인증을 추가하여 사용자가 선택할 수 있는 링크를 추가하도록 기존의 app을 수정할 것이다. 

Github 링크 추가하기 Adding the Github Link

클라이언트에서의 수정은 매우 사소하다. 단지 또다른 링크만 추가해주면 된다:

index.html
<div class="container" ng-show="!home.authenticated">
  <div>
    With Facebook: <a href="/login/facebook">click here</a>
  </div>
  <div>
    With Github: <a href="/login/github">click here</a>
  </div>
</div>

원칙적으로, 일단 우리가 인증 제공자를 추가하려면 "/user" 종단으로부터 되돌아오는 데이터에 대해 더 신중해져야 한다. Github과 페이스북 둘다 사용자 정보안에 "name"필드가 똑같이 있다. 따라서 우리의 종단에 실제로 수정을 해줄 필요가 없다.

Github 인증필터 추가하기 Adding the Github Authentication Filter

서버단의 주요 수정은 우리의 새 링크로 부터 오는 "/login/github" 요청을 처리하는 부가적인 시큐리티 필터를 추가하는 것이다. 이미 우리는 ssoFilter() 메소드에서 만들어진 페이스북을 위한 커스텀 인증 필터를 가지고 있으므로, 인증 경로를 하나이상 처리할 수 있도록 그냥 기존의 것을 Composite필터를 써서 바꿔주기만 하면 된다

SocialApplication.java
private Filter ssoFilter() {

  CompositeFilter filter = new CompositeFilter();
  List<Filter> filters = new ArrayList<>();

  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
  facebookFilter.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()));
  filters.add(facebookFilter);

  OAuth2ClientAuthenticationProcessingFilter githubFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/github");
  OAuth2RestTemplate githubTemplate = new OAuth2RestTemplate(github(), oauth2ClientContext);
  githubFilter.setRestTemplate(githubTemplate);
  githubFilter.setTokenServices(new UserInfoTokenServices(githubResource().getUserInfoUri(), github().getClientId()));
  filters.add(githubFilter);

  filter.setFilters(filters);
  return filter;

}

우리의 예전 ssoFilter()의 코드는 하나는 페이스북, 다른 하나는 Github용으로 중복된다. 이 두개의 필터는 하나의 컴포지트composite로 합쳐진다.

facebook()과 facebookResource() 메소드 역시 유사하게 github()과 githubResource()로 추가해줘야한다:

SocialApplication.java
@Bean
@ConfigurationProperties("github.client")
OAuth2ProtectedResourceDetails github() {
	return new AuthorizationCodeResourceDetails();
}

@Bean
@ConfigurationProperties("github.resource")
ResourceServerProperties githubResource() {
	return new ResourceServerProperties();
}

그리고 이에 상응하는 설정들 역시 추가해준다:

application.yml
github:
  client:
    clientId: bd1c0a783ccdd1c9b9e4
    clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user

여기의 클라이언트 디테일들은 Github에 등록된 정보여야 하며 (페이스북과 동일하게) localhost:8080주소를 가르켜야한다.

앱은 이제 준비되었고 사용자가 페이스북 또는 Github으로 인증을 선택하도록 동작할 것이다.

로컬 사용자 데이터베이스를 추가하기 How to Add a Local User Database

많은 어플리케이션은 인증을 외부 제공자에의해 위임할 경우에도, 그들 사용자의 데이터를 로컬에서 가지고 있어야 한다. 여기에 그 코드를 보여주진 않을 것이지만 이를 쉽게 해주는 두가지 단계가 있다:

  1. 당신의 데이터베이스를 위한 백엔드를 선택한 후, 외부 인증으로부터 일부 또는 전체를 불러올 수 오는데 사용하는 커스텀 User 객체를 위한 (스프링 데이터 등을 사용하여) 리파지토리repository를 설정하라

  2. 당신의 /user 종단에서 리파지토리 검사에 의해 로그인된 각각 고유한 사용자를 위한  User 객체를 규정하자. 만일 현재의 Principal의 식별자를 가진 사용자가 이미 있다면, 업데이트하거나, 그렇지않다면 생성할 수 있다.

힌트: User 객체에 필드 하나를 추가하여 외부 제공자의 고유 식별자를 링크해주자 (사용자의 이름같은 게 아닌 무언가 고유한 것)


반응형

반응형

스프링 부트와 OAuth2

Spring Boot And OAuth2

(원문소스: https://spring.io/guides/tutorials/spring-boot-oauth2/)

기본페이지 추가하기 Add a Welcome Page

이 섹션에서 우리는 '페이스북에 로그인하기'라는 명시적 링크를 추가함으로서 방금 만든 simple앱을 수정할 것이다.  즉시 리다이렉트되는 대신, 새 링크는 홈페이지에 보여져 사용자가 로그인을 할지 안할지 선택할 수 있게 된다. 사용자가 링크를 클릭했을 때만 인증된 컨탠트가 보여질 것이다.

홈페이지의 조건적 컨탠트 Conditional Content in Home Page

어떠한 컨탠트를 사용자가 인증했을 때 또는 우리가 서버측 랜더링(이를테면 Freemaker또는 Thymeleaf)을 사용하지 않을지 조건적으로 랜더하거나 또는 자바스크립트를 조금 써서 우리가 그냥 브라우저에게 그것하라고 물어보게 할 수 있다. 이것을 위해 우리는 AngularJS를 쓸 것이다. 만약 당신이 선호하는 다른 프레임워크가 있다면 클라이언트 코드를 옮기는게 그다지 어렵지 않을 것이다.

앵귤러JS를 쓰기위해 우리는 앵귤러 앱 컨테이너로서 <body>에 마크해 주었다.

index.html
<body ng-app="app" ng-controller="home as home">
...
</body>

Body안의 <div> 엘리먼트들은 그것이 보여지는 부분들 제어하는 모델에 묶여bound질 수 있다.

index.html
<div class="container" ng-show="!home.authenticated">
	Login with: <a href="/login">Facebook</a>
</div>
<div class="container" ng-show="home.authenticated">
	Logged in as: <span ng-bind="home.user"></span>
</div>

이 HTML은 authenticated 플래그와 인증받은 사용자를 위한 user객체를 가진 "home" 컨트롤러를 필요로 한다. 여기 이들 기능의 간단한 구현체를 보자 (<body>)의 끝 줄에 이들을 넣자):

index.html
<script type="text/javascript" src="/webjars/angularjs/angular.min.js"></script>
<script type="text/javascript">
  angular.module("app", []).controller("home", function($http) {
    var self = this;
    $http.get("/user").success(function(data) {
      self.user = data.userAuthentication.details.name;
      self.authenticated = true;
    }).error(function() {
      self.user = "N/A";
      self.authenticated = false;
    });
  });
</script>

서버측 수정 Server Side Changes

이를 동작시키려면, 서버쪽에 약간 수정을 해줘야한다. "home" 컨트롤러는 현재 인증받은 사용자정보를 받아오는  "/user"라는 종단endpoint이 필요하다. 메인 클래스에서 다음과 같이 아주 쉽게할 수 있다:

SocialApplication
@SpringBootApplication
@EnableOAuth2Sso
@RestController
public class SocialApplication {

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

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

}

우리가 핸들러 메소드에 주입한 @RestController 와 @RequestMapping 그리고 java.util.Principal 등을 알아두자

위와 같이 /user 종단에 Principal 전체를 리턴하는 것은 매우 안좋은 발상이다. (브라우저 클라이언트에 노출해서는 안되는 정보를 포함하고 있기 때문이다.) 재빠른 동작을 위해 이렇게 했지만 이 가이드의 후반에 이 종단의 정보를 숨기도록 바꿀 것이다.

이 앱은 이제 사용자가 우리가 제공한 링크를 클릭함으로서 전처럼 인증할 수 있게 잘 동작할 것이다. 이 링크를 보이도록 만드려면 WebSecurityConfigurer를 추가하여 홈페이지의 보안을 꺼주어야 한다:

SocialApplication
@SpringBootApplication
@EnableOAuth2Sso
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {

  ...

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .antMatcher("/**")
      .authorizeRequests()
        .antMatchers("/", "/login**", "/webjars/**")
        .permitAll()
      .anyRequest()
        .authenticated();
  }

}

스프링 부트는 @EnableOAuth2Sso 어노테이션을 가진 클래스의 WebSecurityConfigurer에 특별한 의미를 부여한다: OAuth2 authentication processor를 가진 시큐리티 필터체인security filter chain을 쓸 수 있도록 설정한다. 홈페이지를 보이도록 만드는 데 필요한건 홈페이지와 관련된 정적인 리소스를 authorizeRequests()에 명시하는 것이다. (우리는 인증을 다루는데 필요한 로그인 종단의 접근 또한 포함할 것이다.). 모든 이외의 요청들(예를 들어, /user종단) 인증이 필요하다.

이 단계의 수정을 거쳐 어플리케이션은 완성되었다. 서버를 돌리고 홈페이지를 방문하면 "Login with Facebook"이라는 멋지게 스타일된 HTML링크를 볼 수 있을 것이다. 이 링크는 당신을 페이스북으로 직접 가게해주는게 아니라, 인증을 처리하는 로컬 경로로 데려다준다.(그리고 페이스북으로 리다이렉트를 보낸다). 일단 인증을 받았다면, 로컬앱으로 다시 리다이렉트 되어, 이제 당신의 이름을 보여줄 것이다. ( 당신이 페이스북에 데이터접근을 허락한다고 설정했을 경우)

로그아웃 버튼 추가하기 Add a Logout Button

이 섹션에서 우리는 사용자가 앱을 로그아웃하도록 버튼을 추가함으로서 click 앱을 수정할것이다. 이는 간단한 기능같이 보일지 모르지만 사실 구현에 매우 신중해야한다. 정확히 어떻게 이렇게 하는지 약간의 시간을 들여 곱씹어볼 가치가 있다. 사실 대부분의 수정은 읽기 전용 리소스에서 읽고 쓰기가 가능한 리소스로 (로그아웃을 한다는 것은 상태변경을 해주어야한다) 앱을 변환하는 작업을 해주는 것이다. 단지 정적인 컨텐트가 아닌 어떤 실제 어플리케이션에서 같은 변경을 해주어야한다.

클라이언트측 변경 Client Side Changes

클라이언트에서 우리는 그냥 로그아웃 버튼과 자바스크립트로 서버에 인증을 취소할지 물어보는 호출만 제공해주기만 하면 된다. 먼저, UI의 "인증받은authenticated" 섹션에서 버튼을 추가하자: 

index.html
<div class="container" ng-show="home.authenticated">
  Logged in as: <span ng-bind="home.user"></span>
  <div>
    <button ng-click="home.logout()" class="btn btn-primary">Logout</button>
  </div>
</div>

그다음 logout() 함수를 제공하여 자바스크립트를 참조한다:

index.html
angular
  .module("app", [])
  .controller("home", function($http, $location) {
    var self = this;
    self.logout = function() {
      $http.post('/logout', {}).success(function() {
        self.authenticated = false;
        $location.path("/");
      }).error(function(data) {
        console.log("Logout failed")
        self.authenticated = false;
      });
    };
  });

logout() 함수는 /logout 로  POST 호출을 한뒤 authenticated 플래그를 초기화한다. 이제 우리는 종단의 구현을 위해 서버측으로 옮겨가보자.

로그아웃 종단 추가하기 Adding a Logout Endpoint

스프링 시큐리티는 우리가 하려고 하는 일(세션을 정리하고 쿠키를 무효화invalidate함)을 위한 /logout 종단 지원을 내장하고 있다. 이 종단을 설정하려면, 간단히 우리의 WebSecurityConfigurer에 있는 configure method 메소드를 다음과 같이 확장하면 된다:

SocialApplication.java
@Override
protected void configure(HttpSecurity http) throws Exception {
  http.antMatcher("/**")
    ... // existing code here
    .and().logout().logoutSuccessUrl("/").permitAll();
}

/logout종단은 우리에게 이 요청을 POST로 보낼것을 요구한다. 그리고 Cross Site Request Forgery (씨서프sea surf라 발음하는 CSRF)로 부터 사용자를 보호하기 위해 이 요청안에 하나의 토큰을 넣으라고 요구한다. 이 토큰의 값은 현재 세션으로 링크되어있는데 이는 무엇을 보호하는지 제공해주는 일을 한다. 이제 우리의 자바스크립트 앱에 이 데이터를 넣는 방법을 알아야한다.

앵귤러JS 또한 (그들이 XSRF라 부르는) CSRF 지원을 내장하고 있다. 하지만 스프링 시큐리티의 통상적인 방법과 약간 다르게 구현되어있다. 앵귤러가 서버에게 원하는 것은 "XSRF-TOKEN"이라 부르는 쿠키를 보내주는 것이다. 이것이 들어있으면 앵귤러는 "X-XSRF-TOKEN"이라는 이름의 헤더를 되돌려준다. 스프링 시큐리티에서 이것을 알려주기 위해, 우리는 쿠키를 만드는 필터를 하나 추가해주어야한다, 또한 현재의 CSRF 필터에 이 헤더이름을 알려주어야한다. WebSecurityConfigurer에서:

SocialApplication.java
@Override
protected void configure(HttpSecurity http) throws Exception {
  http.antMatcher("/**")
    ... // existing code here
    .and().csrf().csrfTokenRepository(csrfTokenRepository())
    .and().addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}

csrfHeaderFilter()는 다음의 커스텀 필터이다:

SocialApplication.java
private Filter csrfHeaderFilter() {
  return new 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);
    }
  };
}

그리고 csrfTokenRepository()는 여기 정의되어있다.:

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

출동준비완료! Ready To Roll!

여기까지의 수정으로, 우리는 서버를 동작하고 새로운 로그아웃 버튼을 시도할 준비를 마쳤다. 앱을 시작하고 새 브라우저 창에 홈페이지를 열어서 "login"링크를 클릭하면 당신을 페이스북으로 데려다 줄 것이다. (이미 로그인 되어있다면 아마 리다이렉트를 알아차리지 못할 것이다.). 현재 세션을 종료하기위해 "Logout"버튼을 클릭하면 앱은 비인증 상태로 되돌아 온다. 궁금하면 브라우저가 로컬서버에서 받아온 요청안에 새 쿠키와 헤더를 확인할 수 있을 것이다.

이제 브라우저 클라이언트에서 로그아웃 종단이 동작하는 것을 기억하자, 이제 모든 HTTP요청들((POST,PUT,DELETE,등등)에서 또한 잘 동작할 것이다. 이 앱은 이제 약간은 더 실전적 기능을 가진 좋은 플랫폼이 되었다.


반응형

반응형

스프링 부트와 OAuth2

Spring Boot And OAuth2

(원문소스: https://spring.io/guides/tutorials/spring-boot-oauth2/)


이 가이드에서 OAuth2와 Spring Boot를 사용해 어떻게 하면 "소셜 로그인"의 다양한 일을 하는 예제를 만들 수 있는 지 보여줄 것이다. 간단한 하나의 싱글사인온 제공자에 연결해보는 걸로 시작하여, Facebook 이나 Github과 같은 authentication 제공자와 함께 OAuth2 Authorization Server를 스스로 돌리는 일을 해볼것이다. 예제들은 백엔드로 모두 스프링 부트와 스프링 OAuth를 사용한 단일 페이지 앱이 될 것이며 프론트엔드로 모두 AngularJS 를 사용하였다. 하지만 자바스크립트단의 코드나 서버의 랜더링사용은 최소화 하였다.

각각 새로운 기능을 추가해놓은 몇가지 예제들이다:

  • simple: 매우 기본적인 정적 앱으로 그냥 하나의 홈페이지와 스프링부트의 @EnableOAuth2Sso를 통해 무작위 로그인을 한다 (홈페이지를 방문하면 자동으로 페이스북으로 리다이렉트 될 것이다)

  • click: 사용자가 로그인을 클릭할 수 있는 명시적 링크를 추가하였다.

  • logout: 인증받은 사용자를 위한 로그아웃 링크를 추가하였다.

  • manual @EnableOAuth2Sso 어노테이션을 사용하지않고 수동으로 설정하여 똑같이 동작시켜본다.

  • gitub: 두번째 로그인 제공자로서 Github을 추가하여 사용자는 홈페이지에서 어느것을 사용할 지 고를 수 있다.

  • auth-server스스로 토큰을 발행할 수 있지만 여전히 인증을 위해 외부 OAuth2 제공자를 사용할 수 있는 완전무결한fully-fledged OAuth2  Authorization 서버를 돌린다. 

  • custom-error Github API에 기반한 커스텀 인증과, 비인증 사용자를 위한 에러 메세지를 추가하였다.

이들은 각각 IDE에서 불러올 수 있으며, 앱을 시작할 수 있는 SocialApplication 메인 클래스가 있다. 이들 모두 http://localhost:8080 로 홈페이지에 접근할 수 있다.( 로그인을 하고 그 내용을 보려면 최소한 페이스북 계정이 필요하다). 또한, gmvn spring-boot:run를 사용하여 커맨드라인상에서 앱을 돌리거나, mvn package 를 통해 jar파일을 빌드하고 java -jar target/*.jar 로도 돌릴 수 있다. 프로젝트 상위레벨에 있는 wrapper를 사용하면 메이븐을 설치하지않다도 된다. 예를 들면:

$ cd simple
$ ../mvnw package
$ java -jar target/*.jar
 모든 앱은 localhost:8080

에서 동작한다. 페이스북과 Github에 이 주소로 OAuth2 클라이언트를 등록했기 때문이다. 다른 호스트나 포트로 돌리려면 스스로 각각 앱을 등록하고 config 파일에 해당 인증정보들을 넣어줘야한다. 기본설정값을 사용하면 localhost로 돌린다고 당신의 페이스북이나 Github 인증정보가 누출되거나 위험해지지않는다. 하지만 당신이 인터넷에 무엇을 노출할지 조심해야한다. 공개 소스 컨트롤에 당신의 앱을 등록하지 않아야한다.

페이스북으로 싱글 사인온 하기 Single Sign On With Facebook

이 섹션에서, 우리는 인증을 위해 페이스북을 사용하는 최소한의 어플리케이션을 만들것이다. 이는 스프링 부트의 자동설정을 사용하면 매우 쉽다. 

새 프로젝트 만들기

먼저 스프링 부트 어플리케이션을 만들어야 한다. 이를 위한 수많은 방법이 있지만, 가장 쉬운 것은 http://start.spring.io 를 방문하여 빈 프로젝트를 만드는 것이다. (시작점으로서 "Web" 의존성을 골라주자). 커맨드라인으로 아래와 같이하면 똑같이 만들 수 있다:

$ mkdir ui && cd ui
$ curl https://start.spring.io/starter.tgz -d style=web -d name=simple | tar -xzvf -

이제 당신이 선호하는 IDE에서 이 프로젝트를 불러올 수 있다. (기본적으로 보통의 메이븐 자바프로젝트다) 또는 파일 작업을 하고 그냥 커맨드라인에서 "mvn"을 사용해도 된다.

홈페이지 추가하기

새 프로젝트의 "src/main/resources/static" 폴더에 index.html파일을 만들자. 몇몇의 스타일시트와 자바스크립트를 링크해줘야하며 그 결과는 다음과 같다:

index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>Demo</title>
    <meta name="description" content=""/>
    <meta name="viewport" content="width=device-width"/>
    <base href="/"/>
    <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css"/>
    <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
    <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
	<h1>Demo</h1>
	<div class="container"></div>
</body>
</html>

이 소스의 무엇도 OAuth2 로그인 기능과 무관하지만 우리는 홈페이지에 약간의 기본적인 기능을 시작할 수 있을 뿐아니라 멋지게 보이는 UI 를 원한다.

앱을 시작하고 홈페이지를 로드하면, 스타일 시트가 로드되지 않았다는 걸 알 수 있을 것이다. 이제 그들을 아래와 같이 의존성을 추가해줌으로서 해결할 수 있다:

pom.xml
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>angularjs</artifactId>
	<version>1.4.3</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery</artifactId>
	<version>2.1.1</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>3.2.0</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator</artifactId>
</dependency>

우리는 트위터 부트스트랩과 (우리가 지금 당장 써야하는) jQuery, 거기에 나중에 쓸 AngularJS를 추가했다. 다른 의존성은 webjars "locator"로 webjars 사이트에 의해 라이브러리로서 제공받는 것이고 이것은 스프링에 의해 정확한 버전을 알 필요없이(그렇기 때문에 index.html에서 버전없는 /webjar/all}링크를 사용했다webjars의 정적인 어셋을 위치locate하는데 사용된다. 이 webjar locator를 활성화하려면 어플리케이션의 application.properties에서 Spring MVC 리소스 체인을 활성화시켜줘야한다:

application.properties
spring.resources.chain.enabled: true

이 수정을 통해 이제 우리는 이뻐보이는 홈페이지를 가지게 되었다.

어플리케이션 보호하기 Securing the Application

어플리케이션을 안전하게 만드려면, 스프링 시큐리티 의존성을 추가해주면 된다. 그러면 기본적으로 HTTP Basic으로 보호될 것이다. 따라서 우리가 (페이스북으로 위임하는) "소셜" 로그인을 만드려고 하므로, Spring Security OAuth2 의존성 또한 추가해줘야한다:

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

페이스북으로의 링크를 만드면, 메인클래스에서 @EnableOAuth2Sso 어노테이션을 해줘야한다:

SocialApplication.java
@SpringBootApplication
@EnableOAuth2Sso
public class SocialApplication {

  ...

}

그리고 약간의 설정이 필요하다 (더 나은 식별성을 위해 application.properties을 YAML 으로 변환하였다):

application.yml
security:
  oauth2:
    client:
      clientId: 233668646673605
      clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
      accessTokenUri: https://graph.facebook.com/oauth/access_token
      userAuthorizationUri: https://www.facebook.com/dialog/oauth
      tokenName: oauth_token
      authenticationScheme: query
      clientAuthenticationScheme: form
    resource:
      userInfoUri: https://graph.facebook.com/me
...

이 설정은 페이스북의 개발자사이트에 등록된 클라이언트 앱을 참조한다. 당신은 당신의 앱에서 리다이렉트할 곳을 등록해줘야한다. (우리의 경우 Home page). 이것은 "localhost:8080"으로 등록되었는데, 이 주소로 돌아가는 앱에서만 작동한다.

이 수정으로 앱을 다시 돌리고 http://localhost:8080의 홈페이지를 방문해보자. 홈페이지 대신, 당신은 페이스북 로그인으로 리다이렉트될 것이다. 로그인하려고 인가authorization를 수락하면, 당신의 로컬 앱으로 다시 리다이렉트되어 돌아와지고 홈페이지가 보여질 것이다. 만일 페이스북 로그인이 되어있는 상태면, 브라우저를 완전히 새롭게 열어 쿠키도 없고 캐시된 데이터가 없더라고 이 로컬앱에서 재인증할 필요없다. (이것이 싱글사이온의 의미이다)

당신이 이 섹션의 예제 어플리케이션을 돌리는 중이라면, 브라우저의 쿠키와 HTTP Basic credentials의 캐시를 지웠는지 먼저 확인하자. 크롬의 incognito window를 사용하는 것이 가장 좋은 방법이다.

이 예제의 접근을 승인하는 것grant access은 안전하다. 로컬에서만 동작하고 있는 이 앱에서만 토큰을 사용할 수 있고 범위scope도 제약적이기 때문이다. 당신이 이와 같은 절차를 거치는 앱을 로그인할 때, 무엇을 승인해야하는지 반드시 살펴봐야한다: 당신이 불편해할 만큼의 권한을 요구할 수도 있기때문이다. (예를 들면, 당신의 개인정보를 수정할 수 있는 권한을 요구할 수 있다.)

방금 뭔일 있었나? What Just Happened?

당신이 방금 만든 이 앱은, OAuth2 용어로, 클라이언트 어플리케이션이다. 그리고 이 앱은 페이스북(인가서버Authorization Server)로부터 억세스토큰access token을 받기위해 authorization code grant를 사용했다. 그 후 당신의 로그인ID와 이름을 포함한 (당신이 허용해준) 몇가지 개인 정보를 페이스북에 요청하기 위해 억세스토큰을 사용했다. 이 단계까지 페이스북은 당신이 보낸 토큰을 디코딩하고 사용자의 정보에 접근하는 앱의 권한을 체크하는 등등의 리소스 서버로서 역할을 하고 있다. 이 단계가 성공적으로 완료되면, 당신의 앱은 사용자 정보를 스프링 시큐리티 컨텍스트Spring security context안에 넣음으로서 당신을 인증할 것이다.

(크롬의 F12누르면 보이는) 브라우저 도구를 열고 네트워크 트래픽을 따라가보면, 페이스북으로 리다이렉트되어 가고오고 하는것을 확인할 수 있으며 최종적으로 새 Set-Cookie 헤더를 가지고 홈페이지로 되돌아와진다. 이 쿠키(기본값으로JSESSIONID)는 스프링(또는 어느 서블릿 기반의) 어플리케이션에서 당신의 인증 상세정보를 위한 하나의 토큰이다.

이제 우리는, 어떠한 정보를 보기위해 사용자가 페이스북과 같은 외부 제공자로 인증해야하는 측면에서, 안전한 어플리케이션을 가지게 되었다. 우리는 웹사이트에 매번 이를 사용하려는게 아니라 기본 식별, 그리고 당신의 사이트에 서로 다른 사용자간에 컨텐트를 구별하기 위한 목적으로 이는 오늘날 이러한 종류의 인증이 왜 인기가 많은가를 설명하는 훌륭한 출발점이 될 것이다. 다음 섹션에서 우리는 어플리케이션에 약간의 기본 기능들을 추가하고, 사용자가 처음 페이스북으로 리다이렉트 할 때 언제 그리고 무엇을 하는지 조금더 구체적으로 알아볼 것이다.


반응형

반응형

스프링 세션으로 스케일 아웃하기 Scaling out with Spring Session

원문: http://www.jayway.com/2015/05/31/scaling-out-with-spring-session/

Stateless 아키텍쳐는 근 몇년간 타당한 이유로 꾸준히 인기가 올라왔다. 하지만 stateful 세션기반의 어플리케이션도 꾸준히 중요한 역할을 이어왔다. 예를 들어 더 나은 보안을 위해 CSRF 토큰이 이슈될때처럼. 낮은 부하의 단일 서버를 배포할 때, 세션관리는 당신이 합리적인 타임아웃 유효기간을 사용하고 세션에 많은 양의 데이터를 저장하지 않는 것처럼 매우 직관적이다. 문제는 스케일 아웃하기 더 힘들다는 것이다 각각의 요청은 아마 다른 서버에 있을 지도 모르는 그에 상응하는 세션과 함께 묶여져야하기 때문이다. 이를 극복하기 위해, 서버 공급자들은 서버간 다양한 종류의 세션 복제를 구현해왔다. 또다른 대안으로 로드밸런서를 세션과 붙여 같이 설정할 수 있다. 이 두가지 솔류션 다 잘 동작한다. 하지만 스프링 세션으로 스프링은 또 다른 옵션을 만들어냈다. 이 블로그는 세션을 스케일 아웃하기 위해 어떻게 스프링 세션을 레디스와 함께 설정하는 지 보여줄 거이다. 이 제안된 솔루션은 (스프링 기반이 아닌) 어떠한 서블릿에서도 작동할 것이다 - 이는 당신의 레가시 웹앱을 스케일 아웃해야할때 매우 적합하다.


예제 어플리케이션 Example Application

먼저 우리는 간단한 세션 기반의 HelloServlet 예제를 살펴보자:

HTTP GET이 호출될 때, 서블릿은 세션에 Mattias의 값을 가진 name 속성이 있으면  Hello Mattias! 를 응답하거나, 없다면 기본값인 Hello World!를 응답할 것이다.

HTTP POST이 호출될 때, 서블릿은 요청으로 부터 name 매개변수를 읽고, 새로운 세션을 만들고 (또는, 이미 세셨이 있다면 재사용하고), name의 값을 이와 상응하는 세션 속성에 저장할 것이다.

첫번째 요청은 세션이 없으므로 다음과 같이 다음과 같은 기본 응답을 받게 된다:

두번째 요청은 name 속성을 가진 POST 요청으로 세션이 생성된다:

덧붙이자면,  -i (또는 —-include) 옵션은 응답의 HTTP 헤더에 포함되는 cURL 플래그이며 -d (또는—data) 플래그는 요청의 매개변수로서 데이터를 제출하는데 사용된다. 응답에서 우리는 Set-Cookie 헤더의 값에 주목했다.

세번째 요청으로, GET 요청과 세션 상태의 유효화 검증이다:

어플리케이션의 서로 다른 인스턴스에서 같은 요청을 수행할 때 우리는 문제에 직면한다. 세션이 이미 존재함에도 불구하고 기본 응답이 되돌아왔다:

이제 스프링 세션을 써야할 때다!


스프링 세션 아키텍쳐 Spring Session architecture

스프링 세션의 컨셉은 매우 직관적이다:

  • 새 서블릿 필터를 만든다
  • 그 필터를 당신의 서블릿 필터체인filter chain에 추가한다
  • 그 필터를 레디스와 연결한다 (또는, Hazelcast, GemFire, Coherence가 지원하는 다른 MapSessionRepository 또는 당신에게  Map 레퍼런스를 제공해주는 다른 종류의 데이터 그리드data grid, 그러나 이들은 이 글의 범주에 벗어나므로 더이상 언급하지않는다)

의존성 추가하기 Adding dependencies

먼저, 우리는 몇가지 의존성을 추가해줘야한다. 메이븐에선 다음과 같이 POM에 추가해주면된다:

첫번째 의존성은 Redis 연결을 위해 필요하며 두번째는 스프링이 서블릿 필터를 만들기 위해 필요하다.


스프링 세션 설정 Spring Session Config

스프링 세션은 (내부적으로 Jedis client를 기반으로) 기본적으로 레디스 연결을 지원해준다. 아래의 XML 기반 설정의 예를 참고하거나  자바 기반 설정으로 대체할 수 있다.

마지막 두줄은 스프링에게  spring.redis.port 값을  application.properties 파일에서 찾으라고 알려주는것이다.  다음의 한줄과 같이: 


스프링 세션 설정 등록하기 Registering the Spring Session Config

(만인 당신의 어플리케이션이 이미 스프링을 사용하고 있지 않다면) 스프링 세션 설정을 불러오기위해  web.xml에 다음의 몇 줄을 추가해주어야 한다:


스프링 세션 서블릿 필터 추가하기 Adding the Spring Session servlet filter

어플리케이션에서 필요한 마지막 수정은 새로운 서블릿 필터를 추가하는 것이다. web.xml를 다시 열고 다음을 추가해준다:

single commit에서 바뀐 사항들을 모두 확인할 수 있다:


입 증 Verification

이제 어플리케이션을 모두 업데이트하였다. 원하는대로 동작하는지 입증해보자:

  • 어플리케이션을 다시 빌드한다
  • 레디스가 설치되었고 기본 포트인 6379에서 작동중인지 확인하라. 다른 포트에서 동작하고 있다면 spring.redis.port 설정을 업데이트 해주어야한다
  • 어플리케이션을 이를테면 8080 과 8081과 같이 두개의 인스턴스로 시작하라. 둘다 같은 레디스서버에 연결되어야한다.
  • 위에 언급된 이슈를 시도해보자

먼저 서버 인스턴스 한군데서 POST로 세션 상태를 만든다:

안은 인스턴스에서 GET 요청으로 세션 상태 검증하자:

다른 서버에서 같은 요청을 반복해보면 이제 같은 세션 데이터를 가지고 있는 것을 볼 수 있다:


고려 사항들 Considerations

  • 당신은 세션을 잃을지도 모른다는 걱정없이 원하는대로 서버를 시작하거나 멈출 수 있다. 이를테면 장애극복failover이나 자동 스케일링autoscaling은 세션의 관점에선 자동으로 처리된다.
  • 당신은 이제 더이상 sticky sessions 이나 복잡한 로드밸런서 설정이 필요없다. 부하를 나누려면 간단한 라운드-로빈Round-robin 전략이면 충분하다.
  • 운영의 관점에선 여전히 해줘야 할 것들이 있다. 누군가는 레디스 클러스터를 설정하고 관리해주어야 한다. (만일 AWS에서 동작한다몀 Amazon ElasticCache이 잘 맞을 것이다)
  • 스프링 부트를 사용중이라면 아마도 spring-session.xml설정이나  web.xml.을 추가하고 싶지않을 것이다.  자바 기반 설정을 위해  Spring Boot Guide를 확인하자.
  • 만일 당신이 RESTful API 를 개발중이다 쿠키를 싫어한다며? Spring Session Rest를 확인해보자.

주, 스프링 부트에서는 @EnableRedisHttpSession 어노테이션 한줄로 다 해결됨


반응형

반응형
스프링 부트 레퍼런스 11장 
첫번째 스프링 부트 어플리케이션 개발하기
Developing your first Spring Boot application

11. 첫번째 스프링 부트 어플리케이션 개발하기

스프링 부트의 핵심기능 일부를 사용해서 자바로 간단한 "Hello World!” 웹 어플리케이션을 개발해보자. 여기선 대부분의 IDE가 지원하는 메이븐을 사용할 것이다.

시작에 앞서 터미널을 열고 설치된 자바와 메이븐 버전을 확인해보자 

$ java -version
java version "1.7.0_51"
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.51-b03, mixed mode)
$ mvn -v
Apache Maven 3.2.3 (33f8c3e1027c3ddde99d3cdebad2656a31e8fdf4; 2014-08-11T13:58:10-07:00)
Maven home: /Users/user/tools/apache-maven-3.1.1
Java version: 1.7.0_51, vendor: Oracle Corporation
[Note]

이 예제는 자신의 폴더를 만들어서 진행해야하며 부가적 구성은 해당 폴더 안에서 진행한다고 가정함.

11.1 POM 생성하기

메이븐 pom.xml 파일을 먼저 만들어야한다. 이 pom.xml 파일은 당신의 프로젝트안에서 사용될 레시피다. 선호하는 에디터를 열고 다음을 추가하라:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>myproject</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.5.RELEASE</version>
    </parent>

    <!-- Additional lines to be added here... -->

</project>

이것으로 빌드는 정상 동작한다. mvn package 를 치고 확인할 수 있다. ( “jar will be empty - no content was marked for inclusion!'” 라는 경고메시지는 일단 무시).

[Note]

이 단계에서 IDE로 import할 수 있다. (대부분의 자바 IDE는 메이븐을 내장하고 있다) 그냥 간단히 아무 에디터를 써서 이 예제를 계속 해도 상관없음

11.2 classpath 의존성(dependencies) 추가

스프링 부트는 당신의 classpath에 jar파일들을 쉽게 추가하게 해서는 몇몇의 “Starter POM”들을 제공해주고 있다. 현 예제 어플리케이션은 POM의 parent 센션에서 이미 spring-boot-starter-parent 를 사용하고 있다.

이 spring-boot-starter-parent 는 유용한 메이븐의 기본사항들을 제공해주는 특별한 시작키트이며 또한 dependency-management 섹션을 제공하여 선별된 dependency들을 version태그없이 쓸 수 있다.

다른 “Starter POM”들은 특별한 타입의 어플리케이션을 개발할때 필요로 하는 dependency들을 맞춤제공해준다. 현재 우리는 웹 어플리케이션을 개발중이므로  spring-boot-starter-web dependency를 추가할 것이지만, 그전에 우리가 현재 가지고 있는 dependency들을 살펴보도록 하자

$ mvn dependency:tree

[INFO] com.example:myproject:jar:0.0.1-SNAPSHOT

이 mvn dependency:tree 명령어는 당신의 프로젝 dependency를 트리구조로 보여준다. 보면 알수있듯이 spring-boot-starter-parent 는 그 자체에 의존성을 가지고 있지않다. pom.xml 를 편집해서 spring-boot-starter-web 의존성을 parent 센션 밑에 아래와 같이 추가 해보자:

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

mvn dependency:tree 를 다시 실행해보면 톰캣웹서버나 스프링 부트 자체를 포함하여 부수적인 dependency들이 추가된것을 볼 수 있다. 

11.3 코드작성하기

예제 어플리케이션을 완성하려면, 자바파일을 하나 만들어야 한다. 메이븐은 기본적으로  src/main/java 위치에 있는 소스를 컴파일하므로 해당 경로의 폴더를 만들고 다음의 파일을 추가 한다. src/main/java/Example.java:

import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;

@RestController
@EnableAutoConfiguration
public class Example {

    @RequestMapping("/")
    String home() {
        return "Hello World!";
    }

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

}

코드의 양은 보잘것없지만 많은 일이 진행되었다. 이 중요한 파트를 살펴보자.

11.3.1 @RestController 그리고 @RequestMapping annotations

Example 클래스의 첫번째 어노테이션은 @RestControllerstereotype annotation으로 잘알려진 이것은 코드를 읽을 사람들 또는 특별한 역할을 실행하는 클래스를 가진 스프링에 힌트를 제공한다. 우리의 경우 웹 @Controller 이므로, 스프링은 들어오는 Web request들을 처리할거라고 이해한다.

@RequestMapping 어노테이션은 "라우팅"정보를 제공하며 스프링에게 경로 "/"를 통해 들어오는 HTTP 요청이 home 메소드와 매칭되야한다고 알려준다.

@RestController 어노테이션은 스프링에게 호출자에게 되돌아가는 문자열을 랜더하라고 알려준다.

[Tip]

@RestController 와 @RequestMapping 어노테이션은 Spring MVC 어노테이션이다. (Spring Boot 특화된게 아님). 자세한 정보는 MVC section 참고

11.3.2 The @EnableAutoConfiguration annotation

두번째 클래스 레벨 어노테이션은 @EnableAutoConfiguration. 이 어노테이션은 스프링 부트에게 당신이 추가한 jar dependency들을 기준으로 어떻게 스프링을 설정하길 원하는지 "추측"하라고 알려준다.

spring-boot-starter-web 를 통해 추가된 톰캣과 스프링 MVC가 추가되었으므로,  자동설정 기능은 당신이 웹어플리케이션을 개발하고 있다고 가정하고 그에 따라 스프링을 설정한다.

11.3.3 The “main” method

우리의 어플리케이션의 마지막 파트는 main 메소드이다. 이것은 어플리케이션의 시작점을 위한 자바 명명법을 따르는 표준 메소드이다. 우리의 main 메소드는 run 에 의해 호출되는 스프링 부트의   SpringApplication 클래스를 실행한다.  SpringApplication 는 당신의 어플리케이션을 시동걸어, 스프링을 시작하고 이어 자동설정된 톰캣 웹서버를 동작시킨다. 우리는 기본 스프링 컴포넌트인 SpringApplication에 알리기 위해 Example.class 를 하나의  인자값으로  run  메소드에 넘겨야한다.  args 배열값 또한 command-line 인자값으로 외부적으로 넘길 수 있다.

11.4 예제 실행하기 Running the example

현재 단계에서  spring-boot-starter-parent POM을 사용한 이상 어플리케이션은 실행이 잘된다. 프로젝트의 메인 디렉토리에서 mvn spring-boot:run 를 치면 어플리케이션이 시작한다.

$ mvn spring-boot:run

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::  (v1.2.5.RELEASE)
....... . . .
....... . . . (log output here)
....... . . .
........ Started Example in 2.222 seconds (JVM running for 6.514)

브라우저를 열고 localhost:8080 를 치면 다음의 결과가 보인다:

Hello World!

 ctrl-c를 쳐서 프로그램을 우아하게 종료시키자

11.5 실행가능한 jar 만들기 Creating an executable jar

독립실행 가능한 jar파일을 만듦으로서 예제를 완성시켜보자. 실행가능한 jar (때로는 “fat jars”로 부르기도 함)는 당신의 컴파일된 클래스 뿐만 아니라 코드 실행에 관련된 모든 jar dependency들을 포함하여 압축한다.

실행가능한 jar를 만드려면 spring-boot-maven-plugin 를  pom.xml에 추가해야 한다. 다음의 라인을 dependencies 섹션 다음에 추가하자

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
[Note]

spring-boot-starter-parent POM 은 repackage goal 을 설정하기 위해 <executions> 를 포함하고 있다. 부모 POM이 없다면 스스로 설정을 해야한다. 자세한 문서는 plugin documentation 참고.

 pom.xml 를 저장하고 콘솔에서 mvn package 를 친다.

$ mvn package

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building myproject 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] .... ..
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myproject ---
[INFO] Building jar: /Users/developer/example/spring-boot-example/target/myproject-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:1.2.5.RELEASE:repackage (default) @ myproject ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

target 디렉토리에 myproject-0.0.1-SNAPSHOT.jar파일이 만들어졌다. 대략 10 Mb 정도의 사이즈이며 jar tvf명령어로 내부를 둘려볼 수 있다.

$ jar tvf target/myproject-0.0.1-SNAPSHOT.jar

target 디렉토리에는 myproject-0.0.1-SNAPSHOT.jar.original 라는 이름의 작은 파일도 있는데 이것은 스프링 부트에 의해 재패키지되기전의 메이븐 원본 jar파일이다.

이 어플리케이션을 실행하려면  java -jar 명령어를 쓰자:

$ java -jar target/myproject-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::  (v1.2.5.RELEASE)
....... . . .
....... . . . (log output here)
....... . . .
........ Started Example in 2.536 seconds (JVM running for 2.864)

 ctrl-c로 우아하게 종료할 수 있다.

반응형

'Tutorials' 카테고리의 다른 글

스프링부트와 OAuth2 - (1/4)  (0) 2016.03.19
Scaling out with Spring Session  (0) 2015.12.15
Spring Security and AngularJS Part VII  (1) 2015.12.03
Spring Security and AngularJS Part VI  (0) 2015.12.03
Spring Security and AngularJS Part V  (0) 2015.12.03

반응형

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


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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

hello.js

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

  function($routeProvider, $httpProvider) {

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

    ...

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

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

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

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

same with those.

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

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

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

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

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

        function($routeProvider, $httpProvider) {

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

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

        });

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        function($scope, auth) {

            $scope.credentials = {};

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

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

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

        });

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

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

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

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

});

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

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

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

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

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

    function($http, $location) {

      var auth = {

        authenticated : false,

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

        authenticate : function(credentials, callback) {

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

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

        },

        clear : function() { ... },

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

      };

      return auth;

    });

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      var auth = {

        ...

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

      };

      return auth;

    });

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

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

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

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

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

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

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

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

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

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

결 론 Conclusion

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


반응형

반응형

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


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

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

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

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

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

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

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

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


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


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

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

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

타겟 아키텍쳐 Target Architecture

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

Components of the System

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

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

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

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

백엔드 만들기 Building the Backend

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

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

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

UiApplication.java
@SpringBootApplication
@EnableRedisHttpSession
public class UiApplication {

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

}

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

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

application.properties

server.port: 8081
security.sessions: NEVER

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

리소스 서버 The Resource Server

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

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

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

게이트웨이 The Gateway

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

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

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

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

GatewayApplication.java

@SpringBootApplication
@EnableRedisHttpSession
@EnableZuulProxy
public class GatewayApplication {

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

}

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

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

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

서버 동작시키기 Up and Running

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

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

VerbPathStatusResponse

GET

/ui/

401

Browser prompts for authentication

GET

/ui/

200

index.html

GET

/ui/css/angular-bootstrap.css

200

Twitter bootstrap CSS

GET

/ui/js/angular-bootstrap.js

200

Bootstrap and Angular JS

GET

/ui/js/hello.js

200

Application logic

GET

/ui/user

200

authentication

GET

/resource/

200

JSON greeting

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

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

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

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

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

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

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

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

Login Page

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

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

  ...

  authenticate();

  $scope.credentials = {};

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

}

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

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

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

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

}

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

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

Home Page

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

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

Components of the System

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

application.yml

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

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

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

SecurityConfiguration.class

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

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

}

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

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


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

SecurityConfiguration.java
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

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

}

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

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

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

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

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

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

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

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

function($scope, $http) {

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

})

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

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

AdminApplication.java

@SpringBootApplication
@RestController
@EnableRedisHttpSession
public class AdminApplication {

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

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

}

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

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

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

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

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

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


반응형

반응형

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


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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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

AuthserverApplication.java

@SpringBootApplication
public class AuthserverApplication extends WebMvcConfigurerAdapter {

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

  @Configuration
  @EnableAuthorizationServer
  protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

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

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

}

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

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

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

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

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

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

$ mvn spring-boot:run

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

application.properties

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

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

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

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

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

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

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

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

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

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

  ...

}

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

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

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

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

UI 서버 The UI Server

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

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

UiApplication.java

@SpringBootApplication
@EnableZuulProxy
@EnableOAuth2Sso
public class UiApplication {

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

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

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

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

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

SecurityConfiguration.java

@Configuration
protected static class SecurityConfiguration extends OAuth2SsoConfigurerAdapter {

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

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

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

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

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

application.yml

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

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

클라이언트에선 In the Client

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

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

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

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

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

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

hello.js

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

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

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

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

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

  $scope.credentials = {};

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

});

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

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

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

VerbPathStatusResponse

GET

/

200

index.html

GET

/css/angular-bootstrap.css

200

Twitter bootstrap CSS

GET

/js/angular-bootstrap.js

200

Bootstrap and Angular JS

GET

/js/hello.js

200

Application logic

GET

/home.html

200

HTML partial for home page

GET

/user

302

Redirect to login page

GET

/login

302

Redirect to auth server

GET

(uaa)/oauth/authorize

401

(ignored)

GET

/resource

302

Redirect to login page

GET

/login

302

Redirect to auth server

GET

(uaa)/oauth/authorize

401

(ignored)

GET

/login

302

Redirect to auth server

GET

(uaa)/oauth/authorize

200

HTTP Basic auth happens here

POST

(uaa)/oauth/authorize

302

User approves grant, redirect to /login

GET

/login

302

Redirect to home page

GET

/user

200

(Proxied) JSON authenticated user

GET

/home.html

200

HTML partial for home page

GET

/resource

200

(Proxied) JSON greeting

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

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

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

로그아웃 경험 The Logout Experience

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

결 론 Conclusion

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

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

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

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


반응형

반응형

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


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

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

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

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

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

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

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

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


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


API 게이트웨이 The API Gateway

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

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

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

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

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

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

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

UiApplication.java

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

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

application.yml

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

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

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

pom.xml

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

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

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

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

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

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

로컬 리소스로는:

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

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

더욱 간소화 Further Simplifications

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

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

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

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

application.properties
server.address: 127.0.0.1

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

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

pom.xml

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

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

인증 상태 공유하기 Sharing Authentication State

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

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

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

UiApplication.java

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

  ...

}

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

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

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

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

  ...

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

}

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

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

application.properties

security.sessions: NEVER

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

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

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

VerbPathStatusResponse

GET

/

200

index.html

GET

/css/angular-bootstrap.css

200

Twitter bootstrap CSS

GET

/js/angular-bootstrap.js

200

Bootstrap and Angular JS

GET

/js/hello.js

200

Application logic

GET

/user

302

Redirect to login page

GET

/login

200

Whitelabel login page (ignored)

GET

/resource

302

Redirect to login page

GET

/login

200

Whitelabel login page (ignored)

GET

/login.html

200

Angular login form partial

POST

/login

302

Redirect to home page (ignored)

GET

/user

200

JSON authenticated user

GET

/resource

200

(Proxied) JSON greeting

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

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

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

/trace

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

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

결 론 Conclusion

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



반응형

+ Recent posts