登录并获取数据后,为什么 Angular Http Put 请求在第一次尝试 API 控制器时成功,但在 20 秒后下一次 PUT 失败?

问题描述 投票:0回答:1

我对 ASP.NET Core、Angular HttpClient 和 Observables 还很陌生,所以我需要一些帮助。

我创建并发布了一个不面向公众的基于 Web 的应用程序,该应用程序具有 Angular 9 客户端和 Microsoft ASP.NET Core 2.2 API 控制器后端,该后端使用 Entity Framework Core ORM 访问 SQL Server。我使用 Microsoft ASP.NET Core Identity 进行身份验证、授权、身份模型令牌 (JWT) 和 AntiForgery 作为安全性。

我可以使用凭据登录(从 Angular UI),并使用各种表的 Http Get 请求检索所需的 ORM 实体数据。但是,当我尝试

Post
Put
Delete
表中的实体时,通常只有第一次尝试成功(200 状态),后续尝试会失败(302 状态)。我必须再次注销/登录才能继续进行更新。在初始登录请求之后,我不会在请求中发送凭据。

我包括我的 Angular 组件、存储库和数据源代码以及我的

StartUp.cs
和 API 控制器。

请注意,我仅在登录时生成一次

JWT
作为不记名令牌,将其返回给客户端,并在后续请求中将其发送回 API 进行比较。我现在没有解码用户名值的不记名令牌,但我过去做了同样的结果。

我认为

status code 302
告诉我我的
Put
请求找到了 URL,但此时我不再在服务器上进行身份验证,而且我不知道如何处理这种情况。

我正在考虑的三个选项是(1)发送带有破坏性http请求的

login credentials
,(2)调查
refresh tokens
,或者(3)在每个
Put
Post
Delete之后刷新我的JWT令牌
请求并将其作为新令牌返回给客户端。我的问题是,我不确定这些选项中的任何一个是否设计为与 Microsoft ASP.NET Core Identity 一起使用。

任何帮助指出我遗漏或做错的事情将不胜感激。

从 Angular 9 表更新第一次有效,第二次无效:

浏览器网络选项显示:

角9分量法:

  saveMember(member: Member): boolean {

  this.repo.saveMember(member).subscribe(result => this.members.splice(this.members.
    findIndex(m => m.memberID == member.memberID), 1, result),
    err => console.log('From saveMember(): ', err),
    () => console.log('From saveMember(): Completed'));

  }

Angular 9 存储库 Observable 方法包括 Http HEAD 请求:

    // Repository Method:  creates new member or updates existing member
    saveMember(theMember: Member): Observable<Member> {

       if (theMember.memberID == null || theMember.memberID == 0) {
           this.dataSource.saveMemberHeader().subscribe(result => { this.showError = !result },
            err => console.log('From Head() Request: ', err),
            () => console.log('From Head() Request: Completed'));

       return this.dataSource.saveMember(theMember).pipe(map(response => {
          if (response) {
            this.members.push(response);
          }
          return response;
       })); 

    }
    else {
        this.dataSource.updateMemberHeader().subscribe(result => { this.showError = !result },
          err => console.log('From Head() Request: ', err),
          () => console.log('From Head() Request: Completed'));

      return this.dataSource.updateMember(theMember).pipe(map(response => {
          if (response) {
              let index = this.members.findIndex(item => this.locator(item, response.memberID));
                  this.members.splice(index, 1, response);
          }
          return response;
          }));  
        }
     }

Angular 9 数据源:

  // Datasource http calls to server

  update: string = "update";
  updateMember(member: Member): Observable<Member> {
    return this.sendPutRequest<Member>(`${this.url}/${this.update}`, member);
  }

  private sendPutRequest<T>(url: string, member: Member): Observable<T> {
          let myHeaders = new HttpHeaders();
      if (this.authCookie.JWTauthcookie == null) {
              myHeaders = myHeaders.set("Access-Key", "<secret>");
          } else {
              myHeaders = myHeaders.set("Authorization", "Bearer<" + this.authCookie.JWTauthcookie + ">");
          }
          myHeaders = myHeaders.set("Application-Names", ["ClientApp", "SPA"]);
          return this.http.put<T>(url,
        {
            member: member
        },
        {
            headers: myHeaders
        }).pipe(map(response => { 
        return response;  // response object is a Member entity
        }))
        .pipe(catchError((error: Response) =>
            throwError(`An Error Occurred: ${error.statusText} (${error.status})`)
        ));
  }


  updateMemberHeader(member?: Member): Observable<any> {
        return this.sendRequest<Member>("HEAD", `${this.url}/${this.update}`, member);
  }

  private sendRequest<T>(verb: string, url: string, body?: Member): Observable<T> {
    let myHeaders = new HttpHeaders();
    if (this.authCookie.JWTauthcookie == null) {
        myHeaders = myHeaders.set("Access-Key", "<secret>");
    } else {
      myHeaders = myHeaders.set("Authorization", "Bearer<" + this.authCookie.JWTauthcookie + ">");
        }
        myHeaders = myHeaders.set("Application-Names", ["ClientApp", "SPA"]);
        return this.http.request<T>(verb, url, {
        body: body,
        headers: myHeaders
    }).pipe(map(response => {
        return response;    
          })).pipe(catchError((error: Response) =>
         throwError(`An Error Occurred: ${error.statusText} (${error.status})`)
             ));
      }   

ASP.NET Core Web 帐户控制器:

  using System;
  using System.ComponentModel.DataAnnotations;
  using System.Collections.Generic;
  using System.Threading.Tasks;
  using System.Security.Claims;
  using Microsoft.AspNetCore.Identity;
  using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
  using Microsoft.AspNetCore.Mvc;
  using ServerApp.Infrastructure;  // contains the code for "Jwt_GenerateToken"

  namespace ServerApp.Controllers
  {

      [ValidateAntiForgeryToken]
      public class AccountController : Controller
      {
          private UserManager<IdentityUser> userManager;
          private SignInManager<IdentityUser> signInManager;  
          private RoleManager<IdentityRole> roleManager;

          public static string bearerJWTokenString;  
          private string bearerToken;

          public SignInResult signInResult;
          private string plainName = "";
          private string plainLoginPwd = "";

          public AccountController(UserManager<IdentityUser> userMgr,
        SignInManager<IdentityUser> signInMgr, RoleManager<IdentityRole> roleMgr)
          {
              userManager = userMgr;
              signInManager = signInMgr;
              roleManager = roleMgr;
          }

          [HttpPost("/api/account/login")]  
          [IgnoreAntiforgeryToken]
          public async Task<IActionResult> Login([FromBody] LoginViewModel creds)
          {
              if (ModelState.IsValid && await DoLogin(creds))
              {
                  string isAuthenticated = signInResult.Succeeded.ToString(); 
                  object myJWT = "{ " + '\n' + "   " + '"' + "success" + '"' + ": " + '"' + isAuthenticated + '"' + "," + '\n' +
                  "   " + '"' + "token" + '"' + ':' + '"' + _token + '"' + "," + '\n' +
                  "}";

                  return Ok(myJWT);   
              }
              return BadRequest(); 
          }

          public string _token = null;

          public async Task<bool> DoLogin(LoginViewModel creds)
          {
              plainName = creds.Name;
              plainLoginPwd = creds.Password;

              IdentityUser loginuser = await userManager.FindByNameAsync(plainName);

              if (loginuser != null)
              {
                  await signInManager.SignOutAsync();

                  signInResult =
                  await signInManager.PasswordSignInAsync(loginuser, plainLoginPwd, false, false);

                  if (signInResult.Succeeded)
                  {

                      // generates a signed Json Web Token with current user name
                      _token = Jwt_GenerateToken.GenerateToken(loginuser.ToString(), 90); // in Infrastructure folder
                      bearerJWTokenString = _token;
                  }

                  return signInResult.Succeeded; 
             }
             return false;
          }

      }

      public class LoginViewModel
      {
          [Required]
          public string Name { get; set; }
          [Required]
          public string Password { get; set; }
      }

  }

ASP.NET Core Web API 控制器:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ServerApp.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using System.ComponentModel.DataAnnotations;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using ServerApp.Infrastructure;

namespace ServerApp.Controllers {

[Route("api/members")]
[ApiController]
[Authorize]
[AutoValidateAntiforgeryToken]
public class MemberValuesController : Controller 
{
    private string bearerToken;
    private SecurityToken validatedToken;
    private JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
    private ClaimsPrincipal vtoken = new ClaimsPrincipal();

    private ApplicationDbContext context;

    public MemberValuesController(ApplicationDbContext ctx)
    {
        context = ctx;
    }

    public class MemberModel
    {
        [Required]
        public Member Member { get; set; }  // must capitalize or creates naming convention error!
    }

    [HttpPut("/api/members/update")]  
    [Authorize(Roles = "Admin, SuperUser, User")]
    public async Task<Member> Put([FromBody]  MemberModel MbrModel) 
    {
        bearerToken = Request.Headers["Authorization"];
        var bearer = bearerToken.Substring(7, bearerToken.Length - 8);

        vtoken = tokenHandler.ValidateToken(bearer, Jwt_GenerateToken.vParms, out validatedToken);
        if (vtoken.Identity.Name.ToString() == Jwt_GenerateToken.cPrince.Name.ToString())
        {

            if (vtoken.Identity.IsAuthenticated == true)
            {
                Member UpdatedMember = new Member();
                if (ModelState.IsValid)
                {
                    UpdatedMember = await DoMemberUpdate(MbrModel);
                    return UpdatedMember;
                }
                    return UpdatedMember;
            }
            else
            {
                return ViewBag;
            }
        }
        else
        {
            return ViewBag;
        }
    }

    private async Task<Member> DoMemberUpdate(MemberModel MbrModel)
    {
        Member member = new Member();
        member = MbrModel.Member;

        context.Members.Update(member);

        var saved = false;

        while (!saved)
        {
            try
            {
                await context.SaveChangesAsync();
                saved = true;
                return member;
            }
            catch (DbUpdateConcurrencyException ex)
            {
                foreach (var entry in ex.Entries)
                {
                    if (entry.Entity is Member)
                    {
                        var proposedValues = entry.CurrentValues;
                        var databaseValues = entry.GetDatabaseValues();

                        foreach (var property in proposedValues.Properties)
                        {
                            var proposedValue = proposedValues[property];

                            proposedValues[property] = proposedValue;    //<value to be saved>;
                        }
                        entry.OriginalValues.SetValues(databaseValues);
                    }
                }
            }
        }

        return member;
    }

    [HttpHead("/api/members/update")]
    public async Task<IActionResult> GetUpdateHeader()
    {
        bearerToken = Request.Headers["Authorization"];
        var bearer = bearerToken.Substring(7, bearerToken.Length - 8);

        if (AccountController.bearerJWTokenString == bearer)
        {
            int MbrCount = 0;
            MbrCount = await DoGetMembersHeader();

            if (MbrCount > -1)
            {
                Response.Headers.Add("Items-total", MbrCount.ToString());
            }
            else
            {
                Response.Headers.Add("Items-total", "Bad result");
            }
            return Ok();
        }
        else
        {
            return BadRequest();
        }
    }

    private async Task<int> DoGetMembersHeader()
    {
        return await context.Members.Include(m => m.MemberID)
        .OrderBy(m => m.Last_Name + m.First_Name + m.MidInit)
        .Select(m => new
        {
            m.MemberID,
            m.Last_Name,
            m.First_Name,
            m.MidInit,
            m.Email_Address,
            m.Cell_Phone,

            // ..... more properties

            m.GUIDKey,
            m.RowVersion
        }).CountAsync();
    }
}

生成token的基础设施类:

  using System;
  using System.IdentityModel.Tokens.Jwt;
  using System.Security.Claims;
  using Microsoft.IdentityModel.Tokens;

  namespace ServerApp.Infrastructure
  {
      public class Jwt_GenerateToken
      {
          public static ClaimsIdentity cPrince;
          public static TokenValidationParameters vParms = new TokenValidationParameters();

          private const string Beans = "bhmYDKCEN4pGSEoJcI6t ... more ... ==";
          public static string GenerateToken(string username, int expireMinutes)
          {
              // "expireMinutes" value along with "username" comes from "DoLogin()" method in AccountController.cs
              byte[] symmetricKey = Convert.FromBase64String(Beans);
              var tokenHandler = new JwtSecurityTokenHandler();

              var now = DateTime.UtcNow;
              var tokenDescriptor = new SecurityTokenDescriptor
              {
                  Subject = new ClaimsIdentity(new[]
                  {
                      new Claim(ClaimTypes.Name, username)
                  }),

                  Expires = now.AddMinutes(Convert.ToDouble(expireMinutes)),

                  SigningCredentials = new SigningCredentials(
                      new SymmetricSecurityKey(symmetricKey),
                      SecurityAlgorithms.HmacSha256Signature)
              };
              var stoken = tokenHandler.CreateToken(tokenDescriptor);
              var token = tokenHandler.WriteToken(stoken);

              // creating a static public ClaimsPrincipal object to compare to JWT token in client request
              //  https://stackoverflow.com/questions/40281050/jwt-authentication-for-asp-net-web-api?rq=1
              vParms.IssuerSigningKey = tokenDescriptor.SigningCredentials.Key;
              vParms.RequireExpirationTime = true;
              vParms.ValidateIssuer = false;
              vParms.ValidateAudience = false;
              cPrince = tokenDescriptor.Subject;  // the Claims Identity object to compare to future requests
              // end update

              return token;    
          }

      }
  }

StartUp.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;  
using Microsoft.Extensions.DependencyInjection;
using ServerApp.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Antiforgery;

namespace ServerApp
{
     public class Startup
     {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication();   // is this necessary or redundant here?

            string conString_A = Configuration["TheDatabaseProd:AConnectionString"];

            services.AddDbContext<ApplicationDbContext>(options =>
                      options.UseSqlServer(conString_A));
        
            string conString_I = Configuration["TheDatabaseProd:IConnectionString"];

            services.AddDbContext<IdentityDataContext>(options =>
                      options.UseSqlServer(conString_I));

            services.AddIdentity<IdentityUser, IdentityRole>()
                 .AddEntityFrameworkStores<IdentityDataContext>();

            services.AddMvc()
                    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.AddAntiforgery(options =>
                     {
                         options.HeaderName = "X-XSRF-TOKEN";
                     });

            services.AddMvc(options =>
            {
                options.Filters.Add(new ValidateAntiForgeryTokenAttribute());
            });

            services.AddMemoryCache();    // is this necessary or redundant?

            services.AddDistributedMemoryCache();  

            services.AddSession(); 
            services.AddSession(options =>
            {
                   options.Cookie.Name = "MyApp.Session";
                   options.IdleTimeout = TimeSpan.FromMinutes(10); 
                   options.Cookie.IsEssential = true;  
                   options.Cookie.SameSite = SameSiteMode.Lax;
             });
        }  

        public void Configure(IApplicationBuilder app, IHostingEnvironment env,
                IServiceProvider services, IAntiforgery antiforgery)
        {
                 if (env.IsDevelopment())
                 {
                    app.UseDeveloperExceptionPage();
                 }
                 else
                 {
                    app.UseExceptionHandler("/Home/Error");
                    app.UseHsts();  
                 }

                 app.UseStaticFiles();

                 app.UseStaticFiles(new StaticFileOptions
                 {
                     RequestPath = "",
                     FileProvider = new PhysicalFileProvider(
                          Path.Combine(Directory.GetCurrentDirectory(), "./wwwroot/app"))
                 });

                 app.UseAuthentication();

                 app.UseSession();  

                 app.Use(nextDelegate => context =>
                 {  
                    string path = context.Request.Path.Value;
                    string[] directUrls = { "/appmenu", "/table", "/form", "/form/edit", "/form/create",
                    "/queryTable", "/oneMemberTable", "/retreatTable", "/coreTeamMemberTable" };

                    if (path.StartsWith("/api") || string.Equals("/", path) || directUrls.Any(url => path.StartsWith(url)))
                    {
                        var tokens = antiforgery.GetAndStoreTokens(context);
                        context.Response.Cookies.Append("XSRF-REQUEST-TOKEN", tokens.RequestToken,
                        new CookieOptions()
                        {
                                 HttpOnly = false,  // HttpOnly must be false or the  x-xsrf-token header is not filled in the Request     
                                 Secure = true,
                                 IsEssential = true   // was true, 2023.06.06
                        });
                    }

                    return nextDelegate(context);
                 });

                 app.UseMvc(routes =>
                 {
                     routes.MapRoute(
                         name: "default",
                         template: "{controller=Home}/{action=Index}/{id?}");   
                 });
        }
    }
}
angular asp.net-core http asp.net-core-webapi asp.net-core-identity
1个回答
0
投票

将您的

services.AddAuthentication();
更改为
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);

© www.soinside.com 2019 - 2024. All rights reserved.