Skip to content

Ogata-Kazuyoshi/line-login-spring-security

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ソーシャルログイン@SpringSecurity

目次

環境セットアップ

1. 必要ライブラリーのインポート
    implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
	implementation("org.springframework.boot:spring-boot-starter-security")
2. 各プロバイダでセットアップしたAppにリダイレクトURIを設定

ログインの流れ

OAuth2/ OIDCの認証では大きな流れとして下記が実施される(LINEの場合)
  • ログインの流れ
  1. フロントエンド→バックエンドのログインエンドポイントへHttpリクエスト。/oauth2/authorization/{registrationId}がSpringSecurityがログイン認証を始める最初のエンドポイント
  2. SpringSecurity→各プロバイダの認証エンドポイントを叩く。上記の{registrationId}にともなって、各resistrationに登録されている名前と一致するproviderのauthorizationUriを叩く。LINEの場合は、https://access.line.me/oauth2/v2.1/authorizeに必要なQueryをSpringSecurityが勝手につけてリクエスト送付
  3. ユーザーが認証処理をすると、各プロバイダ→Spring Securityに必要なクエリをつけた状態(codeなど)で、登録されているredirectUriにリダイレクトされる
  4. Spring Security→各プロバイダに必要な情報(codeなど)を付与してアクセストークン発行の依頼(postメソッド)。tokenUri: https://api.line.me/oauth2/v2.1/token
  5. 各プロバイダ→SpringSecurityにアクセストークンなどを含んだ情報をredirect_uriへresponseする。
  6. SpringSecurity→各プロバイダへアクセストークンを使用して、ユーザーの認証情報を取得依頼。userInfoUri: https://api.line.me/v2/profile
  7. 各プロバイダ→SpringSecurityへ上記の情報をredirect_uriへresponseする。
  8. 上記まで問題なければ、各SuccessHandlerが実行される(SuccessHandlerに入るタイミングでは2〜7が全て終わっていて、ユーザー情報はすでに持っている!!)
  9. SuccessHandlerの最後にフロントエンドへredirectする。この時、SpringSecurityが「JSESSIONID」を作成してブラウザが保持する。これによってセッション管理されてUserの認証プロセスを完了する
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            clientId: ${GITHUB_CLIENT_ID}
            clientSecret: ${GITHUB_SECRET_ID}
            scope: read:user
          line:
            clientId: ${LINE_CLIENT_ID}
            clientSecret: ${LINE_SECRET_ID}
            authorizationGrantType: authorization_code
            redirectUri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: profile
        provider:
          line:
            authorizationUri: https://access.line.me/oauth2/v2.1/authorize
            tokenUri: https://api.line.me/oauth2/v2.1/token
            userInfoUri: https://api.line.me/v2/profile
            userNameAttribute: userId
#githubは共通のOAuthプロバイダとして、SpringBootが認識するので、登録不要
#          github:
#            authorization-uri: https://github.com/login/oauth/authorize
#            token-uri: https://github.com/login/oauth/access_token
#            user-info-uri: https://api.github.com/user


#Frontendから叩く場合は、/oauth2/authorization/{registrationId}がバックエンドの認証フローをスタートさせるトリガー

コードの説明

1. application.yml
  • resistrationIdで各プロバイダを区別する
  • 複数のプロバイダを使用する場合は、1つにredirectUriを定義すれば良い。最後に{resistrationId}とすることで、それぞれのプロバイダ用のリダイレクトUriが内部的に定義される
  • scopeは各プロバイダに寄るので調べること
  • フロントエンドから叩く際は、/oauth2/authorization/{registrationId}を叩く。各プロバイダでパスによって切り替えられる
  • 共通OAuthプロバイダの場合(Github、Google、Facebookなど)は、SpringSecurityに既にプロバイダ情報はあるため、authorizationUriとかは特に定義しなくても問題ない。
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            clientId: ${GITHUB_CLIENT_ID}
            clientSecret: ${GITHUB_SECRET_ID}
            scope: read:user
          line:
            clientId: ${LINE_CLIENT_ID}
            clientSecret: ${LINE_SECRET_ID}
            authorizationGrantType: authorization_code
            redirectUri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: profile
        provider:
          line:
            authorizationUri: https://access.line.me/oauth2/v2.1/authorize
            tokenUri: https://api.line.me/oauth2/v2.1/token
            userInfoUri: https://api.line.me/v2/profile
            userNameAttribute: userId
#githubは共通のOAuthプロバイダとして、SpringBootが認識するので、登録不要
#          github:
#            authorization-uri: https://github.com/login/oauth/authorize
#            token-uri: https://github.com/login/oauth/access_token
#            user-info-uri: https://api.github.com/user


#Frontendから叩く場合は、/oauth2/authorization/{registrationId}がバックエンドの認証フローをスタートさせるトリガー
2. SecurityConfiguration.kt
  • 複数のプロバイダを切り替えれるように、各プロバイダ用のSuccessHandlerをBean登録しておく
  • .csrfでポスト処理などの403エラーを回避できる(ただ、ざるになるので本当はだめ)
  • requestMatchers().authentificated()で登録されているエンドポイントは認証されているかのチェックが入る。@AuthenticationPrincipalを使ってUser情報を取りたい場合は、このエンドポイント内にないとだめ
package com.example.backend.auth.config

import com.example.backend.auth.handler.common.AppCustomeAuthenticationSuccessHandler
import com.example.backend.auth.handler.provider.GithubAuthenticationSuccessHandler
import com.example.backend.auth.handler.provider.LineAuthenticationSuccessHandler
import com.example.backend.service.UserService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.AuthenticationSuccessHandler

@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@Configuration
class SecurityConfiguration (
  val userService: UserService
) {
  @Bean
  fun authenticationSuccessHandler(): AuthenticationSuccessHandler {
    return AppCustomeAuthenticationSuccessHandler(
      listOf(
        LineAuthenticationSuccessHandler(userService),
        GithubAuthenticationSuccessHandler(userService)
      )
    )
  }

  @Bean
  fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http
      .csrf().disable()
      .authorizeHttpRequests {
        it.requestMatchers("/api/**")
          .authenticated()
        it.anyRequest()
          .permitAll()
      }
      .oauth2Login {
        it.successHandler(authenticationSuccessHandler())
      }
    return http.build()
  }
}
3. CommonAuthenticationSuccessHandler.kt
  • 各プロバイダーによらず、共通で処理したい部分を記載する。各プロバイダのクラスに継承をして欲しいので、"abstract"の抽象クラスにしている
  • 認証成功時に、どのサクセスハンドラーを使用するかを決定するために、各サクセスハンドラーのsupportsメソッドをAppAuthentication SuccessHandlerが呼ぶ。lineやgithubを決める
  • getOidやgetDisplayNameなど各プロバイダごとでアクセスするキーが変わるものは abstract関数にしておいて、継承先でのoverrideを強制する
  • 認証プロセス終了時に自動で、①supporsメソッド→②onAuthenticationSuccessが呼ばれる
  • 「SecurityContextHolder.getContext().authentication = newAuthentication」の行で、認証後のuser情報を登録して、@AuthenticationPrincipalでアクセスできるようになる。
package com.example.backend.auth.handler.common

import com.example.backend.auth.model.CustomOAuth2User
import com.example.backend.service.UserService
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
import org.springframework.security.oauth2.core.user.OAuth2User

abstract class CommonAuthenticationSuccessHandler(
  private val userService: UserService,
  private val clientRegistrationId: String
) : AppAuthenticationSuccessHandler {

  override fun supports(oauth2Authentication: OAuth2AuthenticationToken): Boolean {
    return clientRegistrationId == oauth2Authentication.authorizedClientRegistrationId
  }

  override fun onAuthenticationSuccess(
    request: HttpServletRequest?,
    response: HttpServletResponse,
    authentication: Authentication
  ) {
    val principal = authentication.principal as OAuth2User
    val oAuth2AuthenticationToken = authentication as OAuth2AuthenticationToken
    val oid = getOid(principal)
    val displayName = getDisplayName(principal)
    val res = userService.getOrCreateUserService(oid = oid, name = displayName)
    val newAuthentication = OAuth2AuthenticationToken(
      CustomOAuth2User(
        userId = res.id.toString(),
        oid = oid,
        name = displayName,
        authorities = principal.authorities,
      ),
      authentication.authorities,
      oAuth2AuthenticationToken.authorizedClientRegistrationId
    )
    SecurityContextHolder.getContext().authentication = newAuthentication
    val redirectUrl = System.getenv("AFTER_AUTH_REDIRECT_URL") ?: "hogehoge"
    response.sendRedirect(redirectUrl)
  }

  abstract fun getOid(principal: OAuth2User): String
  abstract fun getDisplayName(principal: OAuth2User): String
}
4. LineAuthenticationSuccessHandler.kt
  • 共通部分を持てるように、CommonAuthenticationSuccessHandlerクラスを継承する
  • getOidや、getDisplanNameなど各プロバイダでアクセスするキーが異なる部分をこのクラスが担う。
package com.example.backend.auth.handler.provider

import com.example.backend.auth.handler.common.CommonAuthenticationSuccessHandler
import com.example.backend.service.UserService
import org.springframework.security.oauth2.core.user.OAuth2User

class LineAuthenticationSuccessHandler(userService: UserService) : CommonAuthenticationSuccessHandler(userService, "line") {
  override fun getOid(principal: OAuth2User): String = principal.getAttribute<String>("userId") ?: throw Exception("There is no userId")
  override fun getDisplayName(principal: OAuth2User): String = principal.getAttribute<String>("displayName") ?: throw Exception("There is no name")
}
5. CustomOAuth2User.kt
  • attributesのところで、どのキーをPrincipalとして登録するかを定義する。増やしたい場合は増やせる
package com.example.backend.auth.model

import org.springframework.security.core.AuthenticatedPrincipal
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.oauth2.core.user.OAuth2User
import java.io.Serializable
import java.util.UUID

class CustomOAuth2User(
  private val authorities: Collection<GrantedAuthority>,
  private val userId: String,
  private val oid: String,
  private val name: String,
) : OAuth2User {
  private val attributes: Map<String, Any> = mapOf("userId" to userId, "oid" to oid, "name" to name)

  override fun getName(): String {
    return name
  }

  override fun getAttributes(): Map<String, Any> {
    return attributes
  }

  override fun getAuthorities(): Collection<GrantedAuthority> {
    return authorities
  }
}
6. AuthController.kt
  • 各メソッドの引数で@AuthenticationPrincipalをつけると、認証されたuser情報が取れる
  • user情報へのアクセス方法は user.getAttribute("取りたいキー")でGetできる
package com.example.backend.controller

import com.example.backend.auth.model.CustomOAuth2User
import com.example.backend.model.response.ResponceUserInfo
import com.example.backend.service.UserService
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/auth")
class AuthController (
  val userService: UserService
) {
  @GetMapping("/check-auth")
  fun checkAuth (
    @AuthenticationPrincipal user: CustomOAuth2User,
  ) {
    println("userId : " + user.getAttribute("oid"))
  }

}

ChatGPT

1. 起動時に読み込まれるアノテーションに関して
2. application.ymlの設定に関して
3. 認証プロセスに関して
4. SuccessHandlerの切り替えについて
5. mockkのrelaxed = true について
6. slot / capture でメソッド呼び出しで渡された引数をキャプチャーする

参考

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published