Supabase 是近几年很受欢迎的开源 BaaS 平台,它基于 PostgreSQL 提供 databases、auth、storage 等一整套后端能力,其中 Auth 模块内置了邮箱密码、手机验证码、OAuth 第三方登录、Magic Link 等多种认证方式,并且统一签发 JWT 令牌。对于 Spring Boot 开发者来说,把认证这一块交给 Supabase,自己只专注业务接口开发,能省掉大量造轮子的时间。这篇文章就从认证原理讲起,完整演示 Spring Boot 接入 Supabase Auth 的全过程。

一、Supabase Auth 的认证原理与整体架构
Supabase Auth 的核心是 GoTrue 服务,它负责处理用户的注册、登录、邮箱验证、令牌签发等所有认证相关的逻辑。当用户通过 Supabase 提供的 SDK 完成登录后,服务端会返回三个关键令牌:access_token、refresh_token 和一个已过期的 access_token。其中 access_token 本质上就是一个标准的 JWT(JSON Web Token),里面包含了用户的 UUID、邮箱、角色等声明(claims)。
这个 JWT 是使用 Supabase 项目的 JWT Secret 签名的(新项目默认使用非对称密钥,通过 JWKS 端点公开公钥)。也就是说,我们的 Spring Boot 应用不需要每次请求都去调 Supabase 的接口验证用户身份,只需要本地校验 JWT 的签名和有效期即可,这就是典型的无状态认证。整个流程可以概括为三步:客户端调用 Supabase Auth 完成登录拿到 access_token;之后每次请求业务接口时把令牌放在 Authorization 头里带上;Spring Boot 后端校验令牌签名,解析出用户身份后处理业务逻辑。
JWT 的 payload 中比较重要的字段有 sub(用户唯一标识)、email、role(默认是 authenticated)、exp(过期时间戳,access_token 默认一小时过期)。正因为 access_token 有效期较短,客户端还需要配合 refresh_token 定期刷新令牌,这一点在后面进阶部分会详细说明。理解了这套令牌机制,后面的整合工作其实就是围绕两个核心展开:一是封装调用 Supabase Auth API 的客户端,二是在 Spring Security 里搭建 JWT 校验过滤器。
二、创建 Supabase 项目并封装认证客户端
先到 Supabase 官网创建一个项目,进入项目设置页面的 API 分类,记录下 Project URL(形如 https://xxxx.supabase.co)和 anon key。这两个值稍后会配置到 Spring Boot 的配置文件中。需要注意 anon key 是可以暴露在前端的公开密钥,真正需要保密的是 service_role key,它拥有绕过 RLS 策略的权限,绝对不能泄露到客户端。
接下来在 Spring Boot 项目中引入必要的依赖。Supabase 官方的 Java 生态相对薄弱,社区提供的 supabase-java 功能也不算完善,因此更推荐直接用 RestClient(Spring 6 提供的 HTTP 客户端)封装 Auth API,这样可控性更强:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>在 application.yml 中写入 Supabase 相关配置,然后创建一个配置类读取这些属性:
supabase: url: https://xxxx.supabase.co anon-key: 你的anonKey jwt-secret: 你的JWTSecret # 旧项目使用对称密钥时配置
下面是认证客户端的核心实现,封装了注册、登录、刷新令牌三个最常用的操作。Supabase Auth 的 REST 接口都挂在 /auth/v1 路径下,请求头需要带上 apikey 字段:
@Configuration
public class SupabaseConfig {
@Value("${supabase.url}")
private String supabaseUrl;
@Value("${supabase.anon-key}")
private String anonKey;
@Bean
public RestClient supabaseRestClient() {
return RestClient.builder()
.baseUrl(supabaseUrl)
.defaultHeader("apikey", anonKey)
.defaultHeader("Content-Type", "application/json")
.build();
}
}
@Service
public class SupabaseAuthService {
private final RestClient restClient;
public SupabaseAuthService(RestClient supabaseRestClient) {
this.restClient = supabaseRestClient;
}
// 用户注册,邮箱验证流程由 Supabase 自动处理
public Map signUp(String email, String password) {
Map<String, String> body = Map.of("email", email, "password", password);
return restClient.post()
.uri("/auth/v1/signup")
.body(body)
.retrieve()
.body(Map.class);
}
// 邮箱密码登录,返回 access_token 和 refresh_token
public Map signInWithPassword(String email, String password) {
Map<String, String> body = Map.of("email", email, "password", password);
return restClient.post()
.uri("/auth/v1/token?grant_type=password")
.body(body)
.retrieve()
.body(Map.class);
}
// 使用 refresh_token 换取新的 access_token
public Map refreshToken(String refreshToken) {
Map<String, String> body = Map.of("refresh_token", refreshToken);
return restClient.post()
.uri("/auth/v1/token?grant_type=refresh_token")
.body(body)
.retrieve()
.body(Map.class);
}
}注册接口返回的 Map 中会包含 session 信息,如果项目开启了邮箱验证,则只在用户信息里返回,不会直接签发令牌。此时 Supabase 会给用户邮箱发送一封验证邮件,用户点击链接后 email_confirmed_at 字段才会被填充。如果是在本地开发环境测试,可以在 Supabase 控制台的 Auth 设置里暂时关闭邮箱验证开关。
三、用 Spring Security 校验 Supabase 签发的 JWT
客户端拿到令牌后,每次请求都把它放进请求头:Authorization: Bearer eyJhbGciOi...。服务端要做的就是解析并验证这个 JWT。以旧版对称密钥(HS256)为例,我们可以用 jjwt 库结合 Spring Security 的过滤器来实现:
@Component
public class SupabaseJwtFilter extends OncePerRequestFilter {
private final SecretKey key;
public SupabaseJwtFilter(@Value("${supabase.jwt-secret}") String secret) {
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
Claims claims = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
String userId = claims.getSubject();
String email = claims.get("email", String.class);
// 把用户信息塞进 SecurityContext,业务代码随时可取
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userId, email, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "invalid token");
return;
}
}
chain.doFilter(request, response);
}
}然后在 Security 配置类中注册这个过滤器,并放开注册登录相关的公开端点:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final SupabaseJwtFilter jwtFilter;
public SecurityConfig(SupabaseJwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}写一个简单的业务接口验证效果。因为过滤器已经把用户 ID 放进了 SecurityContext,控制器里通过 @AuthenticationPrincipal 就能直接拿到当前登录用户:
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/me")
public Map<String, Object> me(@AuthenticationPrincipal Object userId) {
return Map.of("userId", userId, "message", "认证成功");
}
}用 curl 或者 Postman 测试:先调用登录接口从返回值里复制 access_token,再请求 /api/me 并带上 Authorization 头,能正常返回 userId 就说明整条链路通了。
四、令牌刷新与几个进阶注意事项
access_token 默认一小时过期,客户端不应该让用户频繁重新输密码,而是用 refresh_token 静默换新。常见做法有两种:一是前端在拦截器里检测到 401 就自动调用刷新接口,刷新成功后重发原请求;二是在 Supabase 的 Auth 设置里开启自动刷新会话。如果你们的前端用的是 supabase-js,它的 onAuthStateChange 回调可以监听到令牌刷新事件,配合起来非常方便。
关于密钥类型要特别留意:Supabase 新建项目已经默认采用非对称密钥(ES256 算法),JWT 头部会指向一个 JWKS 端点(https://xxxx.supabase.co/auth/v1/.well-known/jwks.json)。此时服务端不能用对称密钥去验证,而是要在启动时或者首次请求时拉取公钥,用 nimbus-jose-jwt 的 RemoteJWKSSource 可以优雅地处理这件事,公钥还会自动缓存和轮换。如果项目还在用老的对称密钥,建议尽快在控制台迁移到非对称模式,安全性更高。
最后提几个实践建议。第一,不要把 service_role key 写进任何会打包给客户端的代码里,它只在服务端需要绕过行级安全策略时使用。第二,建议在业务表里冗余一份用户基本信息(比如昵称、头像),通过 Supabase 的 Auth Webhook 或在用户注册时同步,避免业务查询频繁跨服务取用户资料。第三,Supabase Auth 支持配置 OAuth 提供商(GitHub、Google 等),接入方式同样只是多了几个重定向接口,Spring Boot 这边的 JWT 校验逻辑完全不用改,因为无论哪种登录方式,最终签发的令牌格式都是统一的。这也是 BaaS 认证最大的好处:认证方式的扩展对业务后端几乎零侵入。
Spring BootSupabase AuthBaaS认证修改时间:2026-09-13 01:28:48