Spring BootでSNSログイン

##概要
Spring Bootで作るwebアプリケーションにSNSログイン機能を実装した際のメモです。

##環境
Java 8(ごめんなさい)
Spring Boot 2.1.0.RELEASE

###Spring SocialはEOL
https://projects.spring.io/spring-social/
https://spring.io/blog/2018/07/03/spring-social-end-of-life-announcement

##使用ライブラリ
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-oauth2-client

##実装対象SNS
- Facebook
- Google

##実装
①POM

```xml:pom.xml
<!-- OAuth2 authentication-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
```

②configuration

```java:SecurityConfig

// ログインに関わる部分のみ抜粋しています。
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @NonNull
    SaveAndGenerateUserDetails saveAndGenerateUserDetails

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // SNSログイン
                .oauth2Login()
                    .loginPage("/login")
                    .userInfoEndpoint()
                    // OAuth認証時に実行するServiceクラス
                    .userService(new OAuth2UserService(saveAndGenerateUserDetails))
                    // OpenId認証時に実行するServiceクラス
                    .oidcUserService(new OidcUserDetailsService(saveAndGenerateUserDetails))
                .and()
                    // 必要があればSuccessHandlerを実装
                    .successHandler(new MyAuthenticationSuccessHandler())
                    // 必要があればSuccessHandlerを実装。今回はデフォルト。
                    .failureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error"))
            .and()
                // ID PASSログインを共存できる
                .formLogin()
                    .loginPage("/login")
                        .usernameParameter("username")
                        .passwordParameter("password")
                        .permitAll()
                    .successHandler(new FormLoginSuccessHandler())
                    .failureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error"))
            .and()
                .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                    .logoutSuccessUrl("/")
                    .invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID")
                    .permitAll()
            .and()
                .exceptionHandling();


}
```


③application.yml

```yml:application.yml
spring:
  security:
    oauth2:
      client:
        registration:
          facebook:
            client-id: 【client-id】
            client-secret: 【client-secret】
            scope:
              - email
              - public_profile
            redirect-uri: 【アプリケーションのURL】/login/oauth2/code/{registrationId}
          google:
            client-id: 【client-id】
            client-secret: 【client-secret】
            scope:
              - email
              - profile
              - openid
            redirect-uri: 【アプリケーションのURL】/login/oauth2/code/{registrationId}
        provider:
          facebook:
            authorizationUri: https://www.facebook.com/v3.3/dialog/oauth
            tokenUri: https://graph.facebook.com/v3.3/oauth/access_token
            userInfoUri: https://graph.facebook.com/v3.3/me

```

④view

```html:login.html
<!-- 各SNSログインのエンドポイントを設定したリンクをつくります。 
     エンドポイントは固定値で下記の通りです。 -->
<!-- 〜もろもろ省略〜 -->
<a href="/oauth2/authorization/facebook">Facebookログイン</a>
<a href="/oauth2/authorization/google">Googleログイン</a>
<!-- 〜もろもろ省略〜 -->
```

⑤Service
⑤-1　OAuth用クラス（SecurityConfigの.userService()に設定）

```java:OAuth2UserService.java
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OAuth2UserService
        extends DefaultOAuth2UserService {

    @NonNull
    SaveAndGenerateUserDetails saveAndGenerateUserDetails

    @Override
    @Transactional(readOnly = false)
    public OAuth2User loadUser(OAuth2UserRequest userRequest)
            throws OAuth2AuthenticationException {
        
        // 実装クラスの紹介は省略します。
        // 概略:
        // registrationIdで分岐して、取得したい情報を取り出す処理をSNSごとにクラスを作成。
        // 取得した情報を元にUser情報をDBに保存
        // 取得した情報を元に認証情報(UserDetails)を返却します。
        // 本クラスの戻り値の型は認証方法によって異なりますが、processメソッドの戻り値の型は、MyUserDetails
        // なのでここで認証方法の差を吸収します。
        return saveAndGenerateUserDetails.process(userRequest,
                super.loadUser(userRequest));
    }
}
```

⑤-2　OpenId認証後の処理（SecurityConfigの.oidcUserService()に設定）

```java:OidcUserDetailsService.java
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OidcUserDetailsService
        extends OidcUserService {

    @NonNull
    SaveAndGenerateUserDetails saveAndGenerateUserDetails;

    @Override
    @Transactional(readOnly = false)
    public OidcUser loadUser(OidcUserRequest userRequest)
            throws OAuth2AuthenticationException {
        
        // ⑤-1と同様
        return saveAndGenerateUserDetails.process(userRequest,
                super.loadUser(userRequest));
    }
}
```

⑥MyUserDetailsクラス

```java:MyUserDetails.java
@SuppressWarnings("serial")
@Data
public class MyUserDetails implements UserDetails, OAuth2User, OidcUser {

    // @Overrideすべきメソッドはよしなに。
    // SNSログインに必要な部分のみ抜粋しています。

    private Map<String, Object> attributes;

    // コンストラクタ
    public MyUserDetails(MyUser user, Map<String, Object> attr) {
        // OAuth、OpenIdの情報のみ記載しています。
        this.attributes = attr;
    }
}

```

##感想
・思ったより自前で用意するクラスは少なくて済んだ
・その分ライブラリ側でよしなにやってくれるのは助かるけどブラックボックスだった
・oauthとopenidを一種類ずつ実装してしまえば容易に横展開可能

##参考URL
- https://www.callicoder.com/spring-boot-security-oauth2-social-login-part-1/
- https://www.callicoder.com/spring-boot-security-oauth2-social-login-part-2/
- https://www.baeldung.com/spring-security-5-oauth2-login
- https://www.codeflow.site/ja/article/spring-security-5-oauth2-login