티스토리 뷰

Dev/Spring

Spring Boot And oAuth2

마이스토리 2017. 6. 15. 14:37

이 글인 spring.io 의 Tutorial (https://spring.io/guides/tutorials/spring-boot-oauth2/) 을 간단하게 번역, 정리한 글입니다.


Spring Boot 기반에서 Spring OAuth 를 사용하여 페이스북과 같은 소셜 로그인을 처리하는 간단한 샘플입니다.

클라이언트 구현에는 AngularJS 및 WebJar 가 사용되었습니다.


프로젝트 생성

STS에서 New Spring Starter Project 위자드 기능을 통해 기본 Web Project를 생성합니다.(여기 샘플은 maven기반)



       


기본 연동 작업

홈페이지 추가

src/main/resources/static/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>


webjars dependency 추가

<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>


어플리케이션 보안을 위한 dependency 추가

<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>


Spring Boot 메인 클래스에 @EnableOAuth2Sso 어노테이션 추가

@SpringBootApplication
@EnableOAuth2Sso
public class SocialApplication {
  ...
}


OAuth2 를 위한 설정파일

src/main/resouces/application.yml (보다 나은 가독성을 위해 YAML 사용)
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


clientId 및 clientSecret은 페이북에 앱을 등록하면 얻을 수 있다. ( https://developers.facebook.com/ )


그리고, 등록된 페이스 북 앱 설정에서 "플랫폼 추가"를 통해 웹사이트 URL을 등록해야 한다. 안 그러면 페이북 로그인 페이지로 이동 시redirction 페이지가 없다고 에러남.


여기까지만 따라하면 일단 페이스북과 로그인 연동되는 앱이 만들어진다. 앱을 실행한 후 http://localhost:8080/ 을 접속하면, 페이스북 로그인 페이지로 리다이렉트되고 로그인이 완료되면 index.html 페이지로 리다이렉트 된다.


웹컴 페이지 구성

지금까지는 앱 전체가 보안이 필요하기 때문에 index.html 페이지조차도 로그인이 필요하다.

추가로 웰컴 페이지를 구성해서 로그인 전일 경우 로그인 링크를 보여주고, 로그인 후라면 로그인 사용자명이 나타나도록 index.html 페이를 구성해보자.


index.html 수정

AngularJS를 시작하기 위해 <body>를 Angular 앱 컨테이너로 표시.
<body ng-app="app" ng-controller="home as home">

</body>


<div> 바인딩. 로그인 상태에 따라.

<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>


인증된 플래그가 있는 'home"컨트롤러와 인증된 사용자를 설명하는 사용자 개체 처리

<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>


서버측 수정

현재 인증된 사용자 정보를 제공하는 /user 엔드포인트에 대한 컨트롤러 구성
간단하게 스프링부트 메인클레스에 @RestController를 추가하고, @RequestMapping 메소드 구현

@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);

  }


}

# 여기 샘플에서는 전체 Principal을 전달했지만, 실제는 필요한 사용자정보만 제공하도록 수정해야함.


그리고, index 페이지, login 링크, /webjars 리소스에 대해서는 보안을 적용하기 않기 위해 아래와 같이 스프링 시큐리티 설정 추가.

@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();

  }


}


여기까지 적용하고 다시 앱을 실행하고, http://localhost:8080 을 접속하면 변경된 내용을 확인할 수 있다. ( 브라우저 캐시 지우고 다시 접속)


로그아웃 기능 추가

클라이언트 수정

index.html 에 로그아웃 링크를 추가하고, logout() 자바스크립트 추가


<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>


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 endpoint 추가

스프링 시큐리티는 세션종료와 쿠키 무효화 처리를 하는 /logout 엔드포인트를 기본적으로 포함하고 있다.

이를 구성하려면 WebSecurityConfigurer에서 configure() 메소드를 확장한다.

@Override

protected void configure(HttpSecurity http) throws Exception {

  http.antMatcher("/**")

    ... // existing code here

    .and().logout().logoutSuccessUrl("/").permitAll();

}


/logout 은 POST로 처리되고 CSRF에서 사용자를 보호하기 위해 요청에 토큰을 포함시켜야 함. 

AngularJS의 XSRF(CSRF) 지원 기능을 Spring Security와 연동하기 위해 아래 코드 추가.

@Override

protected void configure(HttpSecurity http) throws Exception {

  http.antMatcher("/**")

    ... // existing code here

    .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

}


여기까지 적용하고 앱을 다시 실행해서 테스트하면 logout 처리를 확인할 수 있다.


OAuth2 Client 수동설정

@EnableOAuth2Sso 에는 OAuth2 클라이언트와 인증 2가지의 기능있다. 

클라이언트는 재사용 가능하고 OAuth2 리소드와 상호작용할 때도 사용할 수 있다.


클라이언트 부분은 Spring Security OAuth2에서 제공되고 @EnableOAuth2Client 어노테이션으로 처리한다.


기존의 @EnableOAuth2Sso를 제거하고 @EnableOAuth2Client 추가한다.

@SpringBootApplication

@EnableOAuth2Client

@RestController

public class SocialApplication extends WebSecurityConfigurerAdapter {

  ...

}


이렇게 하면, OAuth2ClientContext 생성되고, 이를 이용해서 인증필터를 추가한다.

@SpringBootApplication

@EnableOAuth2Client

@RestController

public class SocialApplication extends WebSecurityConfigurerAdapter {


  @Autowired

  OAuth2ClientContext oauth2ClientContext;


  @Override

  protected void configure(HttpSecurity http) throws Exception {

    http.antMatcher("/**")

      ...

      .and().addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);

  }


  ...


}



이 필터는 아래의 메소드 구현을 통해 만들어진다.

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을 통한 클라이언트 등록에 대해서도 알아야 한다.

 @Bean

  @ConfigurationProperties("facebook.client")

  public AuthorizationCodeResourceDetails facebook() {

    return new AuthorizationCodeResourceDetails();

  }


또, 인증을 완료하려면 페이스북에서 사용자 정보를 제공하는 endpoint를 알아야 한다.

 @Bean

  @ConfigurationProperties("facebook.resource")

  public ResourceServerProperties facebookResource() {

    return new ResourceServerProperties();

  }


위 bean 생성 시 사용한 properties를 맞추가 위해 security.oauth2 대신 facebook 구성 접두어로 변경합니다.

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



위 필터 선언 시 로그인 경로를 facebook 전용으로 변경했으므로 html에도 동일하게 적용한다.

<h1>Login</h1>

<div class="container" ng-show="!home.authenticated">

<div>

With Facebook: <a href="/login/facebook">click here</a>

</div>

</div>


리다이렉트 처리

마지막으로 앱에서 facebook으로의 리다이렉션을 명시적으로 지원하도록 한다.

FilterRegistrationBean을 사용해서 주 스프링 보안 필터 전에 필터(OAuth2ClientContextFilter)를 등록하여 올바른 순서로 호출되도록한다.

이렇게 하면 인증요청 예외에 대한 리다이렉션을 처리할 수 있다.

       @Bean

public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {

   FilterRegistrationBean registration = new FilterRegistrationBean();

   registration.setFilter(filter);

   registration.setOrder(-100);

   return registration;

}



Github과 연동 추가하기

사용자가 faceboox과 github을 선택하여 로그인할 수 있도록 기능 추가.

Github 링크 추가

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>

# authentication provider가 여러 개 추가될 경우, 각 프로바이더가 반환하는 사용자정보에 대해 추의하여야 한다.
Github과 facebook은 동일한 위치에 "name" 필드를 가지고 있으므로 추가 변경을 필요없다.

Github 인증필터 추가하기

기존 필터 생성 메소드에 로직 추가. 둘 이상의 인증경로를 처리할 수 있는 composite로 대체.
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);
  UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());
  tokenServices.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(tokenServices);
  filters.add(facebookFilter);

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

  filter.setFilters(filters);
  return filter;

}

@Bean
@ConfigurationProperties("github.client")
public AuthorizationCodeResourceDetails github() {
return new AuthorizationCodeResourceDetails();
}

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

설정 추가 ( in 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

facebook과 마찬가지로 github 에서도 oAuth 앱을 등록. https://github.com/settings/developers

여기까지 수정하고 앱을 다시 실행하면 facebook과 github을 선택하여 로그인 처리할 수 있음.





댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30