스프링부트 OAuth2 - 1. Facebook 인증

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

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

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

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

Facebook으로 로그인

프로젝트 생성

Spring Starter Project를 생성한다. Dependency는 Web을 선택하고 프로젝트 생성을 완료한다.

프로젝트 생성

페이지 추가

index.html파일을 src/main/resources/static에 추가하고 아래와 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>OAuth2 Sample Application</title>
<meta name="description" content="OAuth2 Sample Application"/>
<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>OAuth2 Sample Application</h1>
<div class="container"></div>
</body>
</html>

<head> 안쪽에 webjars가 보이는데 클라이언트 사이드에서 사용하는 javascript 또는 css 라이브러리 등을 jar 형태로 import시킬 수 있는 기능이라고 보면 되겠다. pom.xml에 해당 내용을 추가해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- webjars -->
<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-core</artifactId>
</dependency>

bootstrap과 jquery를 추가하였다. webjars-locator-core라는 녀석은 앞서 추가한 webjars들의 path를 지정해주는 역할이라고 보면 좋을 것 같다. 앞서 index.html에서 /webjars/**에 위치한 js 파일과 css파일을 불러오기 위한 dependency이다.

Security

어플리케이션을 안전하게 만들기 위해서는(다소 어색한 말이지만 guide에 있는 말을 빌려왔다.) Spring Security와 관련된 dependency를 적용해야 한다. pom.xml에 추가해보자.

1
2
3
4
5
6
7
8
9
10
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>

Facebook으로 연결하기 위하여 Application.java@EnableOAuth2Sso 어노테이션을 추가한다.

1
2
3
4
5
6
7
@SpringBootApplication
@EnableOAuth2Sso // <-- 이부분을 추가한다.
public class SpringBootOauth2Application {
public static void main(String[] args) {
SpringApplication.run(SpringBootOauth2Application.class, args);
}
}

application.yml 또는 application.properties를 열어서 다음과 같이 페이스북 관련 정보를 기입한다. 정보에 대한 내용은 페이스북 개발자 페이지에서 앱 생성 후 oauth2 관련 세팅을 완료한 후에 다음 스텝으로 넘어가자.

1
2
3
4
5
6
7
8
9
10
11
12
security:
oauth2:
client:
client-id: CLIENT-ID
client-secret: CLIENT-SECRET
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

서버를 실행하고 http://localhost:8080으로 접속해보자. 올바르게 설정이 완료되었다면 곧바로 페이스북 앱에 대해 동의하는 화면이 나올 것이다.

서버 실행

앱에서 정보 수신에 대해 동의버튼을 누르면 원래 우리가 호출하려 했던 index.html과 같은 화면이 등장할 것이다. 이 때 (크롬 기준) 개발자도구를 열고 Network 탭을 확인하면 아래와같이 Cookie에 JSESSIONID가 생성된 것을 확인할 수 있다.

로그인 후 확인

Welcome Page 추가하기

Facebook 로그인 링크와 로그인 되었을 때 사용자 정보를 출력하는 페이지를 작성해보자. 다음과 같이 index.html을 작성한다.

1
2
3
4
5
6
<div class="container unauthenticated">
Facebook : <a href="/login">클릭</a>
</div>
<div class="container authenticated" style="display:none">
로그인 되었습니다 : <span id="user"></span>
</div>

authenticatedunauthenticated 클래스를 통해 인증 여부를 확인하고 그에 따라 display를 해주는 부분이다. 이에 대한 내용을 jQuery를 사용하여 작성하자

1
2
3
4
5
6
7
<script type="text/javascript">
$.get("/user", function(data) {
$("#user").html(data.userAuthentication.details.name);
$(".unauthenticated").hide()
$(".authenticated").show()
});
</script>

서버사이드 변경

위 javascript 소스코드처럼 /user 에 대한 부분을 작성해야 한다. UserController.java를 작성하자.

1
2
3
4
5
6
7
@RestController
public class UserController {
@RequestMapping("/user")
public Principal user(Principal principal) {
return principal;
}
}

본래 Principal 전체를 return 하는 것은 권장되는 방법이 아니지만 샘플 프로젝트의 빠른 작업을 위해 이와 같이 사용한다. 추후 브라우저에서 보이고싶지 않은 부분에 대해서는 숨기는 처리를 한다.

잘 작동하지만 사용자가 링크를 클릭 할 수가 없다. 로그인 링크를 표시하려면 WebSecurityConfigurer를 추가하여 설정을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Application.java
@SpringBootApplication
@EnableOAuth2Sso
public class SpringBootOauth2Application extends WebSecurityConfigurerAdapter{
public static void main(String[] args) {
SpringApplication.run(SpringBootOauth2Application.class, args);
}

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

WebSecurityConfigurerAdapter를 상속받고 configure()를 작성한다. 그리고 인증을 처리하는 로그인 엔드포인트와 다른 요청들에 대한 인증이 필요하도록 설정한다.

이제 서버를 재시작하고 접속하면 로그인 링크가 등장하고 로그인을 실행하면 로그인된 사용자의 정보가 등장할 것이다.

로그아웃 버튼 추가하기

index.htmlauthenticated클래스 안쪽에 로그아웃 버튼을 추가해보자. 또한 로그아웃 버튼에 대한 이벤트 함수를 작성한다.

1
2
3
4
5
6
<div class="container authenticated" style="display:none">
로그인 되었습니다 : <span id="user"></span>
<div>
<button onClick="logout()" class="btn btn-primary">로그아웃</button>
</div>
</div>
1
2
3
4
5
6
7
8
var logout = function() {
$.post("/logout", function() {
$("#user").html('');
$(".unauthenticated").show();
$(".authenticated").hide();
});
return true;
}

서버사이드 역시 /logout에 대한 엔드포인트가 필요하다. 조금 전 작성했던 configure()에 로그아웃에 대한 엔드포인트를 작성하자.

/logoutPOST메소드로 요청할 필요가 있다. 또한 CSRF(Cross Site Request Forgery)로부터 보호해야 한다. 현재 세션에 이로부터 보호하는 토큰값이 있기 때문에 클라이언트 단에서 이에 대한 값을 가지고 있어야 한다.

요즘 성행하는 자바스크립트 프레임워크에는 위와 같은 문제에 대한 방도가 내장이 되어있어서 사용해주면 된다. 자세한 내용은 좀 더 봐야 할 것 같다. 아직 이부분에 대해서는 심화학습이 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
// Application.java
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**", "/webjars/**", "/error**")
.permitAll()
.anyRequest()
.authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}

CSRF 토큰을 클라이언트 단에 추가하기

이번 샘플에서는 자바스크립트 프레임워크 등을 사용하지 않기 때문에 pom.xml에 라이브러리를 추가하는 것으로 대체한다. 라이브러리를 추가하고 html에 적용해보자.

1
2
3
4
5
<dependency>
<groupId>org.webjars</groupId>
<artifactId>js-cookie</artifactId>
<version>2.1.0</version>
</dependency>
1
<script type="text/javascript" src="/webjars/js-cookie/js.cookie.js"></script>
1
2
3
4
5
6
7
8
9
10
$.ajaxSetup({
beforeSend : function(xhr, settings) {
if (settings.type == 'POST' || settings.type == 'PUT'
|| settings.type == 'DELETE') {
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
xhr.setRequestHeader("X-XSRF-TOKEN", Cookies.get('XSRF-TOKEN'));
}
}
}
});

어플리케이션을 재시작하고 결과를 확인해본다.

OAuth2 클라이언트에 대한 설정

@EnableOAuth2Sso 어노테이션에는 OAuth2 클라이언트와 인증 두 가지 기능이 있다. 클라이언트 기능은 인증 서버(이번 경우는 Facebook)에서 제공하는 OAuth2 리소스와 통신하는데 사용한다. 인증 기능은 해당 어플리케이션을 타 Spring Security와 연동시키는 것이다. 클라이언트 기능은 Spring Security OAuth2에 의해 제공되는 기능이고, @EnableOAuth2Client에 의해 enable/disable이 가능하다. 이번에는 @EnableOAuth2Sso 어노테이션을 사용하지 않고 @EnableOAuth2Client 어노테이션을 사용해보자.

우선 OAuth2ClientContext를 주입시키고 인증 filter를 security 설정에 추가할 수 있게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootApplication
@EnableOAuth2Client // 변경
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {

@Autowired
OAuth2ClientContext oauth2ClientContext; // 의존성 주입

@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**", "/webjars/**", "/error**")
.permitAll()
.anyRequest()
.authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
.and().addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
}

OAuth2ClientContext를 사용하는 ssoFilter()를 작성한다.

1
2
3
4
5
6
7
8
9
private Filter ssoFilter() {
OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
facebookFilter.setRestTemplate(facebookTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());
tokenServices.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(tokenServices);
return facebookFilter;
}

위 필터에서는 facebook과 관련된 application 속성을 가지고 client 정보를 등록한다.

1
2
3
4
5
@Bean
@ConfigurationProperties("facebook.client") // application 속성
public AuthorizationCodeResourceDetails facebook() {
return new AuthorizationCodeResourceDetails();
}

그리고 facebook 사용자 정보를 가져오는 엔드포인트를 등록하여 인증처리를 한다. (facebookResource() 작성)

1
2
3
4
5
@Bean
@ConfigurationProperties("facebook.resource")
public ResourceServerProperties facebookResource() {
return new ResourceServerProperties();
}

위 수정사항들을 추가하면서 application.yml(application.properties)의 속성을 바꿔줄 필요가 있다.

1
2
3
4
5
6
7
8
9
10
11
facebook:
client:
client-id: CLIENT-ID
client-secret: CLIENT-SECRET
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

그리고 index.html파일을 수정해준다. 링크의 url을 변경한다.

1
2
3
4
5
6
<h1>로그인</h1>
<div class="container unauthenticated">
<div>
Facebook : <a href="/login/facebook">클릭</a> <!-- url 변경 -->
</div>
</div>

마지막으로 어플리케이션에서 Facebook으로 redirect하는 것을 지원하는 Filter를 작성하고 Bean으로 등록한다. 필터는 이미 @EnableOAuth2Client를 통해 등록된 것이기 때문에 정상적인 필터 작동을 위해 연결시켜줄 필요가 있다.

1
2
3
4
5
6
7
@Bean
public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}

이제 마지막으로 로그인 / 로그아웃을 테스트해본다. 잘되는 모습을 볼 수 있다.

막바지의 어노테이션 변경 부분부터 다소 어려운 부분인 것 같다. 평소 못보았던 클래스도 많이 보이고 OAuth2와 관련된 필터 설정이 생각보다 손이 많이 가는 것을 보았다. 포스팅 가장 처음에 말했듯이 가장 어려우면서도 귀찮은 부분인 인증 과정에 대해 책을 정독하듯이 따라오게 되었는데, 앞으로 이 부분(특히 Filter를 적용하는 부분)에 대해 더 심도있게 알아보고 싶다. 그래도 한 번 해놓으면 또 써먹을 날이 오겠지..

다음 포스팅에서는 이와 유사하게 Github 인증을 진행할 것이다.

소스코드

https://github.com/hwiVeloper/SpringBootStudy/tree/2c714d3ba9c511e790e087b9362842017dcfc9f3/spring-boot-oauth2