全栈开发

nextjs 解决登录认证失败跳转登录问题

问题描述,nextjs区分server端以及client端,server端可以直接携带token访问接口,如果这时候token过期失效了,那么需要跳转到登录页面。另外在浏览器页面一个页面可能有很多client端接口访问api,如果token过期失效了,完成了一部分剩下了一部分如何跳转到登录页,以上跳转登录页还要给个提示说明跳转原因。

解决办法:

针对server端,在layout中全局获取用户信息,获取不到就跳转到登录页面,并附带原因

export default async function DashboardLayout({ children }: { children: ReactNode }) {
  // 服务端获取当前登录用户信息
  const user = await fetchCurrentUser();
  if(!user) {
    redirect("/login?status=unauthenticated")
  }
  ...
}

proxy.ts处理

// 未登录:重定向到登录页
if (!accessToken) {
    const loginUrl = new URL('/login', req.url);
    loginUrl.searchParams.set('next', path);
    loginUrl.searchParams.set('status', 'unauthenticated');
    return NextResponse.redirect(loginUrl); // 直接传 URL 对象即可
}

针对client端,拦截401错误,刷新页面,触发页面重新加载,如果有token但是无效,会被server端layout拦截并跳转到登录页面,如果token过期了没了,那么会被中间件proxy拦截并跳转到登录页面

export async function requestAction(
    context: RequestContext,
    path: string,
    init?: RequestInit
): Promise<ApiResponseType> {
    let response: Response;

    try {
        if (context === 'server') {
            response = await fetchWithAuth(path, init);
        } else {
            // client: 调用 /api/proxy/... 路由(由调用方确保 path 正确)
            response = await fetch(path, {
                ...init,
                headers: {
                    'Content-Type': 'application/json',
                    ...init?.headers,
                },
            });
        }
        
        if (response.status === 401 && context === 'client') {
            window.location.reload();
        }
        ...
}

登录页面消息处理

export default function Page() {
  const searchParams = useSearchParams();
  const status = searchParams.get("status")
  useEffect(() => {
    if (status === "unauthenticated") {
      toast.message("登录已过期,请重新登录。", {
        description: "您的登录凭证已失效,请输入账号密码重新登录。",
        duration: 3000,
        icon: "🔒"
      });
    }
  }, [status]);
  ...
}

springboot jwt验证修改:

public class JwtValidator extends OncePerRequestFilter {
    // 定义哪些路径需要 JWT(和 SecurityConfig 保持一致)
    private static final AntPathRequestMatcher PROTECTED_PATHS =
            new AntPathRequestMatcher("/api/**");

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        boolean needsAuth = PROTECTED_PATHS.matches(request);
        // 从请求头中提取 JWT
        String jwt = request.getHeader(JwtConstant.JWT_HEADER);
        if (needsAuth && (jwt == null || !jwt.startsWith("Bearer "))) {
            // 主动拒绝:返回 401
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"message\":\"认证缺失,请重新登录。\"}");
            return; // 不再继续
        }
        if (jwt != null) {
            //Bearer jwt 去掉 "Bearer " 前缀
            jwt = jwt.substring(7);
            try {
                ...
            } catch (Exception e) {
                // 直接返回 401,不要抛异常!
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write("{\"message\":\"无效或过期的认证凭证,请重新登录。\"}");
                return; // 注意:这里 return,不再继续 filterChain
            }
        }
        // 必须调用!
        // 表示“JWT 验证完成,继续处理请求”(比如进入 Controller)。
        filterChain.doFilter(request, response);
    }
}
  • 添加PROTECTED_PATHS 定义哪些路由需要jwt验证
  • 对需要jwt验证的页面校验jwt是否存在以及jwt是否有效