在前后端分离的项目中,认证一直是绕不开的话题。前端使用 Next.js 时,NextAuth.js 几乎是事实上的认证标准库,它支持 GitHub、Google 等第三方登录,也支持 Credentials 邮箱密码登录。但真正让不少团队头疼的是:NextAuth.js 签发的令牌,后端 Spring Boot 如何验证?两边如何共享密钥、如何传递角色信息、如何处理过期与刷新?本文就围绕这套组合,给出一套可以直接落地的整合方案。

一、整体架构与认证流程设计
在动手写代码之前,先把整体流程理清楚。这套方案的核心思路是:NextAuth.js 在登录成功后签发一个 JWT 令牌,前端把它保存在会话中,每次请求后端 API 时通过 Authorization: Bearer xxx 头携带,Spring Boot 端用相同的密钥解析并校验这个令牌,校验通过后从令牌的 Claims 中取出用户身份和角色,构造 SecurityContext 完成鉴权。
这个流程中有两个关键点需要提前确定。第一是密钥归属:推荐由 NextAuth.js 负责签发,Spring Boot 只负责校验,这样登录逻辑全部收敛在前端认证层,后端保持无状态。第二是令牌内容的约定:JWT 的 payload 部分两端必须一致,比如 sub 存用户 ID、role 存角色、exp 存过期时间,这些字段名要在开发前就定好,避免后期联调时反复改动。
还有一点容易被忽略:NextAuth.js 默认的会话策略是 database session,也就是把会话存在数据库里,这种方式签发的 cookie 后端没法解析。要实现前后端共享令牌,必须显式把 session strategy 配置为 jwt,这是整个整合方案的前提条件,很多联调失败的问题最后排查出来都是栽在这一步。
二、NextAuth.js 端配置与 JWT 签发
先在前端完成 NextAuth.js 的配置。安装依赖后,在项目根目录新建 auth.js(v5 版本写法)或在 pages/api/auth 下新建 [...nextauth].js(v4 版本写法)。这里以 Credentials 登录加 JWT 策略为例:
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
export const { handlers, auth, signIn, signOut } = NextAuth({
// 使用 JWT 会话策略,这是与 Spring Boot 对接的前提
session: {
strategy: "jwt",
maxAge: 60 * 60, // 令牌有效期 1 小时
},
// 密钥必须与 Spring Boot 端共享,建议放环境变量
secret: process.env.NEXTAUTH_SECRET,
providers: [
Credentials({
credentials: {
username: {},
password: {},
},
async authorize(credentials) {
// 这里调用你的用户服务校验账号密码
const user = await verifyUser(credentials.username, credentials.password)
if (!user) return null
return { id: user.id, name: user.name, role: user.role }
},
}),
],
callbacks: {
async jwt({ token, user }) {
// 首次登录时把角色信息写入令牌
if (user) {
token.role = user.role
token.sub = user.id
}
return token
},
async session({ session, token }) {
session.user.id = token.sub
session.user.role = token.role
return session
},
},
})配置里有几处值得注意。NEXTAUTH_SECRET 是签名密钥,生产环境务必用强随机字符串,可以用 openssl rand -base64 32 生成,并且通过环境变量注入而不是硬编码。NextAuth.js 默认使用 HS256 对称加密算法签名,这意味着 Spring Boot 端需要持有同一把密钥。
另一个细节是 jwt 回调。默认情况下 NextAuth.js 签发的令牌里包含 name、email、picture 等标准字段,业务字段比如角色需要在这个回调里手动塞进去。如果你想在后端做 RBAC 权限控制,就一定要在登录时把 role 写入 token,否则后端解析出来的令牌里根本没有这个字段。
前端调用后端接口时,最简单的方式是使用 auth() 方法获取令牌并拼接请求头:
import { auth } from "@/auth"
export async function GET() {
const session = await auth()
const res = await fetch("http://api.ipipp.com/orders", {
headers: {
Authorization: `Bearer ${session?.accessToken ?? ""}`,
},
})
return Response.json(await res.json())
}这种方式通过服务端转发请求,浏览器拿不到原始令牌,安全性更好。如果是纯客户端组件直接请求后端,则需要借助中间件把令牌暴露出来,但要注意避免令牌落入 localStorage,防止 XSS 攻击窃取。
三、Spring Boot 端校验 JWT 令牌
后端推荐直接使用 jjwt 库来解析令牌,配合 Spring Security 的过滤器链完成无状态认证。先引入依赖:
<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>接着写一个 JWT 工具类,负责解析和验证令牌。密钥同样放在配置文件中,与前端保持一致:
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
public class JwtUtil {
private final SecretKey key;
public JwtUtil(String secret) {
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public Claims parse(String token) {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
}
}然后在 Spring Security 配置中注册一个过滤器,拦截需要认证的请求:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
try {
Claims claims = jwtUtil.parse(header.substring(7));
String userId = claims.getSubject();
String role = claims.get("role", String.class);
// 构造无密码的认证对象,角色加上 ROLE_ 前缀
var authorities = List.of(new SimpleGrantedAuthority("ROLE_" + role));
var authentication = new UsernamePasswordAuthenticationToken(
userId, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JwtException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效的令牌");
return;
}
}
chain.doFilter(request, response);
}
}安全配置部分,把会话策略设为无状态,并把自定义过滤器插入到 UsernamePasswordAuthenticationFilter 之前。这样后端完全不依赖 Session,水平扩展时也不需要会话粘滞。鉴权规则可以按角色设置,比如管理接口要求 hasRole('ADMIN'),注意 jjwt 解析出来的 role 声明要与前端写入的字段名完全一致,否则拿到的永远是 null,这也是联调时的高频问题。
四、常见坑与生产环境建议
整合过程中的坑主要集中在几个方面。第一个是密钥长度问题:HS256 要求密钥至少 32 字节,如果 NEXTAUTH_SECRET 太短,jjwt 会在运行时抛出 WeakKeyException,生成密钥时直接用足够长度的随机串即可。第二个是时钟偏移:前后端部署在不同机器上时,如果时间不同步,可能出现令牌刚签发就被判定为未生效的情况,运维层面要保证 NTP 时间同步,代码层面也可以在解析时配置 30 秒左右的时钟容差。
第二个方面是跨域配置。前后端分离部署时,如果浏览器直接请求 Spring Boot 接口,需要在后端配置 CORS,允许前端域名、允许携带 Authorization 头,同时注意 CORS 预检请求不能被认证过滤器拦截,否则预检失败会导致业务请求根本发不出去。更稳妥的做法是像前面演示的那样,通过 Next.js 的 API Route 服务端转发,从架构上规避跨域问题。
第三个方面是令牌刷新。JWT 无状态的代价是不容易主动作废,常见的折中方案是短有效期加刷新令牌:访问令牌设为一小时,刷新令牌单独签发且有效期更长,后端为刷新令牌维护一个可撤销的记录。另外,如果业务上有强制下线的需求,可以在 Redis 中维护一个用户令牌版本的哈希表,解析 JWT 后额外比对版本号,版本不一致则拒绝,这样在无状态架构下也能实现类似会话注销的效果。掌握这些细节后,这套 Spring Boot 加 NextAuth.js 的组合就足以支撑绝大多数中大型项目的认证需求了。
Spring BootNextAuth.jsJWT认证修改时间:2026-09-11 18:30:48