social login 서비스를 구현해보자

이번에는 좀 재미있는것을 해보자.

카카오로그인, 페이스북 로그인 같은 소셜 로그인을 구현 할것이다.

facebook 로그인 / github 로그인

사전작업으로 https://developers.facebook.com/ 에 가서 새로운 앱을 만든 후, client id 와 client secret 를 받아두자.

로그인을 할것이니, 로그인 설정을 하는것도 잊지말자.

먼저 의존성을 다운받자

pom.xml

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.4.RELEASE</version>
</dependency>

그 다음 Application 전역에 @EnableOAuth2Sso 어노테이션을 달아주자.

다음으로 application.yml 파일을 resources 아래 만들어서 다음과 같은 내용을 넣어주자

facebook:
  client:
    clientId: {my client id}
    clientSecret: {my 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

my client id 와 my client secret 항목에서 facebook 에서 발급받은 client id 와 client secret 를 넣어주면 된다.

그리고 WebSecurityConfig 를 다음과 같이 수정한다.

@Configuration
@EnableWebSecurity
@EnableOAuth2Client // new
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //...

    @Autowired
    private OAuth2ClientContext auth2ClientContext;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/private/**").hasAnyRole("USER")
                .antMatchers("/admin/**").hasAnyRole("ADMIN")
                .anyRequest().permitAll()
        .and()
            .formLogin()
            .loginPage("/login")
            .usernameParameter("username")
            .passwordParameter("password")
            .failureUrl("/login?error=true")
        .and()
            .logout()
            .deleteCookies("JSESSIONID")
            .clearAuthentication(true)
            .invalidateHttpSession(true)
        .and()
            .exceptionHandling()
                // .accessDeniedPage("/access_denied")
                .accessDeniedHandler(customAccessDeniedHandler)
        .and() // new
            .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class) // new
        ;
    }

    // new
    private Filter ssoFilter() {
        OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
        OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), auth2ClientContext);
        facebookFilter.setRestTemplate(facebookTemplate);
        UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());
        tokenServices.setRestTemplate(facebookTemplate);
        facebookFilter.setTokenServices(tokenServices);
        return facebookFilter;
    }

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

    // new
    @Bean
    @ConfigurationProperties("facebook.resource")
    public ResourceServerProperties facebookResource() {
        return new ResourceServerProperties();
    }

    // new
    @Bean
    public OAuth2ClientContext auth2ClientContext() {
        return new DefaultOAuth2ClientContext();
    }

    // new
    @Bean
    public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
        FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
        registration.setFilter(filter);
        registration.setOrder(-100);
        return registration;
    }
}

new 라고 주석처리 한 항목을 수정 하면 된다.

이제 login.html 에 다음과 같이 넣어주겠다.

login.html

<fieldset>
  <legend>로그인</legend>
  <form th:action="@{/login}" th:method="post">
    <input type="text" name="username" placeholder="아이디를 입력 해 주세요." />
    <br />
    <input type="password" name="password" placeholder="비밀번호를 입력 해 주세요." />
    <br />
    <button type="submit">로그인</button>
  </form>
  <div>
    <a th:href="@{/login/facebook}">페이스북 로그인</a>
    <!-- 이부분을 추가하자. -->
  </div>
</fieldset>

그리고 현재 유저 정보를 볼 수 있도록, /private 에 현재 유저 id 를 가지고 올 수 있도록 하자

SecurityController.java

@Controller
public class SecurityController {

    @GetMapping(value = "/private")
    public String privatePage(Principal principal, Model model) {
        model.addAttribute("user", principal);
        return "private";
    }
}

private.html

<html
  xmlns:th="http://www.thymeleaf.org"
  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
  layout:decorator="layout/layout"
>
  <th:block layout:fragment="contents">
    <h1>비공개 페이지</h1>
    <p th:text="${user.name}"></p>
  </th:block>
</html>

그럼 비공개 페이지 밑에 자기 자신의 아이디가 뜨는 것을 알 수 있다.

Github 로그인

이제 깃헙 로그인을 추가 해보자. 당연히 이번에도 github 에서 oauth2 어플리케이션을 추가해야한다.

login.html

<fieldset>
  <legend>로그인</legend>
  <form th:action="@{/login}" th:method="post">
    <input type="text" name="username" placeholder="아이디를 입력 해 주세요." />
    <br />
    <input type="password" name="password" placeholder="비밀번호를 입력 해 주세요." />
    <br />
    <button type="submit">로그인</button>
  </form>
  <div>
    <a th:href="@{/login/facebook}">페이스북 로그인</a>
  </div>
  <div>
    <a th:href="@{/login/github}">깃허브 로그인</a>
  </div>
</fieldset>

application.yml

github:
  client:
    clientId: {my client id}
    clientSecret: {my client secret}
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user

WebSecurityConfig.java

@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // ...
    private Filter ssoFilter() {
        CompositeFilter filter = new CompositeFilter();
        List<Filter> filters = new ArrayList<>();

        OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
        OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), auth2ClientContext);
        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(), auth2ClientContext);
        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();
    }

    //...
}

ssoFilter 쪽만 변경 하면 된다.

이전에 facebook 로그인을 구현해놔서 복잡하진 않을것이다.

facebook 과 github 쪽에 filter 가 비슷하게 생겼다. 메뉴얼에 따라 리펙토링을 해보자.

먼저 ClientResources 를 만든다.

class ClientResources {

    @NestedConfigurationProperty
    private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();

    @NestedConfigurationProperty
    private ResourceServerProperties resource = new ResourceServerProperties();

    public AuthorizationCodeResourceDetails getClient() {
        return client;
    }

    public ResourceServerProperties getResource() {
        return resource;
    }
}

다음으로, WebSecurityConfig 쪽을 수정하자

WebsecurityConfig.java

@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

    private Filter ssoFilter(ClientResources client, String path) {
        OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
        OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), auth2ClientContext);
        filter.setRestTemplate(template);
        UserInfoTokenServices tokenServices = new UserInfoTokenServices(
                client.getResource().getUserInfoUri(), client.getClient().getClientId());
        tokenServices.setRestTemplate(template);
        filter.setTokenServices(tokenServices);
        return filter;
    }

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

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

이 예제는 https://spring.io/guides/tutorials/spring-boot-oauth2/#_social_login_github 예제 이다.

이제 페이스북 로그인 / 깃허브 로그인을 번갈아가며 해보자.

카카오 로그인 추가하기

예제만 따라해서는 의미가 없다. 응용해서 카카오 로그인을 구현해보자.

카카오 개발자센터 에서 키 발급받는거 있지 말자

이제 차근차근 응용해보자.

먼저 WebSecurityConfig 를 바꿔보자

@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private Filter ssoFilter() {
        CompositeFilter filter = new CompositeFilter();
        List<Filter> filters = new ArrayList<>();
        filters.add(ssoFilter(facebook(), "/login/facebook"));
        filters.add(ssoFilter(github(), "/login/github"));
        filters.add(ssoFilter(kakao(), "/login/kakao")); // new
        filter.setFilters(filters);
        return filter;
    }

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

다음으로 application.yml 에 카카오 정보를 입력 해 보자.

kakao:
  client:
    clientId: {kakao rest api key}
    clientSecret: {kakao secret key}
    accessTokenUri: https://kauth.kakao.com/oauth/token
    userAuthorizationUri: https://kauth.kakao.com/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://kapi.kakao.com/v2/user/me

각프로퍼티들은 문서에 있는 내용들이다.

이제 로그인 페이지에 카카오 로그인을 추가하자

login.html

<fieldset>
  <legend>로그인</legend>
  <form th:action="@{/login}" th:method="post">
    <input type="text" name="username" placeholder="아이디를 입력 해 주세요." />
    <br />
    <input type="password" name="password" placeholder="비밀번호를 입력 해 주세요." />
    <br />
    <button type="submit">로그인</button>
  </form>
  <div>
    <a th:href="@{/login/facebook}">페이스북 로그인</a>
  </div>
  <div>
    <a th:href="@{/login/github}">깃허브 로그인</a>
  </div>
  <div>
    <a th:href="@{/login/kakao}">카카오 로그인</a>
  </div>
</fieldset>

너무 쉽게 끝났다. security 는 정말 놀랍지 않은가. 오늘은 여기까지

현재까지의 소스는 https://github.com/ziponia/spring-security-example tag v2 로 따라가자