知識庫 / Spring / Spring Security RSS 訂閱

Spring Security 與 Firebase 身份驗證集成

Spring Security
HongKong
6
10:58 AM · Dec 06 ,2025

1. 概述

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

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

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

2. 項目設置

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

2.1. 依賴項

讓我們首先將 Firebase Admin 依賴項 添加到我們項目的 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。這使得我們可以在使用多個 Firebase 服務時重用 FirebaseApp Bean,例如 Cloud Firestore 數據庫、Firebase 消息傳遞等。

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

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

現在我們已經定義了我們的 FirebaseAuth Bean,接下來我們創建一個 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() 方法中,我們使用用户提供的 emailpassword 初始化一個新的 CreateRequest 對象。 我們還將 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 文件中,並使用 @Value 註解將其注入到我們新的 FirebaseAuthClient 類中:

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 對象,其中包含用户的 emailpassword,並將 returnSecureToken 設置為 true。然後我們將該請求傳遞給我們的私有 sendSignInRequest() 方法,該方法使用 RestClient 向 Firebase Authentication REST API 發送 POST 請求。

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

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

5. 交換刷新令牌獲取新的身份令牌

現在我們已經完成了登錄功能,讓我們看看如何使用 refreshToken 獲取新的 idToken,噹噹前令牌過期時。 這樣,我們的客户端應用程序就可以在不要求用户重新輸入憑據的情況下,延長用户的登錄狀態。

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

record RefreshTokenRequest(String grant_type, String refresh_token) {}

record RefreshTokenResponse(String id_token) {}

接下來,在我們的 FirebaseAuthClient 類中,我們調用 刷新令牌 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

此外,如果我們需要強制用户重新認證,我們可以撤銷他們的 refresh tokens

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 聲明。

如果 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 端點,這些端點對應於我們的用户註冊、登錄和刷新令牌交換功能。

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

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

7. 結論

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

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

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

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

發佈 評論

Some HTML is okay.