반응형

스프링 비동기와 자바8의 CompletableFuture Spring Async and Java’s 8 CompletableFuture


아래는 '비동기' 유저찾기 메소드의 예제의 구현이다. (전체 소스는 여기)

1@Async
2public Future<User> findUser(String user) throws InterruptedException {
3  System.out.println("Looking up " + user);
4  User results = restTemplate.getForObject("https://api.github.com/users/" + user, User.class);
5  // Artificial delay of 1s for demonstration purposes
6  Thread.sleep(1000L);
7  return new AsyncResult<User>(results);
8}

나는 왜 여전히 'Future'가 이 예제에 있는지 궁금했다. 나는 원작자가 이미 자바8의 새로운 CompletableFuture가 있는 걸 알고 있지만, 아마 자바 6나 7과의 이전 버전 호환성을 고려해서 남겨두었다고 생각했다.

여기있는 매우 정갈한 예제를 보면 이러한 의문을 가진건 나 혼자만은 아닌것 같다. 이 글의 답글 중 하나에서, 당신은 스프링 API 버전 4.2 에 포함되어 있는 힌트 - 이미 지원하고 있는 Future와 AsyncResult에서 CompletableFuture를 사용이 호환가능하다는 를 볼 수 있을 것이다. 나는 '누군가가 이 예제를 돌리면 그는 아마 현재의 구현에 머무를 것이므로 직접 적용해보거나 문서화 하지않는 것은 부끄러운 일이다'라는 생각이 들었다. 표준화된 방법을 사용하지않을 이유가 없다.

그래서 나는 Future를 지우고 CompletableFuture로 바꾸는 작은 수정을 하기로 했다. 또한 Future.isDone() 구문을 주석처리하고  CompletableFuture.allof() 메소드로 대체할 것이다.

또한 호출자 코드를 업데이트하면서 3개의 future 들을 동기화하도록 'service' 메소드의 리턴타입을 바꿨다. 일단 allof() 모두 완료되면 우리는 결과를 출력할 수 있다.

01package hello;
02 
03import java.util.concurrent.CompletableFuture;
04import java.util.concurrent.Future;
05 
06import org.springframework.scheduling.annotation.Async;
07import org.springframework.scheduling.annotation.AsyncResult;
08import org.springframework.stereotype.Service;
09import org.springframework.web.client.RestTemplate;
10 
11@Service
12public class GitHubLookupService {
13 
14    RestTemplate restTemplate = new RestTemplate();
15 
16    @Async
17    public CompletableFuture findUser(String user) throws InterruptedException {
18        System.out.println("Looking up " + user);
19        User results = restTemplate.getForObject("https://api.github.com/users/" + user, User.class);
20        // Artificial delay of 1s for demonstration purposes
21        Thread.sleep(1000L);
22        return CompletableFuture.completedFuture(results);
23    }
24 
25}

이 수정된 예제는 여기서 받을 수 있다. 나는 Tomasz Nirkewicz의 블로그에서 CompletableFuture의 풍부한 메소드 리스트를 매우 실용적이고 멋있게 설명한 이 글이 글을 찾았다. 또한 매우 완벽한 프리젠테이션도 여기서 받을 수 있다.


01@Override
02    public void run(String... args) throws Exception {
03        // Start the clock
04        long start = System.currentTimeMillis();
05 
06        // Kick of multiple, asynchronous lookups
07        CompletableFuture page1 = gitHubLookupService.findUser("PivotalSoftware");
08        CompletableFuture page2 = gitHubLookupService.findUser("CloudFoundry");
09        CompletableFuture page3 = gitHubLookupService.findUser("Spring-Projects");
10 
11        // Wait until they are all done
12        //while (!(page1.isDone() && page2.isDone() && page3.isDone())) {
13          //  Thread.sleep(10); //10-millisecond pause between each check
14        //}
15 
16        //wait until all they are completed.
17        CompletableFuture.allOf(page1,page2,page3).join();
18        //I could join as well if interested.
19 
20        // Print results, including elapsed time
21        System.out.println("Elapsed time: " + (System.currentTimeMillis() - start) +" ms");
22        System.out.println(page1.get());
23        System.out.println(page2.get());
24        System.out.println(page3.get());
25    }



반응형

반응형

스프링에서 @Async로 비동기처리하기 @Async in Spring

[원문: http://www.baeldung.com/spring-async]


1. 개요 Overview

이 글에서 스프링의 비동기 실행 지원asynchronous execution support과 @Async annotation에 대해 살펴볼 것이다. 간단히 설명하면, @Async 어노테이션을 빈bean에 넣으면 별도의 쓰레드에서 실행되는 것이다. 이를테면 호출자는 호출된 메소드가 완료될 때까지 기다릴 필요가 없다.



2. Async 기능 켜기 Enable Async Support

자바 설정Java configuration으로 비동기 처리enabling asynchronous processing를 쓰려면 간단히 설정 클래스에 @EnableAsync를 추가해주기만 하면 된다:

1
2
3
@Configuration
@EnableAsync
public class SpringAsyncConfig { ... }

위의 어노테이션이면 충분하지만, 당신이 필요로 하는 몇가지 옵션 또한 설정해 줄 수 있다:

  • annotation – 기본값, @EnableAsync은 스프링의 Async 어노테이션을 감지하며 EJB 3.1javax.ejb.Asynchronous; 이 옵션으로 사용자 정의된 다른 어노테이션 또한 감지할 수 있다.
  • mode – 사용해야할 advice의 타입을 가르킨다 - JDK proxy 기반 또는 AspectJ weaving.
  • proxyTargetClass – 사용해야할 proxy의 타입을 가르킨다 - CGLIB 또는 JDK; 이 속성값은 오직 mode가 AdviceMode.PROXY 로 설정되어 있을때만 유효하다.
  • order – sets the order in which AsyncAnnotationBeanPostProcessor 가 적용해야할 순서를 설정한다; 단지 모든 현존의 프록시를 고려하기 위해 기본값으로 마지막부터 실행된다.

비동기 처리는 task 네임스페이스를 사용하여 XML 설정을 통해 사용할 수도 있다:

1
2
<task:executor id="myexecutor" pool-size="5"  />
<task:annotation-driven executor="myexecutor"/>

3. @Async 어노테이션 The @Async Annotation

먼저 규칙을 살펴보자 – @Async 는 두가지 제약사항이 있다:

  • 1. public 메소드에만 적용해야한다
  • 2. 셀프 호출self invocation – 같은 클래스안에서 async 메소드를 호출 – 은 작동하지않음

이 이유는 간단한데 메소드가 public이어야 프록시가 될수 있기 때문이고 셀프호출은 프록시를 우회하고 해당 메소드를 직접 호출하기때문에 작동하지않는 것이다.


3.1. 리턴타입이 없는 메소드 Methods with void Return Type

다음과 같이 간단한 설정으로 리턴타입이 void인 메소드가 비동기로 작동한다:

1
2
3
4
5
@Async
public void asyncMethodWithVoidReturnType() {
    System.out.println("Execute method asynchronously. " + Thread.currentThread().getName());
}

3.2. 리턴타입이 있는 메소드 Methods with Return Type

Future 객체에 실제 리턴값을 넣음으로서 @Async 는 리턴타입이 있는 메소드에 적용할 수 있다.can also be applied to a method with return type:

1
2
3
4
5
6
7
8
9
10
11


@Async
public Future<String> asyncMethodWithReturnType() {
    System.out.println("Execute method asynchronously - " + Thread.currentThread().getName());
    try {
        Thread.sleep(5000);
        return new AsyncResult<String>("hello world !!!!");
    } catch (InterruptedException e) {
        //
    }
    return null;
}

스프링은 또한 Future를 구현한 AsyncResult 클래스를 제공하며 이는 비동기 메소드 실행의 결과를 가져오는데 사용한다.

이제 위의 메소드를 호출하여 Future 객체를 사용해 비동기 처리의 결과값을 가져와보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void testAsyncAnnotationForMethodsWithReturnType()
    throws InterruptedException, ExecutionException {
    System.out.println("Invoking an asynchronous method. "
      + Thread.currentThread().getName());
    Future<String> future = asyncAnnotationExample.asyncMethodWithReturnType();
 
    while (true) {
        if (future.isDone()) {
            System.out.println("Result from asynchronous process - " + future.get());
            break;
        }
        System.out.println("Continue doing something else. ");
        Thread.sleep(1000);
    }
}

4. 실행자 The Executor

스프링은 기본값으로 SimpleAsyncTaskExecutor 를 사용하여 실제 메소드들을 비동기 실행한다. 기본 설정은 두가지 레벨로 오버라이드할 수 있다 - 어플리케이션 레벨 또는 개인 메소드 레벨.


4.1. 메소드 레벨로 실행자 오버라이드하기 Override the Executor at the Method Level

설정 클래스에서 필요한 실행자를 선언해주어야한다:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableAsync
public class SpringAsyncConfig {
     
    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        return new ThreadPoolTaskExecutor();
    }
}

그 후 실행자 이름을 @Async에서 속성값으로 제공해주어야 한다:

1
2
3
4
5
@Async("threadPoolTaskExecutor")
public void asyncMethodWithConfiguredExecutor() {
    System.out.println("Execute method with configured executor - "  + Thread.currentThread().getName());
}

4.2. 어플리케이션 레벨로 실행자 오버라이드하기 Override the Executor at the Application Level

이 경우 설정 클래스는AsyncConfigurer 인터페이스를 구현해주어야한다 - 이는 getAsyncExecutor() 메소드를 구현해야한다는 의미이다. 여기에 우리는 전체 어플리케이션을 위한 실행자를 리턴할 것이다 - 이는 이제 @Async로 어노테이션된 메소드를 실행하는 기본 실행자가 된다.

1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
     
    @Override
    public Executor getAsyncExecutor() {
        return new ThreadPoolTaskExecutor();
    }
     
}

5. 예외 처리하기 Exception Handling

메소드의 리턴타입이 Future일 경우 예외처리는 쉽다 - Future.get() 메소드가 예외를 발생한다.

하지만 리턴타입이 void일 때, 예외는 호출 스레드에 전달되지 않을 것이다. 따라서 우리는 예외 처리를 위한 추가 설정이 필요하다.

우리는 AsyncUncaughtExceptionHandler 인터페이스를 구현함으로서 커스텀 비동기 예외처리자를 만들것이다. handleUncaughtException() 메소드는 잡히지않은uncaught 비동기 예외가 발생할때 호출된다:

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomAsyncExceptionHandler  implements AsyncUncaughtExceptionHandler {
 
    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
        System.out.println("Exception message - " + throwable.getMessage());
        System.out.println("Method name - " + method.getName());
        for (Object param : obj) {
            System.out.println("Parameter value - " + param);
        }
    }
     
}

전 섹션에서 우리는 설정 클래스에 의해 구현된 AsyncConfigurer 인터페이스를 보았다. 그 일부로서 우리의 커스텀 비동기 예외처리자를 리턴하는 getAsyncUncaughtExceptionHandler() 메소드 또한 오버라이드해주어야한다:

1
2
3
4
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    return new CustomAsyncExceptionHandler();
}

6. 결 론 Conclusion

이 튜토리얼에서 우리는 스프링에서 비동기 코드를 동작하는 법을 둘러보았다. 매우 기본적인 설정과 어노테이션으로 작동해 보았을 뿐 아니라, 우리가 설정한 실행자, 또는 예외처리 전략과 같은 더 고급진 설정법 또한 둘러보았다.






반응형

반응형

스프링 부트와 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을 소스컨트롤에 올리는 일이 없도록 주의하자!


반응형

+ Recent posts