验证 JWT 令牌 IDX12741 时出错。 JWT:必须有三段(JWS)或者五段(JWE)

Error on validating JWT token IDX12741. JWT: must have three segments (JWS) or five segments (JWE)

我正在尝试在 angular 12 和 .net core 5 中实现令牌刷新功能。

这是我的 JWT 服务注册:

startup.cs:

services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;

        }).AddJwtBearer(options =>
        {
            options.SaveToken = true;
            options.RequireHttpsMetadata = false;
            options.TokenValidationParameters = new TokenValidationParameters()
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ClockSkew = TimeSpan.Zero,

                ValidAudience = _conf["JWT:ValidAudience"],
                ValidIssuer = _conf["JWT:ValidIssuer"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_conf["JWT:Secret"]))
            };
        });

这是我的 appsettings.json:

"JWT": {
"ValidAudience": "http://localhost:4200",
"ValidIssuer": "http://localhost:4200",
"Secret": "JWTRefreshTokenHIGHsecuredPasswordVVVp1OH7Xzyr",
"TokenValidityInMinutes": 1,
"RefreshTokenValidityInDays": 7
}

调用登录控制器后,一个简单的访问令牌和一个刷新令牌被发送回客户端:

登录控制器:

            //login codes omitted for simplicity
            foreach (var role in userRoles)
            {
                authClaims.Add(new Claim(ClaimTypes.Role, role));
            }

            var accessToken = CreateToken(authClaims);
            var refreshToken = GenerateRefreshToken();

            _ = int.TryParse(_configuration["JWT:RefreshTokenValidityInDays"], out int refreshTokenValidityInDays);

            user.RefreshToken = refreshToken;
            user.RefreshTokenExpiryTime = DateTime.Now.AddDays(refreshTokenValidityInDays);

CreateToken 方法:

    private JwtSecurityToken CreateToken(List<Claim> authClaims)
    {
        var authSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
        _ = int.TryParse(_configuration["JWT:TokenValidityInMinutes"], out int TokenValidityInMinutes);

        var expires = DateTime.Now.ToLocalTime().AddMinutes(TokenValidityInMinutes);
        var token = new JwtSecurityToken(
            issuer: _configuration["JWT:ValidIssuer"],
            audience: _configuration["JWT:ValidAudience"],
            claims: authClaims,
            expires: expires,
            signingCredentials: new SigningCredentials(authSecurityKey, SecurityAlgorithms.HmacSha256)
            ) ;
        return token;
    }

现在登录后,我在浏览器 SessionStorage 中保存了访问令牌和刷新令牌。

在客户端,我实现了一个 HTTP 拦截器来处理 401 未经授权的错误。当我的后端显示令牌已过期时,我想刷新我的 JWT 令牌。

auth.interceptor.ts:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  private isRefreshing = false;

  private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);

  constructor(private tokenService: tokenStorageService, private authService: AuthenticationService) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<Object>> {
    debugger;
    let authReq = req;
    const token = this.tokenService.getToken();

    if (token != null) {
      authReq = this.addTokenHeader(req, token);
    }
    return next.handle(authReq).pipe(catchError(error => {
      if (error instanceof HttpErrorResponse && !authReq.url.includes('auth/login') && error.status === 401) {
        debugger;
        return this.handle401Error(authReq, next);
      }
      return throwError(error);
    }));
  }
  private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);
      const token = this.tokenService.getRefreshToken();
      const refreshToken=this.tokenService.getRefreshToken();
      if (token)
        return this.authService.refreshToken(token,refreshToken).pipe(
          switchMap((token: any) => {
            this.isRefreshing = false;
            this.tokenService.saveToken(token.accessToken);
            this.refreshTokenSubject.next(token.accessToken);

            return next.handle(this.addTokenHeader(request, token.accessToken));
          }),
          catchError((err) => {
            debugger;
            this.isRefreshing = false;

            //this.tokenService.signOut();
            return throwError(err);
          })
        );
    }
    return this.refreshTokenSubject.pipe(
      filter(token => token !== null),
      take(1)        switchMap((token) => next.handle(this.addTokenHeader(request, token)))
    );
  }
  private addTokenHeader(request: HttpRequest<any>, token: string) {

    return  request.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });

  }
}

登录后一分钟我的令牌过期并且拦截器调用我的刷新令牌api。

RefreshToken 控制器:

    public async Task<object> RefreshToken(tokenModel tokenModel)
    {
        if (tokenModel == null)
            throw new ServiceException("Invalid Token Model");

        string accessToken = tokenModel.accessToken;
        string refreshToken = tokenModel.refreshToken;

        var principal = GetPrincipalsFromExpiredToken(accessToken);
        if(principal==null)
        {
            throw new  ServiceException("Invalid access token or refresh token");
        }

        string username = principal.Identity.Name;

        var user = await _userManager.FindByNameAsync(username);
        if(user==null || user.RefreshToken!=refreshToken || user.RefreshTokenExpiryTime<=DateTime.Now)
        {
            throw new ServiceException("Invalid access token or refresh token");
        }

        var newAccessToken = CreateToken(principal.Claims.ToList());
        var newRefreshToken = GenerateRefreshToken();
        user.RefreshToken = newRefreshToken;

        await _userManager.UpdateAsync(user);

        return new
        {
            accessToken=new JwtSecurityTokenHandler().WriteToken(newAccessToken),
            refreshToken=newRefreshToken
        };
    }

这是 GetPrincipalsFromExpiredToken 方法:

private ClaimsPrincipal GetPrincipalsFromExpiredToken(string token)
    {
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = false,
            ValidateIssuer = false,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"])),
            ValidateLifetime = false
        };

        var tokenHandler = new JwtSecurityTokenHandler();
        SecurityToken securityToken = null;

        var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);

        if (securityToken is not JwtSecurityToken jwtSecurityToken ||
        !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,
        StringComparison.InvariantCultureIgnoreCase))
            throw new SecurityTokenException("Invalid token");

        return principal;

    }

在 tokenHandler.ValidateToken 我收到此错误消息:

IDX12741:JWT:'[PII 已隐藏。有关更多详细信息,请参阅 https://aka.ms/IdentityModel/PII.]' must have three segments (JWS) or five segments (JWE).

我做错了什么? 有没有更简单的方法来实现 jwt 刷新令牌?

这是生成的令牌:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdEBrYXNiaW0uaXIiLCJqdGkiOiJjM2I2ZTQyZi00ZGI1LTQzMDMtYjY4Mi02YWU5Yzg3ZjI1ZTUiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJVc2VyIiwiZXhwIjoxNjUyMzc0ODI4LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQyMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjQyMDAifQ.8rzkMkVENPAyWV2DpPhUUAzza0cXY_HiUpWq2u_Sqqs

你的 jwt 是什么样子的? JWT由三个parts:header、playload和signature组成,以“.”分割。我想在你的 jwt string.Your 错误是由错误的 jwt 结构引起的。 你可以看到如下测试结果:

已更新

第二次请求的错误信息(我在jwt中删掉了一个"."):