انتقل إلى المحتوى الرئيسي

الفصل 4: OAuth2 وتسجيل الدخول الاجتماعي

مقدمة​

بدلاً من إدارة كلمات المرور بنفسك، لماذا لا تدع Google أو GitHub أو Facebook يتولون الأمر؟ OAuth2 يسمح لمستخدميك بتسجيل الدخول باستخدام حساباتهم الحالية، مما يحسن تجربة المستخدم والأمان.


🎯 أهداف الفصل​

في نهاية هذا الفصل، ستعرف:

  1. ✅ فهم OAuth2 ومفاهيمه
  2. ✅ التمييز بين التدفقات المختلفة لـ OAuth2
  3. ✅ إعداد Spring Security OAuth2 Client
  4. ✅ تنفيذ تسجيل الدخول باستخدام Google
  5. ✅ استرداد معلومات المستخدم

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​

  1. اذهب إلى https://console.cloud.google.com
  2. إنشاء مشروع جديد
  3. اذهب إلى "APIs & Services" → "Credentials"
  4. إنشاء "OAuth 2.0 Client IDs"
  5. النوع: "تطبيق ويب"
  6. 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​

  1. اذهب إلى https://github.com/settings/developers
  2. "OAuth Apps" → "New OAuth App"
  3. Homepage URL: http://localhost:8080
  4. 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).

➡️ الفصل 5: الأمان المتقدم


⬅️ العودة إلى الوحدة 8