1. 概述

在现代 Web 应用中,用户认证和授权是核心组件。从零构建认证层既复杂又耗时,但随着云认证服务的兴起,这个过程变得简单多了。

Firebase Authentication 就是典型代表——这是 Firebase 和 Google 提供的全托管认证服务。

本教程将探讨如何将 Firebase Authentication 与 Spring Security 集成,实现用户创建和认证功能。我们将逐步完成必要配置、实现用户注册登录、并创建自定义认证过滤器来验证私有接口的访问令牌。

2. 项目初始化

开始编码前,需先添加 SDK 依赖并正确配置应用。

2.1. 依赖配置

pom.xml 中添加 Firebase Admin SDK 依赖:

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

该依赖提供了与 Firebase Authentication 服务交互所需的核心类。

2.2. 定义 Firebase 配置 Bean

要连接 Firebase Authentication,需配置私钥进行 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。这样设计便于复用 FirebaseApp 实例(如集成 Cloud FirestoreFirebase Messaging 等服务)。

FirebaseAuth 类是与 Firebase Authentication 服务交互的主入口。

3. 在 Firebase Authentication 中创建用户

现在有了 FirebaseAuth Bean,创建 UserService 类实现用户注册:

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("该邮箱已被注册");
        }
        throw exception;
    }
}

create() 方法中:

  • 用用户邮箱和密码初始化 CreateRequest
  • 简单起见直接设置 emailVerified=true,生产环境应先实现邮箱验证流程
  • 捕获邮箱重复异常并抛出自定义 AccountAlreadyExistsException

4. 实现用户登录功能

用户创建后,需提供登录机制才能访问私有接口。**登录成功后将返回 JWT 格式的 ID 令牌和刷新令牌**。

Firebase Admin SDK 不支持邮箱密码换令牌(通常由客户端处理),演示中我们直接在后端调用登录 REST API

先定义请求/响应的 Record 类:

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

record FirebaseSignInResponse(String idToken, String refreshToken) {}

**调用 Firebase REST API 需要项目 Web API 密钥**。将其存入 application.yaml 并注入 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("登录凭证无效");
        }
        throw exception;
    }
}

关键逻辑:

  • login() 构造请求体(returnSecureToken=true 表示需要返回令牌)
  • sendSignInRequest() 通过 RestClient 发送 POST 请求
  • 成功时返回包含 idTokenrefreshToken 的响应
  • 凭证无效时抛出 InvalidLoginCredentialsException

⚠️ 注意:Firebase 返回的 idToken 有效期固定为 1 小时且不可修改。下一节将介绍如何用 refreshToken 获取新令牌。

5. 用刷新令牌换取新 ID 令牌

登录功能就绪后,看看如何用 refreshTokenidToken 过期时获取新令牌。这样客户端可保持用户长期登录状态,无需重复输入凭证。

先定义请求/响应的 Record 类:

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("刷新令牌无效");
        }
        throw exception;
    }
}

核心流程:

  • exchangeRefreshToken() 构造请求体(grant_type 固定为 refresh_token
  • 发送 POST 请求到指定接口
  • 成功时返回新 id_token
  • 刷新令牌无效时抛出 InvalidRefreshTokenException

若需强制用户重新登录,可撤销其刷新令牌

firebaseAuth.revokeRefreshTokens(userId);

调用 FirebaseAuth 提供的 revokeRefreshTokens() 方法会:

  • 使该用户所有 refreshToken 失效
  • 同时使当前 idToken 失效
  • 实现强制登出效果

6. 集成 Spring Security

用户创建和登录功能已就绪,现在将 Firebase Authentication 与 Spring Security 集成,保护私有接口。

6.1. 创建自定义认证过滤器

继承 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;

    // 标准构造器

    @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,
          "认证失败:令牌缺失、无效或已过期");
        response.getWriter().write(objectMapper.writeValueAsString(problemDetail));
    }

}

关键逻辑拆解:

  1. 令牌提取:从 Authorization 头获取 JWT 并去除 Bearer 前缀
  2. 令牌验证extractUserIdFromToken() 方法验证令牌并提取 user_id 声明
  3. 错误处理:验证失败时返回 ProblemDetail 格式的 JSON 错误
  4. 认证设置:验证通过时,用 userId 创建 UsernamePasswordAuthenticationToken 并存入 SecurityContext

认证成功后,可在服务层通过以下代码获取用户 ID:

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;

// 标准构造器

@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 之前

此配置确保私有接口受保护,仅允许携带有效 JWT 令牌的请求访问

7. 总结

本文详细介绍了 Firebase Authentication 与 Spring Security 的集成方案。我们完成了:

  • ✅ 必要配置
  • ✅ 用户注册、登录、刷新令牌功能
  • ✅ 自定义 Spring Security 过滤器保护私有接口

通过 Firebase Authentication,我们将用户凭证和权限管理的复杂性转移给云服务,从而专注于核心业务开发。

本文所有代码示例可在 GitHub 获取。


原始标题:Integrating Firebase Authentication With Spring Security | Baeldung