外部の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クライアントを登録する必要があります。
- Google Cloud Console(https://console.cloud.google.com/)にアクセス
- 新しいプロジェクトを作成、または既存プロジェクトを選択
- 左側メニューから「APIとサービス」→「認証情報」を選択
- 「認証情報を作成」→「OAuthクライアントID」を選択
- アプリケーションの種類として「ウェブアプリケーション」を選択
- 以下の情報を入力
| 項目 |
設定値 |
| 名前 |
任意(例:Spring Boot OAuth2 App) |
| 承認済みのリダイレクトURI |
http://localhost:8080/login/oauth2/code/google |
- 「作成」をクリックし、表示されるクライアントIDとクライアントシークレットを控える
リダイレクトURIの構造#
Spring SecurityのデフォルトのリダイレクトURIは以下の形式です。
{baseUrl}/login/oauth2/code/{registrationId}
registrationIdは、設定ファイルで指定するOAuthクライアントの識別子です。Googleの場合はgoogleを使用することで、CommonOAuth2Providerによる自動設定が適用されます。
GitHub OAuth2クライアントの設定#
GitHub Developer Settingsでの設定#
GitHub OAuth Appを作成するには、以下の手順を実行します。
- GitHub Developer Settings(https://github.com/settings/developers)にアクセス
- 「OAuth Apps」→「New OAuth App」を選択
- 以下の情報を入力
| 項目 |
設定値 |
| Application name |
任意(例:Spring Boot OAuth2 App) |
| Homepage URL |
http://localhost:8080 |
| Authorization callback URL |
http://localhost:8080/login/oauth2/code/github |
- 「Register application」をクリック
- 表示される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認証の組み合わせなどを検討してみてください。
参考リンク#