본문 바로가기
Back-End/Spring

Spring Boot & OAuth2 기반 소셜 댓글 시스템 개발하기

by Havi 2017. 6. 24.

회사에서의 잉여력 + 개인의 잉여력을 가지고 회사에도 도움 + 개인적인 공부에도 도움이 되는 프로젝트를 진행해 보고 싶었습니다. 제가 맡고 있는 서비스에 아쉬운점이 소셜 댓글이 없다는 것인데(ㅠ.ㅠ) 아쉬움도 달래고 공부도 하고 제 서비스에 댓글기능도 붙일겸 직접 소셜 인증(페이스북, 구글, 트위터, 카카오) & 간단한 댓글 Getting Started를 개발하기로 야심찬 계획(?)을 세웠습니다.
모든 소스는 github에 있습니다.

개요

목표는 페이스북, 구글, 트위터, 카카오 등 국내에서 많이 쓰이는 서비스들의 OAuth인증을 통한 댓글 시스템 구현하기! 
여기서 트위터를 제외한 다른 인증은 모두 OAuth2를 사용합니다. Spring에는 이를 구현한 고마운 라이브러리인 Spring Social과 Spring Security OAuth2가 있습니다. 전자의 경우 마지막 업데이트가 2년전이고 정해진 디비 스키마에 데이터가 저장되는 방식이라 커스터마이징한 개발이 가능한 후자를 선택하였습니다. Spring Security OAuth2는 인증과 개인정보 API가 내부로직을 몰라도 될 만큼 편리하게 구현되어 있어 그냥 갔다 쓰시면 됩니다. 그래도 개발자라면 어떻게 동작하는지 정도는 알아야 겠다는 마음가짐(?) 때문에 동작 프로세스 정도는 소스를 까보면서 분석해 보았습니다.(추후 세션에서...)

cf.트위터 developer 사이트를 들어가 보시면 Application-only authentication으로 OAuth2를 제공해 준다고 설명되어 있습니다. 하지만 이는 Client Credentials Grant로 Application 본인에 대한 인증만 사용할 수 있고 유저에 대한 정보를 가져올 수 없어서 제가 만드는 프로젝트에서는 부합하지 못하였습니다. 다음 세션에서 더 자세히 설명하겠습니다.

필요한 정보 모으기(OAuth2란?)

OAuth는 임시 인증을 위한 방식으로 Token을 사용하는데 이를 표준적인 방법으로 통일한 것입니다. OAuth2는 OAuth protocol의 2버전입니다. 이 프로토콜은 3rd party를 위한 범용적인 인증 표준이 됩니다. OAuth2에서 제공하는 인증 타입 방식은 현재 4가지가 있습니다.(Authorization grant types, Implicit Grant, Resource Owner Password Credentials Grant, Client Credentials Grant 등) 그중 저에게 필요한 방식은 Authorization grant types입니다. Authorization grant types는 웹 서버에서 long-lived access token을 사용하여 사용자 인증을 처리하는 방식으로 제가 선택한 페이스북, 구글, 카카오가 사용하는 방식입니다. 아래 그림의 Flow를 보시면 더 이해하기 쉽습니다.


  • Resource Owner: 인증이 필요한 유저
  • Client: 웹 사이트
  • Authorization Server: 페이스북/구글/카카오 서버
  • Resource Server: 페이스북/구글/카카오 서버

반면에 개요에서 말한 트위터가 제공하는 Client Credentials Grant 방식은 client 자신이 resource owner가 되는 방식입니다. 이 방식은 client 이외의 다른 resource owner로부터 정보를 얻을 수 있는 권한이 없습니다. 애초에 resource owner가 없으므로 사용자의 개인정보를 얻을 수 없는 방식입니다. 따라서 제가 만들려는 소셜 댓글 플랫폼에는 사용할 수 없었습니다.(그래서 트위터는 OAuth1방식으로...ㅠㅠ)


번외로 Implicit Grant 방식은 Authorization grant types처럼 서버와 서버에서 인증을 수행하는 방식으로 클라이언트가 token이나 secret이 노출되지 않는 것과는 다르게 javascript처럼 resource owner쪽에서 전적으로 인증을 수행하는 방식입니다.

Spring Security OAuth2에는 인증타입이 모두 구현되어 있습니다

프로젝트 환경

다음은 프로젝트 개발 환경과 사용한 라이브러리입니다.

  • Java8
  • Spring Boot 1.5.2
  • Spring Security Oauth2
  • Spring Social Twitter
  • JPA
  • lombok
  • logback
  • Gradle 3.5
  • h2
  • embedded redis(for session)
  • Freemarker
  • handlebars.js

설계

인증요청시 Spring Security에 설정된 필터를 통해 인증이 수행되고 인증이 완료되면 redis를 사용해 세션정보를 담습니다. 회원에 대한 정보는 h2 db에 심플한 정보로 저장하여 사용합니다.

기본 구현 프로세스

소셜정보 설정

소셜관련 clientId, clientSecret의 정보를 application.yml에 넣어줍니다. 페이스북의 경우는 userInfo정보를 가져오기 위한 API 규격이 다릅니다. 다른 소셜인증의 경우 필요한 디폴트 정보가 왠만큼 다 있고 scope에 요청정보를 명시해 주는 형식이지만 페이스북은 fields파라미터를 사용하여 fields=id,name,email 형식으로 요청해야 정상적으로 동작합니다.

clientAuthenticationScheme?

clientAuthenticationScheme의 경우 디폴트는 header로 지정되며 form과 query는 같은 방식으로 동작합니다. 이 로직 처리는 DefaultClientAuthenticationHandler 클래스에서 진행되며 header의 경우 아래와 같이 clientId와 clientSecret을 Base64로 인코딩하여 헤더에 포함되는 형식으로 request를 요청합니다. 이에 관한 자세한 사항은 OAuth2 Spec 문서를 참고하시기 바랍니다.

clientAuthenticationScheme Class

인증처리용 Filter 설정

Security설정에서 OAuth2ClientAuthenticationProcessingFilter라는 인증처리용 필터를 가져와서 소셜별로 필요한 설정들을 해줍니다. 그리고 마지막에 FilterRegistrationBean에게 소셜 필터리스트를 set해주고 빈으로 등록해 줍니다.FilterRegistrationBean는 SecurityConfig 이외에 다른 곳에 빈으로 등록하여 사용하셔도 무방합니다.(Filter들의 결집을 위해?) SecurityConfig는 여기를 참조하세요.

OAuth2ClientAuthenticationProcessingFilter의 내부를 까보면 attemptAuthentication이라는 오버라이드된 메소드가 있는데 추후 내부로직 진행이 어떻게 흘러가는지 궁금하시다면 이 부분을 기준으로 주요로직들의 흐름을 읽으실 수 있습니다.

인증 후 세션처리

인증이 완료되면 위의 필터의 setAuthenticationSuccessHandler에서 설정한 경로로 리다이렉트됩니다. 저는 AOP를 사용하여 @SocialUser라는 파라미터를 가진 놈들에게 세션에 있는 user 데이터를 바로 반환하거나 인증이 완료된 후 userDetails에 관한 정보를 User 객체에 맵핑하여 db에 저장해 주고 애노테이션에 선언된 user 객체에 바인딩시켜주는 방식을 사용하였습니다. 즉, @SocialUser를 선언한 파라미터는 user에 대한 정보를 가져올 수 있습니다. 이 부분에 대한 자세한 소스는 이곳을 참조하세요.

여기서 포인트컷을 execution(* *(.., @com.social.annotation.SocialUser (*), ..))와 같이 선언하는 것은 모든 반환타입, 모든 메소드에 @SocialUser 애노테이션이 달려 있는 파라미터를 찾는 다는 의미입니다.(자세한 내용은 이곳을 참조해 주세요) 하지만, 이와 같은 방식은 부트구동시 모든 빈을 탐색하기에 runtime이 굉장히 오래 걸립니다.
따라서 위와 같은 방식의 파라미터 애노테이션을 사용한 방식을 구현하고 싶으시다면 HandlerMethodArgumentResolver와 같은 인터페이스를 구현하여 사용하시는게 바람직합니다.

트위터 인증은?

트위터는 어쩔 수 없이 Spring-social-twitter를 사용하여 OAuth1 Spec으로 구현하였습니다. 이 부분은 소스를 직접 참고하세요~

인증 이외에..

redis는 일부러 embbeded redis를 썼습니다. 추후 실서비스에 적용한다면 바꿔야 겠지만..Getting Started 느낌으로 어디서나 구동하기 좋고 따로 설치와 관리가 필요없기에 사용하기 편리하였습니다.
댓글은 1댑스의 대댓글 기능을 만들고자 하였으나 귀차니즘이 발동하여...대댓글 없이 댓글 하나씩만 달수 있도록 하였습니다.

결과

로그인 인증만 테스트해 볼 수 있는 페이지와 댓글기능 사용시 인증할 수 있는 페이지 2개로 구성되어 있습니다. 댓글 제공 방식은 view나 Json으로 데이터를 전송하는 방식을 생각해 보았습니다.

css는 넘나 어렵네요..

참고사이트


댓글26

  • 지나가던 오타잡이 2017.06.27 09:13

    여기서 트위터를 제외한 다른 인증은 모두 OAuth2를 사용합니다. Spirng에는

    오타 Spirng => Spring
    답글

  • 지니가던 오타잡이 2017.06.28 11:55

    다름니다 => 다릅니다.
    답글

  • 돌아온 오타잡이 2017.06.28 13:36

    만드려는 => 만들려는

    참고 : 국립국어원
    https://twitter.com/urimal365/status/222599778679267328?lang=ko

    답글

  • 지켜보던 오타잡이 2017.06.28 13:38

    인코등히여 => 인코딩하여
    답글

  • 익명 2017.06.28 16:36

    비밀댓글입니다
    답글

  • 이한별 2017.07.04 05:27

    /*다들 오타 잡아 주시는 댓글만 있네요. 그 정도로 꼼꼼히 읽었고 도움이 되었단 말인지.. ㅎㅎ 고맙단 말도 같이 써주면 좋았을텐데요. 어쨌든*/
    전 아직 초보이지만 저랑 비슷한 개발환경(springboot, jpa, gradle 등)에서 튜토리얼처럼 공유해주신 것 보니 반갑습니다.
    (오키에서 왔습니다)
    답글

    • Favicon of https://haviyj.tistory.com BlogIcon Havi 2017.07.04 14:44 신고

      오 드디어 오타잡이님들 말고 다른 분이 댓글을 달아주셨군요!
      음 그렇게 좋은 의미였군요ㅋㅋ
      감사합니다ㅎㅎ

  • Favicon of https://silver0r.tistory.com BlogIcon silver0r 2017.07.04 11:34 신고

    혹시 handlebars 와 freemarker를 사용하신 이유를 알 수 있을까요?
    예를들면 thymeleaf, velocity, sitemesh, tiles 등 라이브러리들이 있고
    js쪽에도 underscore template나 기타 여러가지 있는데 사용하신 이유에 대해서 궁금하네요.
    답글

    • Favicon of https://haviyj.tistory.com BlogIcon Havi 2017.07.04 14:49 신고

      아 서버 템플릿팅 엔진은 그저 제가 가장 잘 알고 있어서 사용하였습니다.
      프론트쪽은 underscore도 사용해 봤지만 template기능이 넘 구려서...ㅠㅠ
      결론적으로 제 취향데로 사용했습니다..ㅎㅎ;;
      어떤걸 사용시던간에 직접 사용해 보고 상황에 맞게 더 나은 선택을 하시길 바랍니다!

  • 감사합니다 2017.08.29 22:33

    크흐,, OAuth 처음 접하고 계속 헤매었는데 ,, 덕분에 드디어 잘 작동하네요 ㅠㅠ
    감사인사를 꼭 남겨야겠어서 댓글로 남기고갑니다!
    답글

  • 우왕 ㅎ 2017.09.03 17:17

    우와.. AOP까지.. 좋은 정보 감사드립니다
    질문이 있는데,, User의 authority가 FACEBOOK, KAKAO 두개 인거같은데
    여기서 ROLE_USER, ROLE_ADMIN 이런식으로 권한 부여하려면 어떻게 해야할까유..?

    답글

    • Favicon of https://haviyj.tistory.com BlogIcon Havi 2017.09.05 09:05 신고

      답변이 늦어져서 죄송합니다ㅠㅠ
      요즘 정신이 없어서;;
      보통은 시큐리티컨텍스트에 권한을 set할 수 있습니다.
      깃헙 예제소스 트위터 인증쪽을 보면 힌트가 나와있는데요.
      위의 예시처럼 특정 링크로 들어와서 인증이 되어 다른 권한을 부여해 준다면
      SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(map, "N/A", AuthorityUtils.createAuthorityList("ROLE_USER","ROLE_ADMIN";)));
      이런식으로 부여할 수 있을거 같습니다.

  • ATro 2017.11.07 14:13

    그동안 spring security를 수박 겉핧기 식으로 사용했는데 많은 도움 되었어요.
    꼼꼼한 정보 감사합니다. (__)
    답글

  • MaeMee 2017.12.18 16:48

    소셜 로그인을 적용하는데 많은 도움 받았습니다. 감사합니다.
    질문이 있는데요. 적용하고 보니 facebook 과 google은 아무 문제 없이 잘 되는데 kakao의 UserInfoTokenServices에서 유저 정보를 가져오는 부분에서 401 에러가 나네요. 혹시 이런적 있으셨나요?
    답글

    • Favicon of https://haviyj.tistory.com BlogIcon Havi 2017.12.26 11:53 신고

      안녕하세요, MaeMee님
      혹시나 카카오 api 규격이 바꼈나 다시 실행해 봤는데 저는 잘 가져오더라구요.
      UserInfoTokenServices는 사용자의 개인정보를 가져오는 클래스인데 사용하시는 환경에서 https://kapi.kakao.com/v1/user/me로 직접 리퀘스트를 날려서 테스트해 보시는것도 해결하시는데 도움이 될 것 같습니다. 토큰도 따로 받아서 날려야 하는데...이러한 방식이 번거로우시다면 사용하시는 IDE의 디버거 모드를 사용하시기를 권장해 드립니다.
      읽어주셔서 감사합니다!

    • Mason 2019.05.27 03:46

      안녕하세요 저도 최근에 같은 경험을 겪고 한참을 해매다 원인을 찾아서 공유 드립니다.

      우선 위 예제에 나와있는 부분에보면 페이스북에만 authenticationScheme, clientAuthenticationScheme 프로퍼티가 query, form 으로 적용되어있는데 이부분을 카카오쪽에도 적용해주시면 정상적으로 동작할거 같습니다.

      디버그모드로 계속추적하다보니 기본이 header로 되어있어서 카카오톡 엑세스 토큰 API 호출시 body 파라미터에 client_id 가 누락되어 전송되다 보니 에러가 발생하더라구요.

      카카오톡 api 규격이 중간에 바뀐건지는 모르겠지만 현재 기준으로는 카카오톡쪽에선는 body parameter 에 존재하는 client id 를 체크하더라구요.

      도움이 되셨으면 좋겠습니다.

  • bruce oh 2018.07.06 11:16

    안녕하세요 잘 보고 있습니다! 다름이 아니라 이거를 gradle project로 개발시에는 application.yml을 어떻게 대체 할 수 있나요?
    답글

    • Favicon of https://haviyj.tistory.com BlogIcon Havi 2018.07.24 22:53 신고

      무슨 말씀이신지 몰르겠네요ㅠㅠ
      현재도 gradle project 환경에서 application.yml로 동작하고 있습니다.
      깃에서 직접 소스를 보시고 추가적인 궁금증이 있으시다면 다시 댓글 달아주세요!
      감사합니다.

  • 쪼랩. 2018.11.19 14:26

    잘 몰라서 그렇습니다만.. 세션을 레디스에 담는 이유가 있는건가요?? 그냥 session에 들고 있으면 안되는건가요??
    spring security를 아직 접해보지 않아서 여쭤봅니다.
    답글

    • Favicon of https://haviyj.tistory.com BlogIcon Havi 2019.06.14 18:06 신고

      여러개의 서버에서 세션을 사용하기 위해서는 공통된 저장소에서 세션을 관리해야 됩니다. 그래서 레디스를 사용하는 예제로 꾸민것입니다.