https://spring.io/guides/tutorials/spring-boot-oauth2/ 를 참조하여 작성하였습니다.
개인적으로 서버사이드 프로그래밍을 하면서 가장 어렵기도 하고 귀찮은 부분이 인증
하는 부분이라고 생각한다. 로직을 작성해 나간다기 보다는 클라이언트와 서버사이드 간의 통신을 통해 사용자를 인지하고 이를 세션 등으로 관리할 뿐 아니라, 권한 문제까지 연결되는 부분이기 때문이다. 고로 가장 민감한 부분이라고 본다.
스프링 공식 홈페이지의 OAuth2 가이드 를 보면 다른 가이드와는 다르게 다소 스크롤의 압박이 느껴진다. 따라서 크게 세 부분으로 나누어 포스팅이 진행될 예정이다.
Facebook으로 로그인
Github로 로그인
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 <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 <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 >
authenticated
와 unauthenticated
클래스를 통해 인증 여부를 확인하고 그에 따라 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 @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.html
에 authenticated
클래스 안쪽에 로그아웃 버튼을 추가해보자. 또한 로그아웃 버튼에 대한 이벤트 함수를 작성한다.
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()
에 로그아웃에 대한 엔드포인트를 작성하자.
/logout
은 POST
메소드로 요청할 필요가 있다. 또한 CSRF(Cross Site Request Forgery)
로부터 보호해야 한다. 현재 세션에 이로부터 보호하는 토큰값이 있기 때문에 클라이언트 단에서 이에 대한 값을 가지고 있어야 한다.
요즘 성행하는 자바스크립트 프레임워크에는 위와 같은 문제에 대한 방도가 내장이 되어있어서 사용해주면 된다. 자세한 내용은 좀 더 봐야 할 것 같다. 아직 이부분에 대해서는 심화학습이 필요하다.
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().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") 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 > </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