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

TP4 - تسجيل الدخول الاجتماعي باستخدام Google OAuth2

أهداف التمرين​

في هذا التمرين، سوف:

  1. ✅ تكوين مشروع Google Cloud
  2. ✅ دمج OAuth2 Client مع Spring Security
  3. ✅ استرداد معلومات المستخدم من Google
  4. ✅ ربط حسابات Google بالمستخدمين المحليين
  5. ✅ دمج تسجيل الدخول الكلاسيكي وتسجيل الدخول الاجتماعي

المدة المقدرة: 1 ساعة و30 دقيقة


المتطلبات الأساسية​

  • حساب Google
  • الوصول إلى Google Cloud Console
  • مشروع التمرين 8.1 (تسجيل الدخول بالنموذج)

الجزء 1: تكوين Google Cloud​

الخطوة 1: إنشاء مشروع​

  1. الذهاب إلى https://console.cloud.google.com
  2. إنشاء مشروع جديد: "Spring Security Demo"
  3. تحديد هذا المشروع

الخطوة 2: تكوين شاشة موافقة OAuth​

  1. القائمة → "APIs & Services" → "OAuth consent screen"
  2. النوع: خارجي
  3. ملء:
    • اسم التطبيق: "تطبيقي Spring"
    • بريد الدعم للمستخدم: بريدك الإلكتروني
    • جهة اتصال المطور: بريدك الإلكتروني
  4. النطاقات: إضافة email, profile, openid
  5. المستخدمين التجريبيين: إضافة بريدك الإلكتروني في Google

الخطوة 3: إنشاء بيانات الاعتماد​

  1. القائمة → "APIs & Services" → "Credentials"
  2. "إنشاء بيانات الاعتماد" → "OAuth client ID"
  3. نوع التطبيق: تطبيق ويب
  4. الاسم: "تطبيق Spring Boot"
  5. عناوين URI المعتمدة لإعادة التوجيه:
    http://localhost:8080/login/oauth2/code/google
  6. نسخ Client ID وClient Secret

الجزء 2: تكوين Spring Boot​

التبعيات​

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

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

الجزء 3: SecurityConfig​

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomUserDetailsService userDetailsService;
private final CustomOAuth2UserService oAuth2UserService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/register", "/error", "/css/**").permitAll()
.anyRequest().authenticated()
)
// تسجيل الدخول الكلاسيكي
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
// تسجيل الدخول OAuth2
.oauth2Login(oauth -> oauth
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(oAuth2UserService)
)
.defaultSuccessUrl("/dashboard")
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.permitAll()
)
.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

الجزء 4: CustomOAuth2UserService​

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

private final UserRepository userRepository;
private final RoleRepository roleRepository;

@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);

String provider = userRequest.getClientRegistration().getRegistrationId();
String providerId = oauth2User.getAttribute("sub"); // Google ID
String email = oauth2User.getAttribute("email");
String name = oauth2User.getAttribute("name");
String picture = oauth2User.getAttribute("picture");

log.info("OAuth2 login: provider={}, email={}", provider, email);

// البحث عن المستخدم أو إنشاؤه
User user = userRepository.findByEmail(email)
.orElseGet(() -> createOAuth2User(email, name, provider, providerId));

// تحديث معلومات OAuth2
user.setOauth2Provider(provider);
user.setOauth2Id(providerId);
user.setProfilePicture(picture);
userRepository.save(user);

return new CustomOAuth2User(oauth2User, user);
}

private User createOAuth2User(String email, String name, String provider, String providerId) {
User user = new User();
user.setEmail(email);
user.setUsername(email); // استخدام البريد الإلكتروني كاسم مستخدم
user.setPassword(""); // لا يوجد كلمة مرور لـ OAuth2
user.setEnabled(true);
user.setOauth2Provider(provider);
user.setOauth2Id(providerId);

// استخراج الاسم الأول/الأخير إذا أمكن
if (name != null) {
String[] parts = name.split(" ", 2);
user.setFirstName(parts[0]);
if (parts.length > 1) {
user.setLastName(parts[1]);
}
}

// تعيين الدور الافتراضي
Role userRole = roleRepository.findByName("ROLE_USER")
.orElseThrow(() -> new RuntimeException("Role ROLE_USER not found"));
user.getRoles().add(userRole);

return userRepository.save(user);
}
}

الجزء 5: 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.toSet());
}

@Override
public String getName() {
return user.getUsername();
}

public User getUser() {
return user;
}

public String getEmail() {
return oauth2User.getAttribute("email");
}

public String getPicture() {
return oauth2User.getAttribute("picture");
}
}

الجزء 6: تحديث كيان المستخدم​

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

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

private String password; // يمكن أن يكون فارغًا لـ OAuth2

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

private String firstName;
private String lastName;
private String profilePicture;

// حقول OAuth2
private String oauth2Provider; // google, github, إلخ.
private String oauth2Id; // ID لدى المزود

private boolean enabled = true;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();

public boolean isOAuth2User() {
return oauth2Provider != null;
}
}

الجزء 7: صفحة تسجيل الدخول​

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>تسجيل الدخول</title>
<style>
.login-container { max-width: 400px; margin: 50px auto; padding: 20px; }
.oauth-btn { display: block; padding: 10px; margin: 10px 0; text-align: center;
text-decoration: none; border-radius: 5px; }
.google { background: #db4437; color: white; }
.separator { text-align: center; margin: 20px 0; color: #666; }
.form-group { margin-bottom: 15px; }
input { width: 100%; padding: 10px; margin-top: 5px; }
button { width: 100%; padding: 10px; background: #007bff; color: white;
border: none; cursor: pointer; }
</style>
</head>
<body>
<div class="login-container">
<h1>تسجيل الدخول</h1>

<div th:if="${param.error}" style="color:red; margin-bottom:15px;">
بيانات الاعتماد غير صحيحة.
</div>
<div th:if="${param.logout}" style="color:green; margin-bottom:15px;">
تم تسجيل الخروج بنجاح.
</div>

<!-- تسجيل الدخول باستخدام OAuth2 -->
<a href="/oauth2/authorization/google" class="oauth-btn google">
🔵 تسجيل الدخول باستخدام Google
</a>

<div class="separator">— أو —</div>

<!-- تسجيل الدخول بالنموذج -->
<form th:action="@{/login}" method="post">
<div class="form-group">
<label>اسم المستخدم</label>
<input type="text" name="username" required>
</div>
<div class="form-group">
<label>كلمة المرور</label>
<input type="password" name="password" required>
</div>
<button type="submit">تسجيل الدخول</button>
</form>

<p style="text-align:center; margin-top:20px;">
<a href="/register">ليس لديك حساب؟ سجل الآن</a>
</p>
</div>
</body>
</html>

الجزء 8: لوحة التحكم مع معلومات OAuth2​

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>لوحة التحكم</title>
</head>
<body>
<h1>لوحة التحكم</h1>

<div sec:authorize="isAuthenticated()">
<h2>مرحبًا، <span sec:authentication="name">User</span>!</h2>

<!-- عرض الصورة إذا كان OAuth2 -->
<div th:if="${user != null and user.profilePicture != null}">
<img th:src="${user.profilePicture}" alt="Profile"
style="width:50px; height:50px; border-radius:50%;">
</div>

<h3>المعلومات:</h3>
<ul>
<li>البريد الإلكتروني: <span th:text="${user.email}">email@test.com</span></li>
<li>المزود: <span th:text="${user.oauth2Provider ?: 'Local'}">-</span></li>
<li>الأدوار: <span sec:authentication="authorities">ROLE_USER</span></li>
</ul>

<form th:action="@{/logout}" method="post">
<button type="submit">تسجيل الخروج</button>
</form>
</div>
</body>
</html>

Controller​

@Controller
public class DashboardController {

@GetMapping("/dashboard")
public String dashboard(Model model, Authentication authentication) {
if (authentication.getPrincipal() instanceof CustomOAuth2User) {
CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
model.addAttribute("user", oAuth2User.getUser());
} else if (authentication.getPrincipal() instanceof CustomUserDetails) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
model.addAttribute("user", userDetails.getUser());
}
return "dashboard";
}
}

الجزء 9: اختبار​

قائمة التحقق​

  1. الذهاب إلى http://localhost:8080/login
  2. النقر على "تسجيل الدخول باستخدام Google"
  3. إعادة التوجيه إلى Google
  4. السماح للتطبيق
  5. إعادة التوجيه إلى /dashboard
  6. رؤية معلومات Google (البريد الإلكتروني، الصورة)
  7. التحقق في قاعدة البيانات: المستخدم تم إنشاؤه مع oauth2_provider="google"
  8. تسجيل الخروج
  9. إعادة تسجيل الدخول باستخدام Google → نفس المستخدم
  10. تسجيل الدخول باستخدام اسم المستخدم/كلمة المرور الكلاسيكية → يعمل أيضًا

التحقق من H2​

SELECT id, username, email, oauth2_provider, oauth2_id 
FROM users;

مكافأة: ربط حساب Google بحساب موجود​

نقطة النهاية للربط​

@PostMapping("/link/google")
@PreAuthorize("isAuthenticated()")
public String linkGoogle(@AuthenticationPrincipal CustomUserDetails currentUser) {
// إعادة التوجيه إلى OAuth2 Google مع حالة خاصة
return "redirect:/oauth2/authorization/google?link=true";
}

معالج النجاح المخصص​

@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
// التحقق مما إذا كان الربط
if (request.getParameter("link") != null) {
// الربط بالحساب الموجود
// ...
}

super.onAuthenticationSuccess(request, response, authentication);
}
}

ملخص​

تدفق OAuth2 مع Google​

1. النقر على "Login with Google"
2. إعادة التوجيه: /oauth2/authorization/google
3. Google: تسجيل الدخول + الموافقة
4. رد الاتصال: /login/oauth2/code/google?code=XXX
5. Spring: تبادل الكود → الرموز
6. CustomOAuth2UserService: إنشاء/البحث عن المستخدم
7. لوحة التحكم: متصل!

النقاط الرئيسية​

  • spring-boot-starter-oauth2-client يدير كل التدفق
  • CustomOAuth2UserService لتخصيص إنشاء المستخدم
  • يمكن للمستخدم أن يكون لديه حساب محلي وOAuth2
  • يمكن تخزين معلومات Google (الصورة، الاسم) محليًا

🎯 الوحدة مكتملة!​

تهانينا! لقد أكملت الوحدة 8 - Spring Security.

ملخص:

  • الفصل 1: بنية Spring Security
  • الفصل 2: تسجيل الدخول بالنموذج وHTTP Basic
  • الفصل 3: مصادقة JWT
  • الفصل 4: OAuth2 وتسجيل الدخول الاجتماعي
  • الفصل 5: الأمان المتقدم (CSRF، CORS، @PreAuthorize)
  • TP8.1: تسجيل الدخول بالنموذج
  • TP8.2: API REST مع JWT
  • TP8.3: الأدوار والأذونات
  • TP8.4: OAuth2 Google (هذا التمرين)

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


⬅️ العودة إلى خطة الدورة