Back to blog

OAuth2 & Đăng Nhập Mạng Xã Hội trong Spring Boot

javaspring-bootoauth2securityauthenticationbackend
OAuth2 & Đăng Nhập Mạng Xã Hội trong Spring Boot

Giới thiệu

Các nút "Đăng nhập bằng Google" và "Đăng nhập bằng GitHub" có mặt ở khắp nơi. Người dùng mong đợi đăng nhập mạng xã hội — nó loại bỏ sự phiền phức khi phải tạo thêm một tài khoản và mật khẩu mới. OAuth2 giúp thực hiện điều này một cách an toàn, và Spring Boot khiến việc triển khai trở nên dễ dàng đáng ngạc nhiên.

Bạn sẽ học được gì

✅ Hiểu giao thức OAuth2 và luồng authorization code
✅ Thiết lập Google và GitHub làm nhà cung cấp OAuth2
✅ Cấu hình Spring Security OAuth2 Client
✅ Xây dựng custom OAuth2 user service để ánh xạ người dùng
✅ Tự động đăng ký người dùng khi đăng nhập mạng xã hội lần đầu
✅ Liên kết nhiều tài khoản mạng xã hội vào một người dùng
✅ Kết hợp đăng nhập OAuth2 với xác thực JWT token
✅ Tích hợp đăng nhập OAuth2 với frontend SPA
✅ Kiểm thử luồng OAuth2 trong integration test
✅ Áp dụng các best practice bảo mật cho production

Yêu cầu trước


1. Hiểu về OAuth2

OAuth2 là gì?

OAuth 2.0 là một framework ủy quyền cho phép người dùng cấp quyền truy cập hạn chế cho ứng dụng bên thứ ba vào tài khoản của họ trên một dịch vụ khác — mà không cần chia sẻ mật khẩu.

Khi người dùng nhấn "Đăng nhập bằng Google", ứng dụng của bạn không bao giờ thấy mật khẩu Google của họ. Thay vào đó, Google xác nhận danh tính và gửi cho ứng dụng một token chứa thông tin hồ sơ người dùng.

Các vai trò trong OAuth2

Vai tròMô tảVí dụ
Resource OwnerNgười dùng sở hữu dữ liệuNgười nhấn "Đăng nhập bằng Google"
ClientỨng dụng của bạn yêu cầu quyền truy cậpỨng dụng Spring Boot của bạn
Authorization ServerCấp token sau khi xác thựcMáy chủ OAuth của Google
Resource ServerLưu trữ dữ liệu người dùng được bảo vệAPI hồ sơ người dùng của Google

Luồng Authorization Code

Luồng Authorization Code là tiêu chuẩn cho ứng dụng web phía server và SPA. Đây là luồng OAuth2 an toàn nhất cho đăng nhập mạng xã hội:

Tại sao dùng luồng Authorization Code?

  • Authorization code được trao đổi giữa server-to-server — không bao giờ lộ ra trình duyệt
  • Access token nằm trên server của bạn — giảm nguy cơ bị đánh cắp token
  • Hỗ trợ refresh token cho phiên đăng nhập dài hạn

OAuth2 vs JWT — Khi nào dùng cái nào

Khía cạnhOAuth2 (Đăng nhập MXH)JWT (Xác thực tùy chỉnh)
Trường hợp sử dụng"Đăng nhập bằng Google/GitHub"Đăng nhập bằng username/password
Ai quản lý mật khẩu?Nhà cung cấp (Google, GitHub)Bạn (cơ sở dữ liệu của bạn)
Đăng ký người dùngĐơn giản hơn — không cần quản lý mật khẩuBạn xử lý mọi thứ
Mô hình tin cậyTin tưởng nhà cung cấpTin tưởng server của bạn
Phù hợp nhất choỨng dụng consumer, giảm ma sátỨng dụng nội bộ, kiểm soát hoàn toàn

Trong thực tế, hầu hết ứng dụng kết hợp cả hai: OAuth2 cho đăng nhập mạng xã hội VÀ JWT cho xác thực API. Người dùng đăng nhập qua Google → ứng dụng cấp JWT → JWT xác thực các yêu cầu API tiếp theo.


2. Thiết lập nhà cung cấp OAuth2

Trước khi viết bất kỳ code nào, bạn cần thông tin đăng nhập OAuth2 từ mỗi nhà cung cấp.

Thiết lập Google OAuth2

  1. Truy cập Google Cloud Console
  2. Tạo dự án mới (hoặc chọn dự án hiện có)
  3. Điều hướng đến APIs & Services → Credentials
  4. Nhấn Create Credentials → OAuth client ID
  5. Chọn Web application
  6. Thêm Authorized redirect URIs:
    http://localhost:8080/login/oauth2/code/google
  7. Sao chép Client ID và Client Secret

Quan trọng: Redirect URI phải khớp chính xác. Mẫu mặc định của Spring Boot là /login/oauth2/code/{registrationId}.

Thiết lập GitHub OAuth App

  1. Truy cập GitHub Developer Settings
  2. Nhấn New OAuth App
  3. Điền:
    • Application name: Tên ứng dụng của bạn
    • Homepage URL: http://localhost:8080
    • Authorization callback URL: http://localhost:8080/login/oauth2/code/github
  4. Nhấn Register application
  5. Sao chép Client ID và tạo Client Secret

Lưu trữ thông tin đăng nhập an toàn

Không bao giờ hardcode secret trong mã nguồn. Sử dụng biến môi trường:

# .env (thêm vào .gitignore!)
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

3. Thiết lập Spring Security OAuth2 Client

Thêm Dependencies

Thêm OAuth2 Client starter vào pom.xml:

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
 
    <!-- Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
 
    <!-- OAuth2 Client - thêm hỗ trợ đăng nhập mạng xã hội -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
 
    <!-- JPA để lưu trữ người dùng -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
 
    <!-- PostgreSQL -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
 
    <!-- Hỗ trợ JWT (để cấp token sau đăng nhập OAuth2) -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.3</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.12.3</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.12.3</version>
        <scope>runtime</scope>
    </dependency>
 
    <!-- Lombok (tùy chọn) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

Cấu hình ứng dụng

Cấu hình cả hai nhà cung cấp OAuth2 trong application.yml:

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope:
              - email
              - profile
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope:
              - user:email
              - read:user
 
  datasource:
    url: jdbc:postgresql://localhost:5432/oauth2_demo
    username: ${DB_USERNAME:postgres}
    password: ${DB_PASSWORD:postgres}
 
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
 
app:
  jwt:
    secret: ${JWT_SECRET:your-256-bit-secret-key-for-jwt-signing-min-32-chars}
    expiration: 86400000  # 24 giờ
  oauth2:
    authorized-redirect-uri: http://localhost:3000/oauth2/redirect

Lưu ý: Spring Boot tự động cấu hình các nhà cung cấp Google và GitHub. Bạn chỉ cần client-id, client-secret, và scope. URL authorization, URL token, và URL thông tin người dùng đã được cấu hình sẵn.

Spring Boot tự động làm gì

Chỉ với dependency và properties ở trên, Spring Boot:

  1. Tạo các endpoint đăng nhập tại /oauth2/authorization/google và /oauth2/authorization/github
  2. Xử lý callback OAuth2 tại /login/oauth2/code/google và /login/oauth2/code/github
  3. Đổi authorization code lấy access token
  4. Lấy thông tin người dùng từ nhà cung cấp
  5. Tạo OAuth2AuthenticationToken trong SecurityContext

Bạn có thể kiểm tra ngay — truy cập http://localhost:8080/oauth2/authorization/google và bạn sẽ được chuyển hướng đến trang đăng nhập Google.


4. Custom OAuth2 User Service

Hành vi mặc định đăng nhập người dùng nhưng không lưu họ vào cơ sở dữ liệu. Hãy xây dựng một custom user service.

Entity User hỗ trợ nhiều nhà cung cấp

@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @Column(nullable = false)
    private String name;
 
    @Column(nullable = false, unique = true)
    private String email;
 
    private String imageUrl;
 
    @Column(nullable = false)
    private Boolean emailVerified = false;
 
    private String password;  // null cho người dùng OAuth2
 
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private AuthProvider provider;
 
    private String providerId;
 
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role = Role.USER;
}

Enum AuthProvider

public enum AuthProvider {
    LOCAL,
    GOOGLE,
    GITHUB
}

Enum Role

public enum Role {
    USER,
    ADMIN
}

User Repository

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

Trích xuất thông tin người dùng OAuth2

Mỗi nhà cung cấp trả về dữ liệu người dùng ở định dạng khác nhau. Hãy tạo một lớp trừu tượng:

public abstract class OAuth2UserInfo {
 
    protected Map<String, Object> attributes;
 
    public OAuth2UserInfo(Map<String, Object> attributes) {
        this.attributes = attributes;
    }
 
    public abstract String getId();
    public abstract String getName();
    public abstract String getEmail();
    public abstract String getImageUrl();
}

Thông tin người dùng Google:

public class GoogleOAuth2UserInfo extends OAuth2UserInfo {
 
    public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
        super(attributes);
    }
 
    @Override
    public String getId() {
        return (String) attributes.get("sub");
    }
 
    @Override
    public String getName() {
        return (String) attributes.get("name");
    }
 
    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }
 
    @Override
    public String getImageUrl() {
        return (String) attributes.get("picture");
    }
}

Thông tin người dùng GitHub:

public class GitHubOAuth2UserInfo extends OAuth2UserInfo {
 
    public GitHubOAuth2UserInfo(Map<String, Object> attributes) {
        super(attributes);
    }
 
    @Override
    public String getId() {
        return String.valueOf(attributes.get("id"));
    }
 
    @Override
    public String getName() {
        return (String) attributes.get("name");
    }
 
    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }
 
    @Override
    public String getImageUrl() {
        return (String) attributes.get("avatar_url");
    }
}

Factory tạo đúng bộ trích xuất:

public class OAuth2UserInfoFactory {
 
    public static OAuth2UserInfo getOAuth2UserInfo(
            String registrationId, Map<String, Object> attributes) {
 
        if (registrationId.equalsIgnoreCase(AuthProvider.GOOGLE.name())) {
            return new GoogleOAuth2UserInfo(attributes);
        } else if (registrationId.equalsIgnoreCase(AuthProvider.GITHUB.name())) {
            return new GitHubOAuth2UserInfo(attributes);
        } else {
            throw new OAuth2AuthenticationProcessingException(
                "Đăng nhập bằng " + registrationId + " không được hỗ trợ");
        }
    }
}

Custom OAuth2 User Service

Đây là component cốt lõi. Nó xử lý đăng nhập OAuth2 và đăng ký/cập nhật người dùng:

@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
 
    private final UserRepository userRepository;
 
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest)
            throws OAuth2AuthenticationException {
 
        OAuth2User oAuth2User = super.loadUser(userRequest);
 
        try {
            return processOAuth2User(userRequest, oAuth2User);
        } catch (Exception ex) {
            throw new OAuth2AuthenticationProcessingException(
                ex.getMessage(), ex);
        }
    }
 
    private OAuth2User processOAuth2User(
            OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
 
        String registrationId = userRequest.getClientRegistration()
            .getRegistrationId();
 
        OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory
            .getOAuth2UserInfo(registrationId, oAuth2User.getAttributes());
 
        if (oAuth2UserInfo.getEmail() == null
                || oAuth2UserInfo.getEmail().isEmpty()) {
            throw new OAuth2AuthenticationProcessingException(
                "Không tìm thấy email từ nhà cung cấp OAuth2");
        }
 
        Optional<User> userOptional = userRepository
            .findByEmail(oAuth2UserInfo.getEmail());
 
        User user;
        if (userOptional.isPresent()) {
            user = userOptional.get();
 
            if (!user.getProvider().name()
                    .equalsIgnoreCase(registrationId)) {
                throw new OAuth2AuthenticationProcessingException(
                    "Bạn đã đăng ký bằng " + user.getProvider()
                    + ". Vui lòng sử dụng tài khoản " + user.getProvider()
                    + " để đăng nhập.");
            }
 
            user = updateExistingUser(user, oAuth2UserInfo);
        } else {
            user = registerNewUser(registrationId, oAuth2UserInfo);
        }
 
        return new CustomUserPrincipal(user, oAuth2User.getAttributes());
    }
 
    private User registerNewUser(
            String registrationId, OAuth2UserInfo oAuth2UserInfo) {
 
        User user = new User();
        user.setProvider(AuthProvider.valueOf(registrationId.toUpperCase()));
        user.setProviderId(oAuth2UserInfo.getId());
        user.setName(oAuth2UserInfo.getName());
        user.setEmail(oAuth2UserInfo.getEmail());
        user.setImageUrl(oAuth2UserInfo.getImageUrl());
        user.setEmailVerified(true);
 
        return userRepository.save(user);
    }
 
    private User updateExistingUser(
            User existingUser, OAuth2UserInfo oAuth2UserInfo) {
 
        existingUser.setName(oAuth2UserInfo.getName());
        existingUser.setImageUrl(oAuth2UserInfo.getImageUrl());
 
        return userRepository.save(existingUser);
    }
}

Custom User Principal

Một principal thống nhất hoạt động cho cả người dùng OAuth2 và local (JWT):

public class CustomUserPrincipal implements OAuth2User, UserDetails {
 
    private Long id;
    private String email;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;
    private Map<String, Object> attributes;
 
    public CustomUserPrincipal(
            Long id, String email, String password,
            Collection<? extends GrantedAuthority> authorities) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.authorities = authorities;
    }
 
    // Constructor cho người dùng OAuth2
    public CustomUserPrincipal(User user, Map<String, Object> attributes) {
        this.id = user.getId();
        this.email = user.getEmail();
        this.password = user.getPassword();
        this.authorities = Collections.singletonList(
            new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
        this.attributes = attributes;
    }
 
    // Factory method cho người dùng local
    public static CustomUserPrincipal create(User user) {
        List<GrantedAuthority> authorities = Collections.singletonList(
            new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
 
        return new CustomUserPrincipal(
            user.getId(), user.getEmail(),
            user.getPassword(), authorities);
    }
 
    // Các phương thức UserDetails
    @Override
    public String getUsername() { return email; }
 
    @Override
    public String getPassword() { return password; }
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
 
    @Override
    public boolean isAccountNonExpired() { return true; }
 
    @Override
    public boolean isAccountNonLocked() { return true; }
 
    @Override
    public boolean isCredentialsNonExpired() { return true; }
 
    @Override
    public boolean isEnabled() { return true; }
 
    // Các phương thức OAuth2User
    @Override
    public Map<String, Object> getAttributes() { return attributes; }
 
    @Override
    public String getName() { return String.valueOf(id); }
 
    public Long getId() { return id; }
}

5. Đăng ký người dùng & Liên kết tài khoản

Xử lý các tình huống khác nhau

Khi người dùng đăng nhập qua OAuth2, có nhiều tình huống có thể xảy ra:

Chiến lược liên kết tài khoản

Triển khai cơ bản ở trên từ chối đăng nhập nếu email tồn tại với nhà cung cấp khác. Để trải nghiệm người dùng tốt hơn, bạn có thể cho phép liên kết tài khoản:

@Entity
@Table(name = "user_social_connections")
@Getter
@Setter
@NoArgsConstructor
public class SocialConnection {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
 
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private AuthProvider provider;
 
    @Column(nullable = false)
    private String providerId;
 
    private String providerEmail;
 
    @Column(nullable = false)
    private LocalDateTime connectedAt = LocalDateTime.now();
}
@Repository
public interface SocialConnectionRepository
        extends JpaRepository<SocialConnection, Long> {
 
    Optional<SocialConnection> findByProviderAndProviderId(
        AuthProvider provider, String providerId);
 
    List<SocialConnection> findByUser(User user);
 
    boolean existsByUserAndProvider(User user, AuthProvider provider);
}

User service cập nhật với liên kết tài khoản:

private OAuth2User processOAuth2User(
        OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
 
    String registrationId = userRequest.getClientRegistration()
        .getRegistrationId();
    AuthProvider provider = AuthProvider.valueOf(
        registrationId.toUpperCase());
 
    OAuth2UserInfo userInfo = OAuth2UserInfoFactory
        .getOAuth2UserInfo(registrationId, oAuth2User.getAttributes());
 
    if (userInfo.getEmail() == null || userInfo.getEmail().isEmpty()) {
        throw new OAuth2AuthenticationProcessingException(
            "Không tìm thấy email từ nhà cung cấp OAuth2");
    }
 
    // Kiểm tra xem kết nối mạng xã hội đã tồn tại chưa
    Optional<SocialConnection> connectionOpt = socialConnectionRepository
        .findByProviderAndProviderId(provider, userInfo.getId());
 
    User user;
    if (connectionOpt.isPresent()) {
        // Người dùng quay lại — cập nhật thông tin
        user = connectionOpt.get().getUser();
        user.setName(userInfo.getName());
        user.setImageUrl(userInfo.getImageUrl());
        userRepository.save(user);
    } else {
        // Kiểm tra người dùng theo email
        Optional<User> userOpt = userRepository
            .findByEmail(userInfo.getEmail());
 
        if (userOpt.isPresent()) {
            // Liên kết nhà cung cấp mới vào tài khoản hiện có
            user = userOpt.get();
            createSocialConnection(user, provider, userInfo);
        } else {
            // Người dùng hoàn toàn mới
            user = registerNewUser(provider, userInfo);
            createSocialConnection(user, provider, userInfo);
        }
    }
 
    return new CustomUserPrincipal(user, oAuth2User.getAttributes());
}
 
private void createSocialConnection(
        User user, AuthProvider provider, OAuth2UserInfo userInfo) {
 
    SocialConnection connection = new SocialConnection();
    connection.setUser(user);
    connection.setProvider(provider);
    connection.setProviderId(userInfo.getId());
    connection.setProviderEmail(userInfo.getEmail());
 
    socialConnectionRepository.save(connection);
}

Trường hợp đặc biệt email GitHub

Người dùng GitHub có thể đặt email ở chế độ riêng tư. Trong trường hợp đó, trường email trong thuộc tính người dùng là null. Bạn cần thực hiện thêm một lệnh gọi API:

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
 
    // ... các trường khác
 
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest)
            throws OAuth2AuthenticationException {
 
        OAuth2User oAuth2User = super.loadUser(userRequest);
 
        // Workaround cho email GitHub
        String registrationId = userRequest.getClientRegistration()
            .getRegistrationId();
 
        Map<String, Object> attributes =
            new HashMap<>(oAuth2User.getAttributes());
 
        if ("github".equals(registrationId)
                && attributes.get("email") == null) {
            String email = fetchGitHubEmail(userRequest);
            attributes.put("email", email);
        }
 
        try {
            return processOAuth2User(userRequest,
                new DefaultOAuth2User(
                    oAuth2User.getAuthorities(),
                    attributes,
                    "id"));
        } catch (Exception ex) {
            throw new OAuth2AuthenticationProcessingException(
                ex.getMessage(), ex);
        }
    }
 
    private String fetchGitHubEmail(OAuth2UserRequest userRequest) {
        RestTemplate restTemplate = new RestTemplate();
 
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(
            userRequest.getAccessToken().getTokenValue());
 
        HttpEntity<String> entity = new HttpEntity<>(headers);
 
        ResponseEntity<List<Map<String, Object>>> response =
            restTemplate.exchange(
                "https://api.github.com/user/emails",
                HttpMethod.GET,
                entity,
                new ParameterizedTypeReference<>() {});
 
        return response.getBody().stream()
            .filter(email -> Boolean.TRUE.equals(email.get("primary")))
            .map(email -> (String) email.get("email"))
            .findFirst()
            .orElseThrow(() ->
                new OAuth2AuthenticationProcessingException(
                    "Không tìm thấy email chính từ GitHub"));
    }
}

6. Kết hợp OAuth2 với JWT

Sau khi đăng nhập OAuth2 thành công, chúng ta cấp JWT token để frontend có thể xác thực các yêu cầu API tiếp theo.

JWT Token Provider

Tái sử dụng hoặc điều chỉnh tiện ích JWT từ Hướng dẫn Xác thực JWT:

@Component
public class JwtTokenProvider {
 
    @Value("${app.jwt.secret}")
    private String jwtSecret;
 
    @Value("${app.jwt.expiration}")
    private long jwtExpirationMs;
 
    public String generateToken(CustomUserPrincipal userPrincipal) {
        return Jwts.builder()
            .subject(String.valueOf(userPrincipal.getId()))
            .issuedAt(new Date())
            .expiration(new Date(
                System.currentTimeMillis() + jwtExpirationMs))
            .signWith(getSigningKey())
            .compact();
    }
 
    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
            .verifyWith(getSigningKey())
            .build()
            .parseSignedClaims(token)
            .getPayload();
 
        return Long.parseLong(claims.getSubject());
    }
 
    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                .verifyWith(getSigningKey())
                .build()
                .parseSignedClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException ex) {
            return false;
        }
    }
 
    private SecretKey getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

OAuth2 Authentication Success Handler

Đây là component quan trọng kết nối đăng nhập OAuth2 và JWT:

@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler
        extends SimpleUrlAuthenticationSuccessHandler {
 
    private final JwtTokenProvider tokenProvider;
 
    @Value("${app.oauth2.authorized-redirect-uri}")
    private String authorizedRedirectUri;
 
    @Override
    public void onAuthenticationSuccess(
            HttpServletRequest request,
            HttpServletResponse response,
            Authentication authentication) throws IOException {
 
        CustomUserPrincipal userPrincipal =
            (CustomUserPrincipal) authentication.getPrincipal();
 
        String token = tokenProvider.generateToken(userPrincipal);
 
        // Chuyển hướng về frontend với JWT token
        String targetUrl = UriComponentsBuilder
            .fromUriString(authorizedRedirectUri)
            .queryParam("token", token)
            .build()
            .toUriString();
 
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}

OAuth2 Authentication Failure Handler

Xử lý lỗi một cách graceful:

@Component
public class OAuth2AuthenticationFailureHandler
        extends SimpleUrlAuthenticationFailureHandler {
 
    @Value("${app.oauth2.authorized-redirect-uri}")
    private String authorizedRedirectUri;
 
    @Override
    public void onAuthenticationFailure(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException exception) throws IOException {
 
        String targetUrl = UriComponentsBuilder
            .fromUriString(authorizedRedirectUri)
            .queryParam("error", exception.getLocalizedMessage())
            .build()
            .toUriString();
 
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}

JWT Authentication Filter

Cùng filter từ hướng dẫn JWT — xác thực JWT trên mỗi request:

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
 
    private final JwtTokenProvider tokenProvider;
    private final CustomUserDetailsService userDetailsService;
 
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain)
            throws ServletException, IOException {
 
        String jwt = getJwtFromRequest(request);
 
        if (jwt != null && tokenProvider.validateToken(jwt)) {
            Long userId = tokenProvider.getUserIdFromToken(jwt);
            UserDetails userDetails =
                userDetailsService.loadUserById(userId);
 
            UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(
                    userDetails, null,
                    userDetails.getAuthorities());
 
            authentication.setDetails(
                new WebAuthenticationDetailsSource()
                    .buildDetails(request));
 
            SecurityContextHolder.getContext()
                .setAuthentication(authentication);
        }
 
        filterChain.doFilter(request, response);
    }
 
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

7. Cấu hình bảo mật

Kết hợp tất cả trong cấu hình bảo mật:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
 
    private final CustomOAuth2UserService customOAuth2UserService;
    private final OAuth2AuthenticationSuccessHandler oAuth2SuccessHandler;
    private final OAuth2AuthenticationFailureHandler oAuth2FailureHandler;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
 
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http)
            throws Exception {
 
        http
            // Tắt CSRF cho API JWT stateless
            .csrf(csrf -> csrf.disable())
 
            // Quản lý session — stateless cho JWT
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
 
            // Quy tắc authorization
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(
                    "/api/auth/**",
                    "/oauth2/**",
                    "/login/oauth2/**"
                ).permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
 
            // Cấu hình đăng nhập OAuth2
            .oauth2Login(oauth2 -> oauth2
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService))
                .successHandler(oAuth2SuccessHandler)
                .failureHandler(oAuth2FailureHandler)
            )
 
            // Xử lý exception
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(
                    (request, response, authException) -> {
                        response.setStatus(
                            HttpServletResponse.SC_UNAUTHORIZED);
                        response.setContentType("application/json");
                        response.getWriter().write(
                            "{\"error\": \"Unauthorized\"}");
                    })
            )
 
            // JWT filter trước filter của Spring Security
            .addFilterBefore(jwtAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class);
 
        return http.build();
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
}

Luồng xác thực hoàn chỉnh


8. Hỗ trợ đăng nhập Local

Để hỗ trợ cả OAuth2 và đăng nhập email/mật khẩu truyền thống song song:

Auth Controller

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
 
    private final AuthenticationManager authenticationManager;
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtTokenProvider tokenProvider;
 
    @PostMapping("/signup")
    public ResponseEntity<?> registerUser(
            @Valid @RequestBody SignUpRequest signUpRequest) {
 
        if (userRepository.existsByEmail(signUpRequest.getEmail())) {
            return ResponseEntity.badRequest()
                .body(Map.of("error",
                    "Địa chỉ email đã được sử dụng"));
        }
 
        User user = new User();
        user.setName(signUpRequest.getName());
        user.setEmail(signUpRequest.getEmail());
        user.setPassword(passwordEncoder.encode(
            signUpRequest.getPassword()));
        user.setProvider(AuthProvider.LOCAL);
 
        userRepository.save(user);
 
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(Map.of("message",
                "Đăng ký người dùng thành công"));
    }
 
    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(
            @Valid @RequestBody LoginRequest loginRequest) {
 
        Authentication authentication =
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginRequest.getEmail(),
                    loginRequest.getPassword()));
 
        SecurityContextHolder.getContext()
            .setAuthentication(authentication);
 
        CustomUserPrincipal userPrincipal =
            (CustomUserPrincipal) authentication.getPrincipal();
 
        String token = tokenProvider.generateToken(userPrincipal);
 
        return ResponseEntity.ok(Map.of(
            "token", token,
            "tokenType", "Bearer"
        ));
    }
}

Request DTOs

@Getter
@Setter
public class SignUpRequest {
 
    @NotBlank
    private String name;
 
    @NotBlank
    @Email
    private String email;
 
    @NotBlank
    @Size(min = 8, max = 40)
    private String password;
}
 
@Getter
@Setter
public class LoginRequest {
 
    @NotBlank
    @Email
    private String email;
 
    @NotBlank
    private String password;
}

Custom User Details Service (cho đăng nhập local)

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
 
    private final UserRepository userRepository;
 
    @Override
    public UserDetails loadUserByUsername(String email)
            throws UsernameNotFoundException {
 
        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException(
                "Không tìm thấy người dùng với email: " + email));
 
        return CustomUserPrincipal.create(user);
    }
 
    public UserDetails loadUserById(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UsernameNotFoundException(
                "Không tìm thấy người dùng với id: " + id));
 
        return CustomUserPrincipal.create(user);
    }
}

9. Tích hợp Frontend

Đăng nhập OAuth2 từ SPA

Frontend của bạn (React, Next.js, Vue, v.v.) khởi tạo luồng OAuth2 bằng cách chuyển hướng đến backend:

// Ví dụ React / Next.js
const API_BASE_URL = 'http://localhost:8080';
 
function LoginPage() {
 
  const handleGoogleLogin = () => {
    window.location.href =
      `${API_BASE_URL}/oauth2/authorization/google`;
  };
 
  const handleGitHubLogin = () => {
    window.location.href =
      `${API_BASE_URL}/oauth2/authorization/github`;
  };
 
  return (
    <div>
      <h1>Đăng Nhập</h1>
 
      <button onClick={handleGoogleLogin}>
        Đăng nhập bằng Google
      </button>
 
      <button onClick={handleGitHubLogin}>
        Đăng nhập bằng GitHub
      </button>
 
      {/* Form đăng nhập local */}
      <form onSubmit={handleLocalLogin}>
        <input type="email" placeholder="Email" />
        <input type="password" placeholder="Mật khẩu" />
        <button type="submit">Đăng nhập</button>
      </form>
    </div>
  );
}

Xử lý Redirect Callback

Sau khi đăng nhập OAuth2, backend chuyển hướng về frontend với JWT token trong query string:

// pages/oauth2/redirect.tsx (hoặc tương đương)
import { useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
 
function OAuth2RedirectPage() {
  const router = useRouter();
  const searchParams = useSearchParams();
 
  useEffect(() => {
    const token = searchParams.get('token');
    const error = searchParams.get('error');
 
    if (token) {
      // Lưu JWT token
      localStorage.setItem('accessToken', token);
      router.push('/dashboard');
    } else if (error) {
      console.error('Lỗi OAuth2:', error);
      router.push('/login?error=' + encodeURIComponent(error));
    }
  }, [searchParams, router]);
 
  return <div>Đang xử lý đăng nhập...</div>;
}
 
export default OAuth2RedirectPage;

Sử dụng JWT Token

Khi đã có JWT, đính kèm nó trong mọi yêu cầu API:

// lib/api.ts
const api = {
  async fetch(url: string, options: RequestInit = {}) {
    const token = localStorage.getItem('accessToken');
 
    const response = await fetch(`${API_BASE_URL}${url}`, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...(token && { Authorization: `Bearer ${token}` }),
        ...options.headers,
      },
    });
 
    if (response.status === 401) {
      localStorage.removeItem('accessToken');
      window.location.href = '/login';
    }
 
    return response;
  },
 
  get(url: string) {
    return this.fetch(url);
  },
 
  post(url: string, data: unknown) {
    return this.fetch(url, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  },
};
 
export default api;

10. Bảo vệ API Endpoints

Lấy thông tin người dùng hiện tại

Tạo annotation @CurrentUser cho tiện lợi:

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal
public @interface CurrentUser {
}

Sử dụng trong controllers:

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
 
    private final UserRepository userRepository;
 
    @GetMapping("/me")
    public ResponseEntity<?> getCurrentUser(
            @CurrentUser CustomUserPrincipal currentUser) {
 
        User user = userRepository.findById(currentUser.getId())
            .orElseThrow(() -> new ResourceNotFoundException(
                "User", "id", currentUser.getId()));
 
        return ResponseEntity.ok(Map.of(
            "id", user.getId(),
            "name", user.getName(),
            "email", user.getEmail(),
            "imageUrl", user.getImageUrl(),
            "provider", user.getProvider()
        ));
    }
 
    @GetMapping("/me/connections")
    public ResponseEntity<?> getSocialConnections(
            @CurrentUser CustomUserPrincipal currentUser) {
 
        User user = userRepository.findById(currentUser.getId())
            .orElseThrow(() -> new ResourceNotFoundException(
                "User", "id", currentUser.getId()));
 
        List<SocialConnection> connections =
            socialConnectionRepository.findByUser(user);
 
        return ResponseEntity.ok(connections.stream()
            .map(conn -> Map.of(
                "provider", conn.getProvider(),
                "email", conn.getProviderEmail(),
                "connectedAt", conn.getConnectedAt()
            ))
            .toList());
    }
}

Bảo mật cấp phương thức

Sử dụng @PreAuthorize để kiểm soát truy cập chi tiết — hoạt động giống nhau bất kể người dùng đăng nhập qua OAuth2 hay thông tin đăng nhập local:

@RestController
@RequestMapping("/api/admin")
public class AdminController {
 
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/users")
    public ResponseEntity<List<User>> getAllUsers() {
        return ResponseEntity.ok(userRepository.findAll());
    }
 
    @PreAuthorize("hasRole('ADMIN')")
    @DeleteMapping("/users/{id}")
    public ResponseEntity<?> deleteUser(@PathVariable Long id) {
        userRepository.deleteById(id);
        return ResponseEntity.noContent().build();
    }
}

11. Kiểm thử OAuth2

Unit Test với Mock OAuth2

Spring Security Test cung cấp @WithMockUser và các tiện ích test OAuth2:

@WebMvcTest(UserController.class)
class UserControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @MockBean
    private UserRepository userRepository;
 
    @Test
    @WithMockUser(username = "1", roles = "USER")
    void testGetCurrentUser() throws Exception {
        User user = new User();
        user.setId(1L);
        user.setName("John Doe");
        user.setEmail("[email protected]");
        user.setProvider(AuthProvider.GOOGLE);
 
        when(userRepository.findById(1L))
            .thenReturn(Optional.of(user));
 
        mockMvc.perform(get("/api/users/me"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John Doe"))
            .andExpect(jsonPath("$.provider").value("GOOGLE"));
    }
 
    @Test
    void testUnauthenticatedAccess() throws Exception {
        mockMvc.perform(get("/api/users/me"))
            .andExpect(status().isUnauthorized());
    }
}

Kiểm thử luồng đăng nhập OAuth2

Sử dụng SecurityMockMvcRequestPostProcessors.oauth2Login():

@SpringBootTest
@AutoConfigureMockMvc
class OAuth2LoginTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    void testOAuth2LoginCreatesUser() throws Exception {
        mockMvc.perform(get("/api/users/me")
                .with(oauth2Login()
                    .oauth2User(new DefaultOAuth2User(
                        List.of(
                            new SimpleGrantedAuthority("ROLE_USER")),
                        Map.of(
                            "sub", "123456",
                            "name", "Test User",
                            "email", "[email protected]",
                            "picture", "https://example.com/photo.jpg"
                        ),
                        "sub"
                    ))))
            .andExpect(status().isOk());
    }
}

Integration Test với luồng đầy đủ

@SpringBootTest
@AutoConfigureMockMvc
class AuthIntegrationTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Autowired
    private UserRepository userRepository;
 
    @Autowired
    private JwtTokenProvider tokenProvider;
 
    @Test
    void testLocalSignupAndLogin() throws Exception {
        // 1. Đăng ký
        mockMvc.perform(post("/api/auth/signup")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {
                        "name": "Jane Doe",
                        "email": "[email protected]",
                        "password": "password123"
                    }
                    """))
            .andExpect(status().isCreated());
 
        // 2. Đăng nhập và lấy token
        MvcResult result = mockMvc.perform(post("/api/auth/login")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {
                        "email": "[email protected]",
                        "password": "password123"
                    }
                    """))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.token").exists())
            .andReturn();
 
        String token = JsonPath.read(
            result.getResponse().getContentAsString(), "$.token");
 
        // 3. Truy cập endpoint được bảo vệ với JWT
        mockMvc.perform(get("/api/users/me")
                .header("Authorization", "Bearer " + token))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.email").value("[email protected]"))
            .andExpect(jsonPath("$.provider").value("LOCAL"));
    }
 
    @AfterEach
    void cleanup() {
        userRepository.deleteAll();
    }
}

12. Cân nhắc cho Production

Bảng kiểm tra bảo mật

Vấn đềGiải pháp
Lưu trữ thông tin đăng nhậpSử dụng biến môi trường hoặc trình quản lý secrets (Vault, AWS Secrets Manager)
Yêu cầu HTTPSCác nhà cung cấp OAuth2 yêu cầu HTTPS cho redirect URI trong production
Tham số stateSpring Security tự động xử lý bảo vệ CSRF qua tham số state
Token trong URLJWT trong redirect URL có thời hạn ngắn; frontend lưu trữ nó và xóa khỏi URL
Giới hạn tốc độGiới hạn số lần đăng nhập để ngăn tấn công brute-force
Hết hạn tokenĐặt thời hạn JWT ngắn (15-60 phút) với refresh token
Xác thực redirect URIChỉ cho phép redirect URI đã được whitelist

Xác thực Redirect URI

Ngăn chặn tấn công open redirect bằng cách xác thực redirect URI:

@Component
public class OAuth2AuthenticationSuccessHandler
        extends SimpleUrlAuthenticationSuccessHandler {
 
    @Value("${app.oauth2.authorized-redirect-uri}")
    private String authorizedRedirectUri;
 
    @Override
    public void onAuthenticationSuccess(
            HttpServletRequest request,
            HttpServletResponse response,
            Authentication authentication) throws IOException {
 
        String targetUrl = determineTargetUrl(request, authentication);
 
        if (response.isCommitted()) {
            logger.debug("Response đã được commit.");
            return;
        }
 
        getRedirectStrategy().sendRedirect(
            request, response, targetUrl);
    }
 
    private String determineTargetUrl(
            HttpServletRequest request,
            Authentication authentication) {
 
        CustomUserPrincipal userPrincipal =
            (CustomUserPrincipal) authentication.getPrincipal();
 
        String token = tokenProvider.generateToken(userPrincipal);
 
        return UriComponentsBuilder
            .fromUriString(authorizedRedirectUri)
            .queryParam("token", token)
            .build()
            .toUriString();
    }
}

application.yml cho Production

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: email, profile
            redirect-uri: "https://yourdomain.com/login/oauth2/code/google"
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: user:email, read:user
            redirect-uri: "https://yourdomain.com/login/oauth2/code/github"
 
app:
  jwt:
    secret: ${JWT_SECRET}
    expiration: 3600000  # 1 giờ trong production
  oauth2:
    authorized-redirect-uri: https://yourdomain.com/oauth2/redirect

Thêm nhà cung cấp mới

Kiến trúc này giúp dễ dàng thêm nhà cung cấp mới. Ví dụ, để thêm Facebook:

  1. Đăng ký ứng dụng tại Facebook Developers
  2. Thêm vào application.yml:
spring:
  security:
    oauth2:
      client:
        registration:
          facebook:
            client-id: ${FACEBOOK_CLIENT_ID}
            client-secret: ${FACEBOOK_CLIENT_SECRET}
            scope: email, public_profile
  1. Tạo FacebookOAuth2UserInfo:
public class FacebookOAuth2UserInfo extends OAuth2UserInfo {
 
    public FacebookOAuth2UserInfo(Map<String, Object> attributes) {
        super(attributes);
    }
 
    @Override
    public String getId() {
        return (String) attributes.get("id");
    }
 
    @Override
    public String getName() {
        return (String) attributes.get("name");
    }
 
    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }
 
    @Override
    public String getImageUrl() {
        Map<String, Object> pictureObj =
            (Map<String, Object>) attributes.get("picture");
        if (pictureObj != null) {
            Map<String, Object> data =
                (Map<String, Object>) pictureObj.get("data");
            if (data != null) {
                return (String) data.get("url");
            }
        }
        return null;
    }
}
  1. Thêm vào OAuth2UserInfoFactory và enum AuthProvider

Các vấn đề thường gặp

Vấn đề 1: Redirect URI không khớp

Lỗi: redirect_uri_mismatch từ Google hoặc GitHub

Giải pháp: Redirect URI trong bảng điều khiển nhà cung cấp OAuth phải khớp chính xác với URI mà Spring Boot sử dụng:

http://localhost:8080/login/oauth2/code/google   (development)
https://yourdomain.com/login/oauth2/code/google  (production)

Kiểm tra dấu gạch chéo cuối, http vs https, và số cổng.

Vấn đề 2: Email GitHub là Null

Vấn đề: Người dùng GitHub với email riêng tư trả về null cho trường email.

Giải pháp: Yêu cầu scope user:email và lấy email qua GitHub API (đã trình bày ở Phần 5).

Vấn đề 3: CORS với OAuth2 Redirects

Vấn đề: Lỗi CORS khi frontend chuyển hướng đến endpoint OAuth2 của backend.

Giải pháp: Frontend nên sử dụng window.location.href (chuyển hướng toàn trang), không phải fetch(). Đăng nhập OAuth2 yêu cầu chuyển hướng trình duyệt, không phải lệnh gọi AJAX.

// ĐÚNG — chuyển hướng toàn trang
window.location.href = '/oauth2/authorization/google';
 
// SAI — sẽ gây lỗi CORS
fetch('/oauth2/authorization/google');

Vấn đề 4: Xung đột quản lý Session

Vấn đề: Luồng OAuth2 cần session để lưu authorization request, nhưng API của bạn là stateless.

Giải pháp: Sử dụng HttpSessionOAuth2AuthorizationRequestRepository tạm thời trong luồng OAuth2, nhưng giữ API stateless với JWT:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
        throws Exception {
 
    http
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .oauth2Login(oauth2 -> oauth2
            .authorizationEndpoint(auth -> auth
                .authorizationRequestRepository(
                    new HttpSessionOAuth2AuthorizationRequestRepository()))
            // ...
        );
 
    return http.build();
}

Lưu ý: Để OAuth2 hoàn toàn stateless, hãy cân nhắc sử dụng lưu trữ authorization request dựa trên cookie.


Tóm tắt và những điểm chính

✅ Luồng OAuth2 Authorization Code là tiêu chuẩn cho đăng nhập mạng xã hội — giữ token phía server
✅ Spring Boot tự động cấu hình Google và GitHub — chỉ cần thêm thông tin đăng nhập trong application.yml
✅ CustomOAuth2UserService ánh xạ dữ liệu nhà cung cấp sang mô hình người dùng và xử lý đăng ký
✅ Liên kết tài khoản cho phép người dùng kết nối nhiều nhà cung cấp vào một tài khoản
✅ Kết hợp OAuth2 với JWT — OAuth2 xác thực, sau đó ứng dụng cấp JWT cho truy cập API
✅ OAuth2SuccessHandler là cầu nối bằng cách cấp JWT sau đăng nhập mạng xã hội thành công
✅ Tích hợp frontend sử dụng chuyển hướng toàn trang, không phải AJAX, cho luồng OAuth2
✅ Annotation @CurrentUser cung cấp cách truy cập người dùng đã xác thực trong controller
✅ Test với oauth2Login() mock từ Spring Security Test cho integration testing
✅ Bảo mật production yêu cầu HTTPS, xác thực redirect URI, và quản lý thông tin đăng nhập đúng cách


Tiếp theo là gì?

Bây giờ bạn đã thêm đăng nhập mạng xã hội vào ứng dụng Spring Boot, hãy tiếp tục xây dựng:

Tiếp tục chuỗi Spring Boot

  • Kiểm thử nâng cao: Học contract testing và performance testing để đảm bảo luồng OAuth2 sẵn sàng cho production
  • Triển khai Docker & Kubernetes: Container hóa ứng dụng với quản lý secrets phù hợp cho thông tin OAuth2
  • Microservices với Spring Cloud: Triển khai xác thực OAuth2 tập trung trên nhiều services

Bài viết Spring Boot liên quan

Bài viết nền tảng


Thuộc chuỗi Lộ trình học Spring Boot

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.