Spring Security 與 Firebase 身份驗證集成

Spring Security
Remote
1
11:54 AM · Nov 30 ,2025

1. 概述

在現代 Web 應用程序中,用户身份驗證和授權是關鍵組成部分。從頭開始構建我們的身份驗證層是一個具有挑戰性和複雜性的任務。然而,隨着基於雲的身份驗證服務興起,這個過程變得更加簡單。

例如,Firebase 身份驗證 是一種由 Firebase 和 Google 提供的完全託管身份驗證服務。

在本教程中,我們將探索如何將 Firebase 身份驗證與 Spring Security 集成,以創建和驗證我們的用户。 我們將逐步完成必要的配置、實現用户註冊和登錄功能,並創建一個自定義身份驗證過濾器,以驗證用户令牌,用於私有 API 端點。

2. 項目設置

在深入實施之前,我們需要包含 SDK 依賴項並正確配置我們的應用程序。

2.1. 依賴項

首先,我們將 Firebase 管理者依賴項 添加到項目的 pom.xml 文件中:

<dependency>
    <groupId>com.google.firebase</groupId>
    <artifactId>firebase-admin</artifactId>
    <version>9.3.0</version>
</dependency>

此依賴項為我們提供了與應用程序交互的 Firebase 身份驗證服務所需的類。

2.2. 定義 Firebase 配置 Bean

為了與 Firebase 身份驗證交互,我們需要配置我們的 私鑰 以進行 API 請求的身份驗證。

對於我們的演示,我們將 private-key.json 文件放在項目的 src/main/resources 目錄中。 但是,在生產環境中,應從環境變量或密鑰管理系統加載私鑰,以提高安全性

我們將使用 @Value 註解加載私鑰並將其用於定義我們的 Bean:

@Value("classpath:/private-key.json")
private Resource privateKey;

@Bean
public FirebaseApp firebaseApp() {
    InputStream credentials = new ByteArrayInputStream(privateKey.getContentAsByteArray());
    FirebaseOptions firebaseOptions = FirebaseOptions.builder()
      .setCredentials(GoogleCredentials.fromStream(credentials))
      .build();
    return FirebaseApp.initializeApp(firebaseOptions);
}

@Bean
public FirebaseAuth firebaseAuth(FirebaseApp firebaseApp) {
    return FirebaseAuth.getInstance(firebaseApp);
}

我們首先定義我們的 FirebaseApp Bean,然後使用它來創建我們的 FirebaseAuth Bean。 這允許我們在與 Cloud Firestore Database、Firebase Messaging 等多個 Firebase 服務交互時重用 FirebaseApp Bean。

FirebaseAuth 類是與 Firebase 身份驗證服務交互的主要入口點。

3. 在 Firebase 身份驗證中創建用户

現在我們已經定義了 FirebaseAuth 豆,讓我們創建一個 UserService 類並將其引用以在 Firebase 身份驗證中創建新用户:

private static final String DUPLICATE_ACCOUNT_ERROR = "EMAIL_EXISTS";

public void create(String emailId, String password) {
    CreateRequest request = new CreateRequest();
    request.setEmail(emailId);
    request.setPassword(password);
    request.setEmailVerified(Boolean.TRUE);

    try {
        firebaseAuth.createUser(request);
    } catch (FirebaseAuthException exception) {
        if (exception.getMessage().contains(DUPLICATE_ACCOUNT_ERROR)) {
            throw new AccountAlreadyExistsException("Account with given email-id already exists");
        }
        throw exception;
    }
}

在我們的 create() 方法中,我們使用 CreateRequest 對象初始化用户 emailpassword我們還將 emailVerified 的值設置為 true 以簡化操作,但是,在生產應用程序中,我們可能需要實現一個電子郵件驗證過程

此外,我們處理了給定 emailId 的賬户已存在的情況,並拋出自定義 AccountAlreadyExistsException

4. 實現用户登錄功能

現在我們已經可以創建用户,自然需要允許他們在訪問我們的私有API端點之前進行身份驗證。我們將實現用户登錄功能,該功能將在成功身份驗證後返回一個ID令牌,形式為JWT和刷新令牌

Firebase admin SDK 不支持使用電子郵件/密碼憑據進行令牌交換,因為此功能通常由客户端應用程序處理。但是,為了演示目的,我們將直接調用sign-in REST API,從我們的後端應用程序中。

首先,我們將聲明一些記錄來表示請求和響應的數據載體:

record FirebaseSignInRequest(String email, String password, boolean returnSecureToken) {}

record FirebaseSignInResponse(String idToken, String refreshToken) {}

為了調用Firebase身份驗證REST API,我們需要我們的Firebase項目的Web API密鑰我們將將其存儲在我們的application.yaml文件中,並將其注入到我們新的FirebaseAuthClient類中,使用@Value註解:

private static final String API_KEY_PARAM = "key";
private static final String INVALID_CREDENTIALS_ERROR = "INVALID_LOGIN_CREDENTIALS";
private static final String SIGN_IN_BASE_URL = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword";

@Value("${com.baeldung.firebase.web-api-key}")
private String webApiKey;

public FirebaseSignInResponse login(String emailId, String password) {
    FirebaseSignInRequest requestBody = new FirebaseSignInRequest(emailId, password, true);
    return sendSignInRequest(requestBody);
}

private FirebaseSignInResponse sendSignInRequest(FirebaseSignInRequest firebaseSignInRequest) {
    try {
        return RestClient.create(SIGN_IN_BASE_URL)
          .post()
          .uri(uriBuilder -> uriBuilder
            .queryParam(API_KEY_PARAM, webApiKey)
            .build())
          .body(firebaseSignInRequest)
          .contentType(MediaType.APPLICATION_JSON)
          .retrieve()
          .body(FirebaseSignInResponse.class);
    } catch (HttpClientErrorException exception) {
        if (exception.getResponseBodyAsString().contains(INVALID_CREDENTIALS_ERROR)) {
            throw new InvalidLoginCredentialsException("Invalid login credentials provided");
        }
        throw exception;
    }
}

在我們的login()方法中,我們創建一個FirebaseSignInRequest對象,其中包含用户的emailpasswordreturnSecureToken設置為true。然後我們將此請求傳遞給我們的私有sendSignInRequest()方法,該方法使用RestClient向Firebase身份驗證REST API發送POST請求。

如果請求成功,我們將返回包含用户idTokenrefreshToken的響應,傳遞給調用者。如果登錄憑據無效,我們將拋出自定義InvalidLoginCredentialsException

請注意,我們從Firebase接收到的idToken的有效期限為1小時,並且我們無法更改它。在下一部分中,我們將探討如何允許我們的客户端應用程序使用返回的refreshToken來獲取新的ID令牌。

5. 交換刷新令牌以獲取新的 ID 令牌

現在我們已經完成了登錄功能,refreshToken 來獲取當前令牌過期時的新 idToken。這允許我們的客户端應用程序在不要求用户重新輸入憑據的情況下,保持用户長時間登錄。

我們將首先定義表示請求和響應負載的記錄:

record RefreshTokenRequest(String grant_type, String refresh_token) {}

record RefreshTokenResponse(String id_token) {}

接下來,在我們的 FirebaseAuthClient 類中,讓我們調用 refresh token exchange REST API

private static final String REFRESH_TOKEN_GRANT_TYPE = "refresh_token";
private static final String INVALID_REFRESH_TOKEN_ERROR = "INVALID_REFRESH_TOKEN";
private static final String REFRESH_TOKEN_BASE_URL = "https://securetoken.googleapis.com/v1/token";

public RefreshTokenResponse exchangeRefreshToken(String refreshToken) {
    RefreshTokenRequest requestBody = new RefreshTokenRequest(REFRESH_TOKEN_GRANT_TYPE, refreshToken);
    return sendRefreshTokenRequest(requestBody);
}

private RefreshTokenResponse sendRefreshTokenRequest(RefreshTokenRequest refreshTokenRequest) {
    try {
        return RestClient.create(REFRESH_TOKEN_BASE_URL)
          .post()
          .uri(uriBuilder -> uriBuilder
            .queryParam(API_KEY_PARAM, webApiKey)
            .build())
          .body(refreshTokenRequest)
          .contentType(MediaType.APPLICATION_JSON)
          .retrieve()
          .body(RefreshTokenResponse.class);
    } catch (HttpClientErrorException exception) {
        if (exception.getResponseBodyAsString().contains(INVALID_REFRESH_TOKEN_ERROR)) {
            throw new InvalidRefreshTokenException("Invalid refresh token provided");
        }
        throw exception;
    }
}

在我們的 exchangeRefreshToken 方法中,我們創建一個 RefreshTokenRequest,其中包含 refresh_token grant type 和提供的 refreshToken。然後我們將此請求傳遞給我們的私有 sendRefreshTokenRequest 方法,該方法向目標 API 端點發送 POST 請求。

如果請求成功,我們將返回包含新 idToken 的響應。如果提供的 refreshToken 無效,我們將拋出自定義 InvalidRefreshTokenException

firebaseAuth.revokeRefreshTokens(userId);

我們調用 revokeRefreshTokens 方法,該方法由 FirebaseAuth 類提供。它不僅無效化了用户發出的所有 refreshTokens,還無效化了用户的活動 idToken,從而將他們從我們的應用程序中註銷。

6. 集成 Spring Security

在我們已經實現的用户創建和登錄功能的基礎上,讓我們使用 Firebase Authentication 與 Spring Security 保護我們的私有 API 端點。

6.1. 創建自定義認證 Filter

首先,我們將創建一個擴展 OncePerRequestFilter 類的自定義認證過濾器:

@Component
class TokenAuthenticationFilter extends OncePerRequestFilter {

    private static final String BEARER_PREFIX = "Bearer ";
    private static final String USER_ID_CLAIM = "user_id";
    private static final String AUTHORIZATION_HEADER = "Authorization";

    private final FirebaseAuth firebaseAuth;
    private final ObjectMapper objectMapper;

    // standard constructor

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) {
        String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER);

        if (authorizationHeader != null && authorizationHeader.startsWith(BEARER_PREFIX)) {
            String token = authorizationHeader.replace(BEARER_PREFIX, "");
            Optional<String> userId = extractUserIdFromToken(token);

            if (userId.isPresent()) {
                var authentication = new UsernamePasswordAuthenticationToken(userId.get(), null, null);
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } else {
                setAuthErrorDetails(response);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }

    private Optional<String> extractUserIdFromToken(String token) {
        try {
            FirebaseToken firebaseToken = firebaseAuth.verifyIdToken(token, true);
            String userId = String.valueOf(firebaseToken.getClaims().get(USER_ID_CLAIM));
            return Optional.of(userId);
        } catch (FirebaseAuthException exception) {
            return Optional.empty();
        }
    }

    private void setAuthErrorDetails(HttpServletResponse response) {
        HttpStatus unauthorized = HttpStatus.UNAUTHORIZED;
        response.setStatus(unauthorized.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(unauthorized,
          "Authentication failure: Token missing, invalid or expired");
        response.getWriter().write(objectMapper.writeValueAsString(problemDetail));
    }

}

在我們的 doFilterInternal() 方法中,我們從傳入的 HTTP 請求中提取 Authorization 頭,並移除 Bearer 前綴以獲取 JWT token

然後,使用我們私有的 extractUserIdFromToken() 方法,我們驗證 token 的真實性並從其 user_id claim 中提取。

如果 token 驗證失敗,我們將創建一個 ProblemDetail 錯誤響應,使用 ObjectMapper 將其轉換為 JSON,並將其寫入 HttpServletResponse

如果 token 有效,我們將創建一個 UsernamePasswordAuthenticationToken 的新實例,其中 userId 作為 Principal,然後將其設置到 SecurityContext

在成功認證後,我們可以從 SecurityContext 中檢索已認證用户的 userId ,在我們的服務層:

String userId = Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
  .map(Authentication::getPrincipal)
  .filter(String.class::isInstance)
  .map(String.class::cast)
  .orElseThrow(IllegalStateException::new);

為了遵循單一職責原則,我們可以將我們的上述邏輯放在一個單獨的 AuthenticatedUserIdProvider 類中。 這有助於服務層保持已認證用户與他們執行的操作之間的關係。

6.2. 配置 SecurityFilterChain

最後,讓我們配置我們的 SecurityFilterChain 以使用我們的自定義認證過濾器:

private static final String[] WHITELISTED_API_ENDPOINTS = { "/user", "/user/login", "/user/refresh-token" };

private final TokenAuthenticationFilter tokenAuthenticationFilter;

// standard constructor

@Bean
public SecurityFilterChain configure(HttpSecurity http) {
    http
      .authorizeHttpRequests(authManager -> {
        authManager.requestMatchers(HttpMethod.POST, WHITELISTED_API_ENDPOINTS)
          .permitAll()
          .anyRequest()
          .authenticated();
      })
      .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();}

我們允許未身份驗證訪問 /user/user/login/user/refresh-token 端點,這些端點對應於我們的用户註冊、登錄和刷新令牌交換功能。

最後,我們將我們的自定義 TokenAuthenticationFilterUsernamePasswordAuthenticationFilter 之前添加到過濾器鏈中。

此設置確保了我們的私有 API 端點受到保護,並且僅允許具有有效 JWT 令牌的請求訪問它們

7. 結論

在本文中,我們探討了如何將 Firebase 身份驗證與 Spring Security 集成。

我們完成了必要的配置,實現了用户註冊、登錄和刷新令牌交換功能,並創建了一個自定義 Spring Security 過濾器來保護我們的私有 API 端點。

通過使用 Firebase 身份驗證,我們可以將管理用户憑據和訪問的複雜性外包出去,從而使我們能夠專注於構建核心功能。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.