Spring Bootで「Twitterでログイン」を実装する

Spring

WebサービスではおなじみのTwitterでログインする機能。Spring Bootで作ったアプリケーションでもどうやら実装できるようなのでちょっと試してみました。さすがはSpring、OAuthのトークンを取ってきて云々を実装しなくて良いように仕組みが用意されているようです。

前提バージョン

この記事で使用した各種ライブラリのバージョンは以下の通り。

  • Spring Boot:2.0.4.RELEASE
  • kotlin:1.2.70
  • Java:1.8.0_181
  • Gradle:4.10.1
  • Spring Boot Social Twitter Starter:1.5.16.RELEASE
  • Spring Social Security:1.1.6.RELEASE

やりたいこと

WebサービスでよくあるTwitterでログインを実装し、Spring Securityで実装した通常のフォームログインの仕組みと連携させるイメージ。フォームログインが実装されていることを前提とし、Twitterでログインリンクを踏むとTwitterアカウントの情報を元にフォームログイン用のユーザが作成されるようにしておいて、認可制御などは通常のログインユーザと同じ仕組みに乗っかるような形。

必要な実装・設定作業

以下の実装および作業を行う必要があります。

  • 依存関係の設定
  • Spring Securityによるフォームログインの実装
  • Twitterアプリケーションの作成
  • Socialログインの実装
    • テーブル作成
    • SocialUserDetailsの実装クラス
    • SocialUserDetailsServiceの実装クラス
    • ConnectionSignUpの実装クラス
    • SocialConfigurerAdapterの実装クラス
    • SpringSocialConfigurerの適用
    • ログインリンクの実装

依存関係の設定

build.gradleに依存関係を設定します。

dependencies {
    // 省略
    compile('org.springframework.boot:spring-boot-starter-jdbc')
    compile('org.springframework.boot:spring-boot-starter-security')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-social-twitter:1.5.16.RELEASE')
    compile('org.springframework.social:spring-social-security:1.1.6.RELEASE')
    // 省略
}

その他必要なものを追加してください。

Spring Securityによるフォームログインの実装

基本的なフォームログインの実装なので説明は簡易なものにしますが、今回検証した際は認証に使用するユーザ名としてはメールアドレスを使用し、ログイン後の画面に表示するユーザ名をオプション項目として指定可能にしました。

UserDetailsの実装クラスのデフォルト実装として提供されてorg.springframework.security.core.userdetails.Userを拡張し、表示名をユーザに紐づく情報として追加しています。

class CustomUser(
    username: String,
    password: String,
    enabled: Boolean,
    accountNonExpired: Boolean,
    credentialsNonExpired: Boolean,
    accountNonLocked: Boolean,
    authorities: MutableCollection,
    val displayName: String
) : User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities)

また、RDB上でユーザを管理する際のUserDetailsManagerの実装として提供されてJdbcUserDetailsManagerを拡張します。

@Service
@Transactional
class CustomJdbcUserDetailsManager(dataSource: DataSource) : JdbcUserDetailsManager(dataSource) {

    @Autowired
    lateinit var cunstomUserMapper: CustomUserMapper

    override fun createUser(user: UserDetails) {
        val customUser = user as CustomUser
        customUserMapper.createUser(User(customUser.username, customUser.password, customUser.isEnabled, customUser.displayName))
        customUserMapper.createAuthority(user.username, user.authorities.joinToString(separator = ","))
    }

    override fun loadUserByUsername(username: String): UserDetails {
        val users = customUserMapper.findUsers(username)
        val authority = customUserMapper.findAuthority(username)
        if (users == null || authority == null) throw UsernameNotFoundException("ユーザが見つかりませんでした。")
        return CustomUser(username, users.password, users.enabled, true, true, true, mutableListOf(SimpleGrantedAuthority(authority)), users.displayName)
    }
}

ユーザデータを保持するdataクラスやCRUDを行うmapperクラスなどは適宜用意してください。

後はWebSecurityConfigurerAdapterの実装クラスでコンフィグします。

@Configuration
class WebSecurityConfiguration(val userDetailsService: CustomJdbcUserDetailsManager, val passwordEncoder: PasswordEncoder) : WebSecurityConfigurerAdapter() {

    override fun configure(web: WebSecurity) {
        // 省略
    }

    override fun configure(http: HttpSecurity) {
        // 省略
    }

    override fun configure(auth: AuthenticationManagerBuilder) {
        val daoAuthenticationProvider = DaoAuthenticationProvider()
        daoAuthenticationProvider.setUserDetailsService(userDetailsService)
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder)
        auth.authenticationProvider(daoAuthenticationProvider)
    }
}

ログインパスの設定等は説明を省略しますが、このクラスで設定します。

Twitterアプリケーションの作成

Twitter Application Management
Application registration & configuration for developers using the Twitter REST & Streaming APIs.

こちらからTwitterアプリケーションを作成し、Consumer KeyとConsumer Secretを取得します。注意点としてはこんなところでしょうか(2018/09/30時点)。

  • 電話番号登録済みのTwitterアカウントが必要
  • アプリケーションの説明を英文で入力する必要がある
  • Callback URLに妥当なURLを入力する必要がある

ローカル環境での動作を想定してCallback URLは複数登録しておくのが良いです。

  • https://yourdomain/auth/twitter
  • http://127.0.0.1:8080/auth/twitter

といった感じ。/auth/twitterの部分はSpring SocialでTwitterのOAuth認証を行う際のエントリポイントです。

取得したConsumer KeyとConsumer Secretを使用してTwitterのAPIをコールします。

Socialログインの実装

Spring Securityで実装したフォームログインと連携する形でSocialログインを実装します。

テーブル作成

RDBでユーザ管理を行う場合に必要なSpring Socialが使用するテーブルを作成します。ベースとなるDDLはSpring Socialが提供していますので、必要があればカラム長などを修正してテーブル作成を行いましょう。

spring-projects/spring-social
Allows you to connect your applications with SaaS providers such as Facebook and Twitter. - spring-projects/spring-social

SocialUserDetailsの実装クラス

Socialログイン時UserDetailsであるSocialUserDetailsの実装クラスを作成します。org.springframework.security.core.userdetails.Userと同様にSocialUserという実装が提供されていますが、フォームログインと同様に表示名を追加したいので拡張します。

class CustomSocialUser(
    username: String,
    password: String,
    enabled: Boolean,
    accountNonExpired: Boolean,
    credentialsNonExpired: Boolean,
    accountNonLocked: Boolean,
    authorities: MutableCollection,
    val displayName: String
) : SocialUser(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities)

SocialUserDetailsServiceの実装クラス

SocialUserDetailsServiceの実装クラスをBean定義しておくことで、Socialログイン時のUserDetailsServiceとして利用できます。

@Service
@Transactional
class SocialUserDetailsServiceImpl() : SocialUserDetailsService {

    @Autowired
    lateinit var customUserMapper: CustomUserMapper

    override fun loadUserByUserId(username: String): SocialUserDetails {
        val users = customUserMapper.findUsers(username)
        val authority = customUserMapper.findAuthority(username)
        return CustomSocialUser(username, users.password, users.enabled, true, true, true, mutableListOf(SimpleGrantedAuthority(authority)), users.displayName)
    }
}

mapperクラスは適宜用意してください。ここでは、loadUserByUserIdの実装としてフォームログインで使用するユーザ情報を管理するテーブルからユーザ情報を取得し、拡張したSocialUserDetailsを返却させています。

ConnectionSignUpの実装クラス

ConnectionSignUpの実装クラスをBean定義することで、Socialログイン時にフォームログインに使用するユーザデータを管理するテーブルに自動的にユーザを追加することができます。

@Component
@Transactional
class ConnectionSignUpImpl() : ConnectionSignUp {

    @Autowired
    lateinit var customUserMapper: CustomUserMapper

    @Autowired
    lateinit var customJdbcUserDetailsManager: CustomJdbcUserDetailsManager

    override fun execute(connection: Connection<*>): String {
        val profile = connection.fetchUserProfile()
        var username = ""
        var usernameDecided = false

        while (!usernameDecided) {
            username = RandomStringUtils.randomAlphanumeric(255)
            val users = customUserMapper.findUsers(username)
            if (users == null) usernameDecided = true
        }

        val customUser = CustomUser(username, RandomStringUtils.randomAlphanumeric(255), true, true, true, true, mutableListOf(SimpleGrantedAuthority("USER")), profile.username)
        customJdbcUserDetailsManager.createUser(customUser)

        return username
    }
}

executeメソッドをoverrideすることで、Socialログイン時のユーザ追加を実装できます。ここではユーザ名をランダムに生成し、重複していないことを確認して設定しています。また、表示名としてTwitterのprofileからユーザ名を取得して設定しています。

SocialConfigurerAdapterの実装クラス

ここまで実装してきた各種実装が有効となるようにSocialConfigurerAdapterの実装クラスを作成し、かつBean定義を有効にします。

@Configuration
@EnableSocial
@ConfigurationProperties("app.social.twitter")
class SocialConfiguration() : SocialConfigurerAdapter() {

    @Autowired
    lateinit var dataSource: DataSource

    @Autowired
    lateinit var connectionSignUpImpl: ConnectionSignUpImpl

    @Autowired
    @Qualifier("queryableTextEncryptor")
    lateinit var textEncryptor: TextEncryptor

    lateinit var appId: String
    lateinit var appSecret: String

    override fun addConnectionFactories(connectionFactoryConfigurer: ConnectionFactoryConfigurer, environment: Environment) {
        connectionFactoryConfigurer.addConnectionFactory(TwitterConnectionFactory(appId, appSecret))
    }

    override fun getUsersConnectionRepository(connectionFactoryLocator: ConnectionFactoryLocator): UsersConnectionRepository {
        var repository = JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, textEncryptor)
        repository.setConnectionSignUp(connectionSignUpImpl)
        return repository
    }

    override fun getUserIdSource(): UserIdSource {
        return AuthenticationNameUserIdSource()
    }
}

SocialConfigurerAdapterの実装クラスをBean定義しておくことで、Socialログインに必要なコンフィグが行われます。また、@EnableSocialを付与することで、Socialログインに必要なorg.springframework.social.config.annotation.SocialConfigurationによるBean定義も有効になります。

addConnectionFactoriesメソッドでは、Twitterへのアクセスを行うTwitterConnectionFactoryを追加しています。ここで使用しているTwitterConsumer KeyとConsumer Secretについては環境変数に設定したものをプロパティから取得しています。

app:
  social:
    twitter:
      appId: ${SPRING_SOCIAL_TWITTER_APP_ID}
      appSecret: ${SPRING_SOCIAL_TWITTER_APP_SECRET}
Consumer KeyとConsumer Secretはリポジトリにコミットして公開されないようにしておきましょう。

SpringSocialConfigurerの適用

SpringSocialConfigurerをSpring Secutiryのコンフィグに設定し、適用します。

@Configuration
class WebSecurityConfiguration(val userDetailsService: CustomJdbcUserDetailsManager, val dataSource: DataSource, val passwordEncoder: PasswordEncoder) : WebSecurityConfigurerAdapter() {
    // 省略
    override fun configure(http: HttpSecurity) {
        http.
            // 省略
            .and().apply(SpringSocialConfigurer().postLoginUrl("/web/mypage").connectionAddedRedirectUrl("/web/mypage").defaultFailureUrl("/web/login?error=twitter"))
    }
}

WebSecurityConfigurerAdapterを継承したクラスでのconfigureメソッドで設定を行う際、SpringSocialConfigurerを用いて設定を行います。ここでは、ログイン後のURL等を設定しています。これでSocialAuthenticationFilterによるSocialログインが有効になります。

ログインリンクの実装

SocialAuthenticationFilterによるSocialログインでは、ログインを行うパスは/auth/{providerid}になります。twitterの場合のprovideridはtwitterですね。

公式リファレンスの通りリンクを貼ればSocialログインを行う事ができ、ログインが成功すれば設定したログイン後のパスに遷移します。

<!-- TWITTER SIGNIN -->
<p><a th:href="@{/auth/twitter}"><img th:src="@{/resources/social/twitter/sign-in-with-twitter-d.png}"/></a></p>
Spring Social Reference

例ではthymleafが使われていますが、リンクが張られればOKです。

まとめ

Twitterを例にSpring BootアプリケーションでのSpring Socialを利用したログイン処理を試してみました。Twitterだけでなく、メインのプロジェクトとしてはFaceookとLinkedInがサポートされ、今後の予定はコミュニティプロジェクトを含むと色々なSNSが利用できるようです。

私の場合はTwitterでのログインを最も使ってみたかった・・・というか他のSNSはあんまり・・・という感じだったのでTwitterを試してみました。

今回は諸事情によりサンプルを上げていませんが・・・何かの参考になれば幸いです。

この記事に対するコメント