스프링부트 OAuth2 - 3. OAuth 인증 서버 구축

https://spring.io/guides/tutorials/spring-boot-oauth2/ 를 참조하여 작성하였습니다.

개인적으로 서버사이드 프로그래밍을 하면서 가장 어렵기도 하고 귀찮은 부분이 인증하는 부분이라고 생각한다. 로직을 작성해 나간다기 보다는 클라이언트와 서버사이드 간의 통신을 통해 사용자를 인지하고 이를 세션 등으로 관리할 뿐 아니라, 권한 문제까지 연결되는 부분이기 때문이다. 고로 가장 민감한 부분이라고 본다.

스프링 공식 홈페이지의 OAuth2 가이드를 보면 다른 가이드와는 다르게 다소 스크롤의 압박이 느껴진다. 따라서 크게 세 부분으로 나누어 포스팅이 진행될 예정이다.

  1. Facebook으로 로그인
  2. Github로 로그인
  3. OAuth2 인증 서버 구축

인증 서버 구축

이번 포스팅에서는 앞서 만들었던 어플리케이션을 OAuth2 인증 서버로 만든다. 앞서 구현했던 facebookgithub를 통한 인증을 사용하지만, 별도의 자체 액세스 토큰을 만들어서 인증을 수행한다. 이 토큰을 사용하여 백엔드 자원을 보호하고 타 어플리케이션과 SSO를 수행한다.

인증 설정 다듬기

인증 서버를 본격적으로 구축하기 전에 githubfacebook에 관련된 설정을 먼저 다듬어야 할 필요가 있다. ssoFilter()를 수정해보자.

1
2
3
4
5
6
7
8
9
10
11
12
// Application.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;
}

ssoFilter()overloading한 새로운 메서드를 작성해야 한다.

1
2
3
4
5
6
7
8
9
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(client.getResource().getUserInfoUri(), client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
return filter;
}

이전에 ssoFilter()에 작성한 내용과 유사하지만 약간의 공통화 과정을 거친 것이라 보면 되겠다. ClientResources라는 오브젝트는 존재하지 않는다. 따라서 별도의 wrapper 객체를 생성한다. 이는 별도의 @Beaㅜ로 선언된 OAuth2ProtectedResourceDetailsResourceServerProperties를 통합하는 역할이라 보면 되겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ClientResources {
@NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();

@NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();

public AuthorizationCodeResourceDetails getClient() {
return client;
}

public ResourceServerProperties getResource() {
return resource;
}
}

wrapper 클래스는 @NestedConfigurationProperty를 사용하여 어노테이션 프로세서가 하나의 값을 표현하지 않고 중첩 형식을 나타내기 때문에 메타데이터에 접근하여 크롤링하도록 지시한다.

위 wrapper 클래스 작성으로 spring 설정을 이전처럼 사용할 수 있을 뿐만 아니라 공통화를 지향한 결과이다. 마지막으로 각각 provider의 설정값을 가져오도록 @Bean을 생성한다.

1
2
3
4
5
6
7
8
9
10
11
@Bean
@ConfigurationProperties("github")
public ClientResources github() {
return new ClientResources();
}

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

인증 서버 가용 상태로 만들기

특별히 어마어마한 타이핑이 필요하지 않고 어노테이션 하나로 끝낼 수 있다.

1
2
3
4
5
6
@SpringBootApplication
@EnableOAuth2Client
@EnableAuthorizationServer // 추가
public class SpringBootOauth2Application extends WebSecurityConfigurerAdapter{
// ...
}

새로운 어노테이션을 추가하게 되면 필요한 엔드포인트와 security를 어플리케이션에 로드할 것이다. 그리고 몇가지 OAuth2 클라이언트에 관한 정보를 설정해야 한다.

1
2
3
4
5
6
7
8
# application.yml
security:
oauth2:
client:
client-id: acme
client-secret: acmesecret
scope: read, write
auto-approve-scopes: '.*'

위 작업은 facebook.client*, github.client*에 대한 작업과 동일한 것이다. auto-approve-scopes는 위 설정처럼 정규표현식으로 작성이 가능하다. 이번 포스팅에서 작성하는 샘플에서는 특별히 제한을 두지 않기 때문에 모든 것을 허용하지만, 실제 프로젝트나 운영 단계에서는 세부적으로 설정할 필요가 있다.

인증 서버 구축을 마무리하기 위해서 UI에 관련된 security 설정이 필요하다. 샘플 어플리케이션이기 때문에 많은 설정이 필요하진 않지만, oauth와 관련된 endpoint 등의 필요한 부분에 설정해 줄 필요가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**") // ①
.authorizeRequests()
.antMatchers("/", "/login**", "/webjars/**", "/error**").permitAll() // ②
.anyRequest().authenticated() // ③
.and().exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/")) // ④
.and().logout().logoutSuccessUrl("/").permitAll()
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and().addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
}
  • ① : 기본적으로 모든 요청은 보호된다.
  • ② : 로그인 엔드포인트는 제외된다.
  • ③ : 그 외의 모든 요청은 인증이 필요하다.
  • ④ : 인증되지 않은 사용자는 /로 redirect 된다.

Access Token 얻기

이제 우리가 구축한 인증 서버를 통해 Access Token을 얻을 수 있다. 가장 쉬운 방법은 “acme” 클라이언트를 통해서이다. curl 명령어를 사용하여 토큰을 얻어보자

1
2
3
4
$ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=client_credentials
# % Total % Received % Xferd Average Speed Time Time Time Current
# Dload Upload Total Spent Left Speed
# 100 146 0 117 100 29 7312 1812 --:--:-- --:--:-- --:--:-- 9125{"access_token":"c42ba0f2-543e-4275-9eb2-efb1f48fa680","token_type":"bearer","expires_in":43186,"scope":"read write"}

단순히 토큰을 얻는 것만으로는 뭔가 완벽한 어플리케이션을 위해서는 부족해 보인다. 특정 유저에게 생성하도록 만들어야 한다. 스프링 어플리케이션을 구동했을 때 아마 Using generated security password: ... 처럼 자동 생성되는 기본 암호를 볼 수 있을 것이다. 이를 이용하여 다시 토큰을 얻어보자.

1
2
3
4
$ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=password -d username=user -d password=...
# % Total % Received % Xferd Average Speed Time Time Time Current
# Dload Upload Total Spent Left Speed
# 100 251 0 172 100 79 1577 724 --:--:-- --:--:-- --:--:-- 2302{"access_token":"629d6260-5eba-43e7-9072-7608b6b46254","token_type":"bearer","refresh_token":"bd7e65ce-6663-40b1-8307-04787221197f","expires_in":43199,"scope":"read write"}

명령어의 “…” 부분에는 앞서 말한 자동생성되는 암호를 기입해주어서 curl명령을 날리면 역시 토큰을 받을 수 있다. 현재 테스트한 방법 외에 일반적인 소셜 로그인에서는 “인증 코드”를 받아야 한다. 즉, 이를 통해 redirect, cookie 등을 핸들링하거나 외부 provider로부터 UI를 렌더링할 수 있어야 한다.

클라이언트 어플리케이션 생성하기

우리가 구축한 인증 서버에 필요한 client를 생성해보자. ClientApplication.java를 생성할 것이다. 단, 기존 *Application.java와 같은 패키지(또는 서브 패키지)에 위치해서는 안된다. 스프링은 기존의 Application을 구동하면서 ClientApplication을 하나의 설정으로 구동시킬 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ClientApplication.java
// src/test/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파일을 생성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# client.yml
server:
port: 9999
servlet:
context-path: /client
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
spring:
main:
allow-bean-definition-overriding: true

메인이 되는 어플리케이션 설정과 비슷하지만, facebook이나 github 대신 “acme” 클라이언트로 통하도록 설정이 되어있다. 어플리케이션은 9999 포트에서 띄워져서 기존 포트와의 충돌을 방지한다. server.context-path의 값을 별도로 세팅하였다. 따라서 http://localhost:9999/client 를 통해 확인이 가능하다. 어플리케이션을 시작하면 아래 그림처럼 두개의 포트가 띄워질 것이다.(사실 이 부분을 늦게 확인하는 바람에 어떻게 9999포트가 열린 상태인지 확인을 못했다.)

Client쪽 port가 올라온 모습

사용자 정보 엔드포인트 보호하기

Single sign on을 위한 새로운 인증 서버를 사용하기 위해 facebook과 github를 사용하는데, 사용자가 인증할 때 생성된 쿠키로 보호된다. 로컬에 부여 된 액세스 토큰과 함께 보안을 유지하기 위해 기존의 엔드포인트를 재사용하고 새 경로로 alias를 지정할 수 있다.

1
2
3
4
5
6
@RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
Map<String, String> map = new LinkedHashMap<>();
map.put("name", principal.getName());
return map;
}

기존 메서드에서 PrincipalMap으로 바꾼 이유는 브라우저에서 민감한 정보를 노출시키지 않기 위해 숨기는 것이다. 추가적으로 필요한 부분이 있다면 Map에 추가적으로 put해줌으로써 브라우저에 노출시키는 것이 가능하다.

“/me” 경로는 어플리케이션이 리소스 서버로 선언됨으로서 access token으로 보호된다. 다음과 같이 새로운 설정 클래스를 만들어보자

1
2
3
4
5
6
7
8
9
10
// Application.java
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/me")
.authorizeRequests().anyRequest().authenticated();
}
}

@Order 어노테이션을 추가한다. SecurityPropertiesACCESS_OVERRIDE_ORDERdeprecated 상태이므로 넘어간다. 어플리케이션이 security 수행 시, 엔드포인트에 대한 우선순위가 있지만 여기서는 정하지 않겠다.

테스트

http://localhost:9999/client/ 로 접속하게 되면 redirect가 되면서 localhost:8080으로 이동할 것이다. facebook과 github 로그인 중 하나를 선택하면 인증이 시작되고 완료되면 다시 http://localhost:9999/client/ 로 이동될 것이다. 그리고 인증된 사용자의 이름이 출력되면서 테스트를 마무리할 수 있다.

Client Application 테스트 결과

마무리

길고 긴 OAuth2인증부터 서버 구축까지 마무리를 해보았다. Spring Security는 이러한 기능도 하고 있지만 더 많은 기능을 담고 있는 강력한 모듈이기 때문에 추가적인 학습이 필요하다. 사실 이 포스팅 뒤에 에러 처리 등의 자잘한 과정이 남아있지만 이번 포스팅에서는 다루지 않겠다. 아마 여기까지 따라오기만 해도 꽤나 지칠 수 있기 때문이다. 이정도면 OAuth2의 기능을 살펴보았다고 해도 좋다. 참고 URL은 포스팅 처음에 링크를 걸어두었으니 가서 참고하면 좋을 것이다.(영어긴 하지만..)

소스코드

https://github.com/hwiVeloper/SpringBootStudy/tree/495193fb5ca875be9b7833765ce6379971bc0ba0/spring-boot-oauth2

스프링부트 OAuth2 - 3. OAuth 인증 서버 구축

https://hwiveloper.github.io/2019/04/21/spring-boot-oauth2-authserver/

Author

hwiVeloper

Posted on

2019-04-21

Updated on

2022-12-29

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

댓글

You forgot to set the shortname for Disqus. Please set it in _config.yml.