You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular+NestJS+Auth0鉴权报错:Jwt Malformed排查求助

问题:Angular客户端发送JWT至NestJS服务器时触发'Jwt Malformed'错误

项目背景

使用Angular、NestJS和PostgreSQL开发全栈项目,通过Auth0实现认证(Authn)与授权(Authz)。数据库存储用户数据时保存Auth0返回的唯一sub ID,核心逻辑为:客户端登录后请求服务器数据时,服务器校验JWT中的sub与数据库中的sub是否匹配。

当前问题

客户端已获取并发送JWT至服务器,但NestJS的JwtAuthGuard抛出Jwt Malformed错误。手动在Postman中传入同一个JWT时请求正常,可完整获取数据,因此怀疑问题出在客户端代码。


相关代码

JwtAuthGuard代码

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  handleRequest(
    ...args: Parameters<
      InstanceType<ReturnType<typeof AuthGuard>>['handleRequest']
    >
  ) {
    console.log(args);
    return super.handleRequest(...args);
  }
}

报错信息

[
  null,
  false,
  JsonWebTokenError: jwt malformed
      at module.exports [as verify] (C:\Projects\dnd-server\node_modules\jsonwebtoken\verify.js:70:17)
      at module.exports [as JwtVerifier] (C:\Projects\dnd-server\node_modules\passport-jwt\lib\verify_jwt.js:4:16)
      at C:\Projects\dnd-server\node_modules\passport-jwt\lib\strategy.js:104:25
      at JwtStrategy.secretProvider [as _secretOrKeyProvider] (C:\Projects\dnd-server\node_modules\jwks-rsa\src\integrations\passport.js:42:14)
      at JwtStrategy.authenticate (C:\Projects\dnd-server\node_modules\passport-jwt\lib\strategy.js:99:10)
      at attempt (C:\Projects\dnd-server\node_modules\passport\lib\middleware\authenticate.js:378:16)
      at authenticate (C:\Projects\dnd-server\node_modules\passport\lib\middleware\authenticate.js:379:7)
      at C:\Projects\dnd-server\node_modules\@nestjs\passport\dist\auth.guard.js:88:3
      at new Promise (<anonymous>)
      at C:\Projects\dnd-server\node_modules\@nestjs\passport\dist\auth.guard.js:80:83,
  ExecutionContextHost {
    args: [ [IncomingMessage], [ServerResponse], [Function: next] ],
    constructorRef: [class CharacterController],
    handler: [AsyncFunction: findOne],
    contextType: 'http',
    getRequest: [Function: getRequest],
    getResponse: [Function: getResponse],
    getNext: [Function: getNext]
  },
  undefined
]

Auth策略代码(auth.strategy.ts)

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import * as jwksRsa from 'jwks-rsa';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      // 从Authorization头提取JWT
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),

      // 验证token的issuer和audience
      issuer: `https://${process.env.AUTH0_DOMAIN}/`,
      audience: process.env.AUTH0_AUDIENCE,

      // 使用Auth0公钥验证签名
      secretOrKeyProvider: jwksRsa.passportJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
      }),

      algorithms: ['RS256'],
    });
  }

  async validate(payload: any) {
    return {
      sub: payload.sub, // 返回sub声明
    };
  }
}

角色控制器代码(character.controller.ts)

import {
  Body,
  Controller,
  Get,
  Param,
  Post,
  Req,
  UseGuards,
} from '@nestjs/common';
import { CharacterService } from './character.service';
import { Character } from './character.model';
import { JwtAuthGuard } from 'src/auth/auth.guard';

@Controller('character')
export class CharacterController {
  constructor(private readonly characterService: CharacterService) {}

  @Post()
  async create(@Body() character: Character): Promise<Character> {
    const createdCharacter = await this.characterService.create(character);
    console.log('Character created successfully:', createdCharacter);
    return createdCharacter;
  }

  @Get(':id')
  @UseGuards(JwtAuthGuard)
  async findOne(@Param('id') id: string, @Req() req): Promise<Character> {
    const sub = req.user?.sub;
    if (!sub) {
      console.error("The 'sub' parameter is undefined or null.");
      throw new Error("Invalid 'sub' parameter");
    }

    console.log('Server SUB:', sub);

    const character = await this.characterService.findOne(+id);
    if (!character) {
      console.error(`Character with ID ${id} not found.`);
      throw new Error('Character not found');
    }

    if (character.sub !== sub) {
      console.error(
        `Server SUB ${sub} does not match character sub ${character.sub}.`,
      );
      throw new Error('Unauthorized access');
    }

    return character;
  }
}

Angular客户端代码(creator.component.ts)

saveFormDataToFile() {
    const data = this.characterCreationForm.value;
    this.http.post("http://localhost:3000/character", data).subscribe(
      (response) => {
        console.log("Form data saved successfully:", response);
      },
      (error) => {
        console.error("Error saving form data:", error);
      }
    );
  }

  retrieveFormDataFromServer(id: number) {
    const authToken = this.auth.idTokenClaims$;

    authToken.subscribe((token) => {
      if (token) {
        const headers = new HttpHeaders().set(
          "Authorization",
          `Bearer ${token.__raw}`
        );

        console.log("FULL TOKEN:", token);
        console.log("Access token retrieved successfully:", token.__raw);
        this.http
          .get(`http://localhost:3000/character/${id}`, { headers })
          .subscribe(
            (response: any) => {
              console.log("Form data retrieved successfully:", response);
              this.characterCreationForm.patchValue(response);
            },
            (error) => {
              console.error("Error retrieving form data:", error);
            }
          );
      } else {
        console.error("Error retrieving access token.");
      }
    });
  }

排查与解决方案

1. 替换ID Token为Access Token

Auth0的ID Token仅用于客户端身份验证,访问API必须使用Access Token。当前客户端代码使用idTokenClaims$获取的是ID Token,这是核心问题。修改客户端代码,改用getAccessTokenSilently()获取Access Token:

async retrieveFormDataFromServer(id: number) {
  try {
    // 获取用于API访问的Access Token
    const accessToken = await this.auth.getAccessTokenSilently();
    const headers = new HttpHeaders().set(
      "Authorization",
      `Bearer ${accessToken}`
    );

    this.http.get(`http://localhost:3000/character/${id}`, { headers })
      .subscribe(
        (response: any) => {
          console.log("Form data retrieved successfully:", response);
          this.characterCreationForm.patchValue(response);
        },
        (error) => {
          console.error("Error retrieving form data:", error);
        }
      );
  } catch (error) {
    console.error("获取Access Token失败:", error);
  }
}

2. 验证JWT格式与请求头

  • 检查客户端打印的token.__raw(或修改后的accessToken)是否为标准三段式JWT字符串(header.payload.signature),无多余空格或特殊字符。
  • 确认请求头格式正确:Authorization: Bearer <token>,其中Bearer后必须有一个空格。

3. 配置NestJS CORS确保头传递

确保NestJS服务器允许Angular客户端传递Authorization头,在main.ts中添加CORS配置:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 配置CORS
  app.enableCors({
    origin: "http://localhost:4200", // 替换为你的Angular端口
    allowedHeaders: ["Authorization", "Content-Type"],
    credentials: true,
  });
  await app.listen(3000);
}
bootstrap();

4. 验证Auth0配置一致性

确保NestJS的AUTH0_AUDIENCE环境变量与Auth0中API的标识符一致,AUTH0_DOMAIN与Auth0域名一致,避免因配置不匹配导致token验证失败。


内容的提问来源于stack exchange,提问作者Andriy Panasiuk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 07:39:53