外部のIdentity Provider(IdP)を利用したソーシャルログインは、ユーザーにとって新規アカウント作成の手間を省き、開発者にとってはパスワード管理のリスクを軽減できる効果的な認証方式です。本記事では、Spring Security 6.4でOAuth2/OpenID Connectを使用し、GoogleとGitHubによるソーシャルログインを実装する方法を解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
Spring Security 6.4.x
ビルドツール Maven または Gradle
IDE VS Code または IntelliJ IDEA

事前に以下の準備を完了してください。

  • Spring Securityの基本概念(認証・認可の違い)の理解
  • Googleアカウント(Google Cloud Console用)
  • GitHubアカウント(GitHub Developer Settings用)

OAuth2とOpenID Connectの基本

ソーシャルログインの実装に入る前に、OAuth2とOpenID Connectの基本概念を理解しておきましょう。

OAuth2とは

OAuth2は、サードパーティアプリケーションがユーザーの資格情報を直接扱うことなく、限定的なアクセス権を取得するための認可フレームワークです。OAuth2自体は「認可」のためのプロトコルであり、「認証」は範囲外です。

OpenID Connect(OIDC)とは

OpenID Connect(OIDC)は、OAuth2の上に構築された認証レイヤーです。OAuth2の認可機能に加えて、ユーザーの身元を確認する認証機能を提供します。GoogleのOAuth2実装はOIDCに準拠しています。

プロトコル 役割 提供する情報
OAuth2 認可 アクセストークン
OpenID Connect 認証 + 認可 IDトークン + アクセストークン

認可コードフローの仕組み

ソーシャルログインで使用される認可コードフロー(Authorization Code Flow)は、以下の手順で進行します。

sequenceDiagram
    participant User as ユーザー
    participant App as アプリケーション
    participant AuthServer as 認可サーバー<br>(Google/GitHub)
    participant Resource as リソースサーバー<br>(UserInfo API)

    User->>App: 1. ログインボタンクリック
    App->>AuthServer: 2. 認可リクエスト(client_id, redirect_uri, scope)
    AuthServer->>User: 3. ログイン画面表示
    User->>AuthServer: 4. 認証情報入力 & 同意
    AuthServer->>App: 5. 認可コード返却(redirect_uriへ)
    App->>AuthServer: 6. アクセストークン要求(認可コード + client_secret)
    AuthServer->>App: 7. アクセストークン + IDトークン発行
    App->>Resource: 8. ユーザー情報取得(アクセストークン)
    Resource->>App: 9. ユーザー情報返却
    App->>User: 10. 認証完了・セッション確立

認可コードフローの特徴は以下のとおりです。

特徴 説明
セキュリティ クライアントシークレットはサーバーサイドでのみ使用
トークン保護 アクセストークンがブラウザに直接露出しない
リフレッシュ対応 リフレッシュトークンによるトークン更新が可能

プロジェクトのセットアップ

依存関係の追加

Spring Boot OAuth2 Clientを使用するには、以下の依存関係を追加します。

Mavenの場合(pom.xml):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

Gradleの場合(build.gradle):

1
2
3
4
5
6
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

spring-boot-starter-oauth2-clientには、OAuth2ログインに必要なすべてのコンポーネントが含まれています。

Google OAuth2クライアントの設定

Google Cloud Consoleでの設定

Google OAuth2を使用するには、Google Cloud Consoleでプロジェクトを作成し、OAuth2クライアントを登録する必要があります。

  1. Google Cloud Console(https://console.cloud.google.com/)にアクセス
  2. 新しいプロジェクトを作成、または既存プロジェクトを選択
  3. 左側メニューから「APIとサービス」→「認証情報」を選択
  4. 「認証情報を作成」→「OAuthクライアントID」を選択
  5. アプリケーションの種類として「ウェブアプリケーション」を選択
  6. 以下の情報を入力
項目 設定値
名前 任意(例:Spring Boot OAuth2 App)
承認済みのリダイレクトURI http://localhost:8080/login/oauth2/code/google
  1. 「作成」をクリックし、表示されるクライアントIDとクライアントシークレットを控える

リダイレクトURIの構造

Spring SecurityのデフォルトのリダイレクトURIは以下の形式です。

{baseUrl}/login/oauth2/code/{registrationId}

registrationIdは、設定ファイルで指定するOAuthクライアントの識別子です。Googleの場合はgoogleを使用することで、CommonOAuth2Providerによる自動設定が適用されます。

GitHub OAuth2クライアントの設定

GitHub Developer Settingsでの設定

GitHub OAuth Appを作成するには、以下の手順を実行します。

  1. GitHub Developer Settings(https://github.com/settings/developers)にアクセス
  2. 「OAuth Apps」→「New OAuth App」を選択
  3. 以下の情報を入力
項目 設定値
Application name 任意(例:Spring Boot OAuth2 App)
Homepage URL http://localhost:8080
Authorization callback URL http://localhost:8080/login/oauth2/code/github
  1. 「Register application」をクリック
  2. 表示されるClient IDを控え、「Generate a new client secret」でシークレットを生成

アプリケーション設定

application.ymlの設定

取得したクライアントIDとシークレットをapplication.ymlに設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope:
              - openid
              - profile
              - email
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope:
              - read:user
              - user:email

logging:
  level:
    org.springframework.security: DEBUG

環境変数の設定

クライアントシークレットはソースコードにハードコーディングせず、環境変数で管理します。

Windows(PowerShell):

1
2
3
4
$env:GOOGLE_CLIENT_ID="your-google-client-id"
$env:GOOGLE_CLIENT_SECRET="your-google-client-secret"
$env:GITHUB_CLIENT_ID="your-github-client-id"
$env:GITHUB_CLIENT_SECRET="your-github-client-secret"

macOS/Linux:

1
2
3
4
export GOOGLE_CLIENT_ID="your-google-client-id"
export GOOGLE_CLIENT_SECRET="your-google-client-secret"
export GITHUB_CLIENT_ID="your-github-client-id"
export GITHUB_CLIENT_SECRET="your-github-client-secret"

CommonOAuth2Providerによる自動設定

Spring Securityには、主要なOAuthプロバイダーのエンドポイント設定がCommonOAuth2Providerとして事前定義されています。

プロバイダー 自動設定される項目
Google authorization-uri, token-uri, user-info-uri, jwk-set-uri
GitHub authorization-uri, token-uri, user-info-uri
Facebook authorization-uri, token-uri, user-info-uri
Okta カスタム設定が必要

registrationIdをプロバイダー名(google, github等)と一致させることで、これらの設定が自動適用されます。

Security設定クラスの実装

基本的なSecurityFilterChainの設定

OAuth2ログインを有効化するための基本的なSecurity設定を実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.oauth2demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/login", "/error", "/css/**", "/js/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(withDefaults());
        
        return http.build();
    }
}

この設定により、以下の動作が実現されます。

  • //login/error、静的リソースは認証なしでアクセス可能
  • その他のエンドポイントは認証が必要
  • デフォルトのOAuth2ログインページが有効化

カスタムログインページの設定

デフォルトのログインページをカスタマイズする場合は、以下のように設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/login", "/error", "/css/**", "/js/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error=true")
            );
        
        return http.build();
    }
}
設定項目 説明
loginPage() カスタムログインページのパス
defaultSuccessUrl() ログイン成功後のリダイレクト先
failureUrl() ログイン失敗時のリダイレクト先

OidcUserServiceによるユーザー情報取得

OidcUserServiceのカスタマイズ

OpenID Connect対応プロバイダー(Google等)からユーザー情報を取得し、独自のユーザーエンティティにマッピングするには、OidcUserServiceをカスタマイズします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example.oauth2demo.config;

import com.example.oauth2demo.domain.User;
import com.example.oauth2demo.repository.UserRepository;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;

@Service
public class CustomOidcUserService extends OidcUserService {

    private final UserRepository userRepository;

    public CustomOidcUserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
        OidcUser oidcUser = super.loadUser(userRequest);
        
        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String email = oidcUser.getEmail();
        String name = oidcUser.getFullName();
        String picture = oidcUser.getPicture();
        
        // ユーザー情報をデータベースに保存または更新
        User user = userRepository.findByEmail(email)
            .map(existingUser -> {
                existingUser.setName(name);
                existingUser.setPicture(picture);
                existingUser.setProvider(registrationId);
                return userRepository.save(existingUser);
            })
            .orElseGet(() -> {
                User newUser = new User();
                newUser.setEmail(email);
                newUser.setName(name);
                newUser.setPicture(picture);
                newUser.setProvider(registrationId);
                return userRepository.save(newUser);
            });
        
        return oidcUser;
    }
}

OAuth2UserServiceのカスタマイズ(GitHub用)

GitHubはOIDCに対応していないため、OAuth2UserServiceをカスタマイズします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.example.oauth2demo.config;

import com.example.oauth2demo.domain.User;
import com.example.oauth2demo.repository.UserRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;

    public CustomOAuth2UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oauth2User = super.loadUser(userRequest);
        
        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        
        // GitHubの場合の処理
        if ("github".equals(registrationId)) {
            String email = oauth2User.getAttribute("email");
            String name = oauth2User.getAttribute("name");
            String login = oauth2User.getAttribute("login");
            String avatarUrl = oauth2User.getAttribute("avatar_url");
            
            // emailがnullの場合はloginを使用
            String identifier = email != null ? email : login + "@github.com";
            
            User user = userRepository.findByEmail(identifier)
                .map(existingUser -> {
                    existingUser.setName(name != null ? name : login);
                    existingUser.setPicture(avatarUrl);
                    existingUser.setProvider(registrationId);
                    return userRepository.save(existingUser);
                })
                .orElseGet(() -> {
                    User newUser = new User();
                    newUser.setEmail(identifier);
                    newUser.setName(name != null ? name : login);
                    newUser.setPicture(avatarUrl);
                    newUser.setProvider(registrationId);
                    return userRepository.save(newUser);
                });
        }
        
        return oauth2User;
    }
}

カスタムUserServiceの登録

作成したカスタムUserServiceをSecurityConfigに登録します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomOidcUserService customOidcUserService;
    private final CustomOAuth2UserService customOAuth2UserService;

    public SecurityConfig(CustomOidcUserService customOidcUserService,
                         CustomOAuth2UserService customOAuth2UserService) {
        this.customOidcUserService = customOidcUserService;
        this.customOAuth2UserService = customOAuth2UserService;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/login", "/error", "/css/**", "/js/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true)
                .userInfoEndpoint(userInfo -> userInfo
                    .oidcUserService(customOidcUserService)
                    .userService(customOAuth2UserService)
                )
            );
        
        return http.build();
    }
}

ユーザーエンティティとリポジトリ

Userエンティティの定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.example.oauth2demo.domain;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String name;

    private String picture;

    @Column(nullable = false)
    private String provider;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }

    // Getter/Setter省略
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getPicture() { return picture; }
    public void setPicture(String picture) { this.picture = picture; }
    public String getProvider() { return provider; }
    public void setProvider(String provider) { this.provider = provider; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public LocalDateTime getUpdatedAt() { return updatedAt; }
}

UserRepositoryの定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.example.oauth2demo.repository;

import com.example.oauth2demo.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

コントローラーとビューの実装

HomeControllerの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.oauth2demo.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "home";
    }

    @GetMapping("/login")
    public String login() {
        return "login";
    }

    @GetMapping("/dashboard")
    public String dashboard(@AuthenticationPrincipal OAuth2User principal, Model model) {
        if (principal != null) {
            model.addAttribute("name", principal.getAttribute("name"));
            model.addAttribute("email", principal.getAttribute("email"));
            
            // プロバイダーによって属性名が異なる
            String picture = principal.getAttribute("picture");
            if (picture == null) {
                picture = principal.getAttribute("avatar_url"); // GitHub
            }
            model.addAttribute("picture", picture);
        }
        return "dashboard";
    }
}

ログインページのテンプレート(login.html)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ログイン</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background-color: #f5f5f5;
        }
        .login-container {
            background: white;
            padding: 40px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            text-align: center;
            max-width: 400px;
            width: 100%;
        }
        h1 {
            margin-bottom: 30px;
            color: #333;
        }
        .oauth-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 100%;
            padding: 12px 20px;
            margin: 10px 0;
            border: 1px solid #ddd;
            border-radius: 4px;
            text-decoration: none;
            color: #333;
            font-size: 16px;
            transition: background-color 0.3s;
        }
        .oauth-btn:hover {
            background-color: #f0f0f0;
        }
        .oauth-btn img {
            width: 20px;
            height: 20px;
            margin-right: 10px;
        }
        .google-btn {
            background-color: white;
        }
        .github-btn {
            background-color: #24292e;
            color: white;
            border-color: #24292e;
        }
        .github-btn:hover {
            background-color: #3a3f44;
        }
        .error-message {
            color: #d32f2f;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div class="login-container">
        <h1>ログイン</h1>
        
        <div th:if="${param.error}" class="error-message">
            ログインに失敗しました。もう一度お試しください。
        </div>
        
        <a href="/oauth2/authorization/google" class="oauth-btn google-btn">
            <img src="https://www.google.com/favicon.ico" alt="Google">
            Googleでログイン
        </a>
        
        <a href="/oauth2/authorization/github" class="oauth-btn github-btn">
            <img src="https://github.com/favicon.ico" alt="GitHub" style="filter: invert(1);">
            GitHubでログイン
        </a>
    </div>
</body>
</html>

ダッシュボードページのテンプレート(dashboard.html)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ダッシュボード</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background-color: #f5f5f5;
        }
        .dashboard-container {
            background: white;
            padding: 40px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            text-align: center;
            max-width: 500px;
            width: 100%;
        }
        .profile-image {
            width: 100px;
            height: 100px;
            border-radius: 50%;
            margin-bottom: 20px;
        }
        h1 {
            margin-bottom: 10px;
            color: #333;
        }
        .email {
            color: #666;
            margin-bottom: 30px;
        }
        .logout-btn {
            display: inline-block;
            padding: 12px 24px;
            background-color: #dc3545;
            color: white;
            text-decoration: none;
            border-radius: 4px;
            transition: background-color 0.3s;
        }
        .logout-btn:hover {
            background-color: #c82333;
        }
    </style>
</head>
<body>
    <div class="dashboard-container">
        <img th:src="${picture}" alt="Profile" class="profile-image" 
             onerror="this.src='https://via.placeholder.com/100'">
        <h1>ようこそ、<span th:text="${name}">ユーザー</span>さん</h1>
        <p class="email" th:text="${email}">email@example.com</p>
        
        <form th:action="@{/logout}" method="post">
            <button type="submit" class="logout-btn">ログアウト</button>
        </form>
    </div>
</body>
</html>

権限のマッピング

GrantedAuthoritiesMapperの実装

OAuth2プロバイダーから取得した情報に基づいて、アプリケーション独自の権限を付与できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.example.oauth2demo.config;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

@Component
public class CustomAuthoritiesMapper implements GrantedAuthoritiesMapper {

    @Override
    public Collection<? extends GrantedAuthority> mapAuthorities(
            Collection<? extends GrantedAuthority> authorities) {
        
        Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
        
        authorities.forEach(authority -> {
            if (authority instanceof OidcUserAuthority oidcAuth) {
                // OIDCユーザー(Google等)の場合
                mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
                
                String email = oidcAuth.getIdToken().getEmail();
                if (email != null && email.endsWith("@admin.example.com")) {
                    mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
                }
                
            } else if (authority instanceof OAuth2UserAuthority oauth2Auth) {
                // OAuth2ユーザー(GitHub等)の場合
                mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
                
                String login = (String) oauth2Auth.getAttributes().get("login");
                // 特定のGitHubユーザーに管理者権限を付与
                if ("admin-user".equals(login)) {
                    mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
                }
            }
        });
        
        return mappedAuthorities;
    }
}

SecurityConfigへの登録

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomOidcUserService customOidcUserService;
    private final CustomOAuth2UserService customOAuth2UserService;
    private final CustomAuthoritiesMapper authoritiesMapper;

    public SecurityConfig(CustomOidcUserService customOidcUserService,
                         CustomOAuth2UserService customOAuth2UserService,
                         CustomAuthoritiesMapper authoritiesMapper) {
        this.customOidcUserService = customOidcUserService;
        this.customOAuth2UserService = customOAuth2UserService;
        this.authoritiesMapper = authoritiesMapper;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/login", "/error", "/css/**", "/js/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true)
                .userInfoEndpoint(userInfo -> userInfo
                    .oidcUserService(customOidcUserService)
                    .userService(customOAuth2UserService)
                    .userAuthoritiesMapper(authoritiesMapper)
                )
            );
        
        return http.build();
    }
}

OAuth2ログインのアーキテクチャ

Spring Security OAuth2ログインの内部処理フローを理解しておくと、トラブルシューティングに役立ちます。

flowchart TD
    A[OAuth2AuthorizationRequestRedirectFilter] -->|認可リクエスト生成| B[AuthorizationServer]
    B -->|認可コード返却| C[OAuth2LoginAuthenticationFilter]
    C -->|トークン交換| D[OAuth2AccessTokenResponseClient]
    D -->|アクセストークン取得| E{プロバイダータイプ}
    E -->|OIDC| F[OidcUserService]
    E -->|OAuth2| G[OAuth2UserService]
    F --> H[OidcUser生成]
    G --> I[OAuth2User生成]
    H --> J[OAuth2AuthenticationToken]
    I --> J
    J --> K[SecurityContext保存]

主要コンポーネント

コンポーネント 役割
OAuth2AuthorizationRequestRedirectFilter 認可リクエストを生成し、プロバイダーへリダイレクト
OAuth2LoginAuthenticationFilter 認可コードを受け取り、認証処理を実行
OAuth2AccessTokenResponseClient 認可コードとアクセストークンを交換
OidcUserService OIDCプロバイダーからユーザー情報を取得
OAuth2UserService OAuth2プロバイダーからユーザー情報を取得

トラブルシューティング

よくあるエラーと対処法

エラー 原因 対処法
redirect_uri_mismatch リダイレクトURIの不一致 プロバイダー設定のリダイレクトURIとアプリケーション設定を確認
invalid_client クライアントID/シークレットの誤り 環境変数の値を再確認
access_denied ユーザーが同意を拒否 エラーページでの適切なメッセージ表示
server_error プロバイダー側のエラー 時間をおいて再試行

デバッグログの有効化

問題の特定には、Spring Securityのデバッグログが有効です。

1
2
3
4
logging:
  level:
    org.springframework.security: DEBUG
    org.springframework.security.oauth2: TRACE

プロキシ環境での設定

企業ネットワークなどプロキシ環境では、追加設定が必要な場合があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
    DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
    
    RestTemplate restTemplate = new RestTemplate();
    // プロキシ設定
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.example.com", 8080)));
    restTemplate.setRequestFactory(factory);
    
    client.setRestOperations(restTemplate);
    return client;
}

本番環境での考慮事項

HTTPSの必須化

本番環境では、必ずHTTPSを使用してください。OAuth2の仕様上、認可コードやトークンがネットワーク上で露出するリスクを防ぐためです。

1
2
3
4
5
6
server:
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: ${SSL_KEYSTORE_PASSWORD}
    key-store-type: PKCS12

クライアントシークレットの安全な管理

方法 説明 推奨環境
環境変数 シンプルだが管理が煩雑 開発・検証環境
Spring Cloud Config 中央管理が可能 マイクロサービス環境
HashiCorp Vault 高度なシークレット管理 大規模本番環境
AWS Secrets Manager AWSネイティブ AWS環境

セッション管理

OAuth2ログインはセッションベースで動作します。スケールアウト環境では、Redisなどの外部セッションストアの利用を検討してください。

1
2
3
4
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

まとめ

本記事では、Spring Security 6.4を使用したOAuth2ソーシャルログインの実装方法を解説しました。主要なポイントを振り返ります。

項目 内容
OAuth2の基本 認可コードフローによる安全なトークン取得
Spring Boot設定 spring-boot-starter-oauth2-clientによる簡潔な設定
プロバイダー設定 Google Cloud ConsoleとGitHub Developer Settingsでのクライアント登録
ユーザー情報取得 OidcUserService/OAuth2UserServiceのカスタマイズ
権限マッピング GrantedAuthoritiesMapperによる柔軟な権限付与

OAuth2ソーシャルログインを導入することで、ユーザー体験の向上とセキュリティリスクの軽減を同時に実現できます。次のステップとして、複数プロバイダーでのアカウントリンク機能や、OAuth2とJWT認証の組み合わせなどを検討してみてください。

参考リンク