الفصل 4: OAuth2 وتسجيل الدخول الاجتماعي
مقدمة
بدلاً من إدارة كلمات المرور بنفسك، لماذا لا تدع Google أو GitHub أو Facebook يتولون الأمر؟ OAuth2 يسمح لمستخدميك بتسجيل الدخول باستخدام حساباتهم الحالية، مما يحسن تجربة المستخدم والأمان.
🎯 أهداف الفصل
في نهاية هذا الفصل، ستعرف:
- ✅ فهم OAuth2 ومفاهيمه
- ✅ التمييز بين التدفقات المختلفة لـ OAuth2
- ✅ إعداد Spring Security OAuth2 Client
- ✅ تنفيذ تسجيل الدخول باستخدام Google
- ✅ استرداد معلومات المستخدم
1. مفاهيم OAuth2
الأدوار الأربعة
| الدور | الوصف | المثال |
|---|---|---|
| مالك المورد | المستخدم الذي يمتلك البيانات | جان الذي يريد تسجيل الدخول |
| العميل | تطبيقك | your-app.com |
| خادم التفويض | الخادم الذي يقوم بالمصادقة | accounts.google.com |
| خادم المورد | الخادم الذي يحتوي على البيانات | googleapis.com |
تدفق رمز التفويض
التدفق الأكثر أمانًا لتطبيقات الويب:
2. إعداد Spring Boot
التبعيات
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
إعداد Google
الخطوة 1: إنشاء مشروع Google Cloud
- اذهب إلى https://console.cloud.google.com
- إنشاء مشروع جديد
- اذهب إلى "APIs & Services" → "Credentials"
- إنشاء "OAuth 2.0 Client IDs"
- النوع: "تطبيق ويب"
- Authorized redirect URIs:
http://localhost:8080/login/oauth2/code/google
الخطوة 2: application.properties
# Google OAuth2
spring.security.oauth2.client.registration.google.client-id=VOTRE_CLIENT_ID
spring.security.oauth2.client.registration.google.client-secret=VOTRE_CLIENT_SECRET
spring.security.oauth2.client.registration.google.scope=openid,profile,email
إعداد GitHub
الخطوة 1: إعدادات مطور GitHub
- اذهب إلى https://github.com/settings/developers
- "OAuth Apps" → "New OAuth App"
- Homepage URL:
http://localhost:8080 - Authorization callback URL:
http://localhost:8080/login/oauth2/code/github
الخطوة 2: application.properties
# GitHub OAuth2
spring.security.oauth2.client.registration.github.client-id=VOTRE_CLIENT_ID
spring.security.oauth2.client.registration.github.client-secret=VOTRE_CLIENT_SECRET
spring.security.oauth2.client.registration.github.scope=user:email
3. SecurityConfig لـ OAuth2
الإعداد الأساسي
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/error", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth -> oauth
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
)
.logout(logout -> logout
.logoutSuccessUrl("/")
)
.build();
}
}
صفحة تسجيل الدخول مع أزرار OAuth2
<!-- templates/login.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>تسجيل الدخول</title>
<style>
.oauth-buttons { margin-top: 20px; }
.oauth-btn {
display: inline-block;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
text-decoration: none;
color: white;
}
.google { background: #db4437; }
.github { background: #333; }
</style>
</head>
<body>
<div class="login-container">
<h1>تسجيل الدخول</h1>
<!-- تسجيل الدخول التقليدي -->
<form th:action="@{/login}" method="post">
<input type="text" name="username" placeholder="اسم المستخدم">
<input type="password" name="password" placeholder="كلمة المرور">
<button type="submit">تسجيل الدخول</button>
</form>
<hr>
<!-- تسجيل الدخول باستخدام OAuth2 -->
<div class="oauth-buttons">
<p>أو قم بتسجيل الدخول باستخدام:</p>
<a href="/oauth2/authorization/google" class="oauth-btn google">
🔵 Google
</a>
<a href="/oauth2/authorization/github" class="oauth-btn github">
⚫ GitHub
</a>
</div>
</div>
</body>
</html>
4. استرداد معلومات المستخدم
في Controller
@Controller
public class ProfileController {
@GetMapping("/dashboard")
public String dashboard(
@AuthenticationPrincipal OAuth2User oauth2User,
Model model) {
if (oauth2User != null) {
model.addAttribute("name", oauth2User.getAttribute("name"));
model.addAttribute("email", oauth2User.getAttribute("email"));
model.addAttribute("picture", oauth2User.getAttribute("picture"));
model.addAttribute("attributes", oauth2User.getAttributes());
}
return "dashboard";
}
}
السمات حسب المزود
Google
oauth2User.getAttribute("sub"); // معرف فريد
oauth2User.getAttribute("name"); // الاسم الكامل
oauth2User.getAttribute("email"); // البريد الإلكتروني
oauth2User.getAttribute("picture"); // رابط الصورة
GitHub
oauth2User.getAttribute("id"); // معرف فريد
oauth2User.getAttribute("login"); // اسم المستخدم
oauth2User.getAttribute("name"); // الاسم الكامل
oauth2User.getAttribute("email"); // البريد الإلكتروني (قد يكون null)
oauth2User.getAttribute("avatar_url"); // رابط الصورة الرمزية
5. ربط OAuth2 بمستخدميك
الاستراتيجية
كيان OAuth2Connection
@Entity
@Table(name = "oauth2_connections")
@Data
@NoArgsConstructor
public class OAuth2Connection {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(nullable = false)
private String provider; // google, github
@Column(nullable = false)
private String providerUserId; // معرف لدى المزود
@Column(nullable = false)
private LocalDateTime connectedAt = LocalDateTime.now();
}
CustomOAuth2UserService
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
private final OAuth2ConnectionRepository connectionRepository;
@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerUserId = getProviderUserId(oauth2User, provider);
String email = oauth2User.getAttribute("email");
String name = oauth2User.getAttribute("name");
// البحث عن اتصال موجود
Optional<OAuth2Connection> existingConnection = connectionRepository
.findByProviderAndProviderUserId(provider, providerUserId);
User user;
if (existingConnection.isPresent()) {
user = existingConnection.get().getUser();
} else if (email != null && userRepository.existsByEmail(email)) {
// الربط بحساب موجود عبر البريد الإلكتروني
user = userRepository.findByEmail(email).orElseThrow();
createConnection(user, provider, providerUserId);
} else {
// إنشاء حساب جديد
user = createNewUser(email, name);
createConnection(user, provider, providerUserId);
}
return new CustomOAuth2User(oauth2User, user);
}
private String getProviderUserId(OAuth2User oauth2User, String provider) {
return switch (provider) {
case "google" -> oauth2User.getAttribute("sub");
case "github" -> String.valueOf(oauth2User.getAttribute("id"));
default -> throw new OAuth2AuthenticationException("مزود غير مدعوم");
};
}
private User createNewUser(String email, String name) {
User user = new User();
user.setEmail(email);
user.setUsername(email); // أو توليد اسم مستخدم
user.setPassword(""); // لا توجد كلمة مرور لـ OAuth2
user.setEnabled(true);
// إضافة دور افتراضي
return userRepository.save(user);
}
private void createConnection(User user, String provider, String providerUserId) {
OAuth2Connection connection = new OAuth2Connection();
connection.setUser(user);
connection.setProvider(provider);
connection.setProviderUserId(providerUserId);
connectionRepository.save(connection);
}
}
CustomOAuth2User
public class CustomOAuth2User implements OAuth2User {
private final OAuth2User oauth2User;
private final User user;
public CustomOAuth2User(OAuth2User oauth2User, User user) {
this.oauth2User = oauth2User;
this.user = user;
}
@Override
public Map<String, Object> getAttributes() {
return oauth2User.getAttributes();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
@Override
public String getName() {
return user.getUsername();
}
public User getUser() {
return user;
}
}
الإعداد مع خدمة مخصصة
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/error").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth -> oauth
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.defaultSuccessUrl("/dashboard")
)
.build();
}
}
6. دمج OAuth2 + تسجيل الدخول بالنموذج
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/register", "/error").permitAll()
.anyRequest().authenticated()
)
// تسجيل الدخول بالنموذج
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
// تسجيل الدخول باستخدام OAuth2
.oauth2Login(oauth -> oauth
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.defaultSuccessUrl("/dashboard")
)
.logout(logout -> logout.permitAll())
.build();
}
7. OAuth2 لـ API REST (JWT + OAuth2)
تدفق هجين
نقطة النهاية لتبادل الرموز
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class OAuth2TokenController {
private final GoogleIdTokenVerifier verifier;
private final UserService userService;
private final JwtService jwtService;
@PostMapping("/google")
public ResponseEntity<AuthResponse> googleAuth(@RequestBody GoogleAuthRequest request) {
try {
// تحقق من رمز Google
GoogleIdToken idToken = verifier.verify(request.getIdToken());
if (idToken == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
GoogleIdToken.Payload payload = idToken.getPayload();
String email = payload.getEmail();
String name = (String) payload.get("name");
String pictureUrl = (String) payload.get("picture");
// إنشاء أو العثور على المستخدم
User user = userService.findOrCreateGoogleUser(email, name, pictureUrl);
// توليد JWT
UserDetails userDetails = new CustomUserDetails(user);
String accessToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);
return ResponseEntity.ok(AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.username(user.getUsername())
.build());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
}
8. ملخص
المفاهيم الرئيسية
| المصطلح | الوصف |
|---|---|
| OAuth2 | بروتوكول التفويض |
| Authorization Code | تدفق لتطبيقات الويب |
| Access Token | الوصول إلى الموارد |
| ID Token | هوية المستخدم (OpenID Connect) |
الإعداد الأدنى
spring.security.oauth2.client.registration.google.client-id=XXX
spring.security.oauth2.client.registration.google.client-secret=YYY
روابط تسجيل الدخول
/oauth2/authorization/google → تسجيل الدخول باستخدام Google
/oauth2/authorization/github → تسجيل الدخول باستخدام GitHub
🎯 الخطوة التالية
الآن بعد أن أتقنت المصادقة، دعنا ننتقل إلى ميزات الأمان المتقدمة (CSRF، CORS، @PreAuthorize).