完美解決asp.net core 3.1 兩個AuthenticationScheme(cookie,jwt)共存在一個項目中,基於領域驅動設計(DDD)超輕量級快速開發架構

內容

在我的項目中有mvc controller(view 和 razor Page)同時也有webapi,那麼就需要網站同時支持2種認證方式,web頁面的需要傳統的cookie認證,webapi則需要使用jwt認證方式,兩種默認情況下不能共存,一旦開啟了jwt認證,cookie的登錄界面都無法使用,原因是jwt是驗證http head “Authorization” 這屬性.所以連login頁面都無法打開.

解決方案

實現web通過login頁面登錄,webapi 使用jwt方式獲取認證,支持refreshtoken更新過期token,本質上背後都使用cookie認證的方式,所以這樣的結果是直接導致token沒用,認證不是通過token唯一的作用就剩下refreshtoken了

通過nuget 安裝組件包

Microsoft.AspNetCore.Authentication.JwtBearer

下面是具體配置文件內容

//Jwt Authentication
      services.AddAuthentication(opts =>
      {
        //opts.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        //opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
      })
      //這裡是關鍵,添加一個Policy來根據http head屬性或是/api來確認使用cookie還是jwt chema
        .AddPolicyScheme(settings.App, "Bearer or Jwt", options =>
        {
          options.ForwardDefaultSelector = context =>
          {
            var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false;
            // You could also check for the actual path here if that's your requirement:
            // eg: if (context.HttpContext.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCulture))
            if (bearerAuth)
              return JwtBearerDefaults.AuthenticationScheme;
            else
              return CookieAuthenticationDefaults.AuthenticationScheme;
          };
        })
//這裏和傳統的cookie認證一致       .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
       {
         options.LoginPath = "/Identity/Account/Login";
         options.LogoutPath = "/Identity/Account/Logout";
         options.AccessDeniedPath = "/Identity/Account/AccessDenied";
         options.Cookie.Name = "CustomerPortal.Identity";
         options.SlidingExpiration = true;
         options.ExpireTimeSpan = TimeSpan.FromSeconds(10); //Account.Login overrides this default value
       })
        .AddJwtBearer(x =>
      {
        x.RequireHttpsMetadata = false;
        x.SaveToken = true;
        x.TokenValidationParameters = new TokenValidationParameters
        {
          ValidateIssuerSigningKey = true,
          IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["Jwt:Key"])),
          ValidateIssuer = true,
          ValidateAudience = true,
          ValidateLifetime = true,
          ValidIssuer = Configuration["Jwt:Issuer"],
          ValidAudience = Configuration["Jwt:Issuer"],
        };
      });

 //這裏需要對cookie做一個配置
      services.ConfigureApplicationCookie(options =>
      {
        // Cookie settings
        options.Cookie.Name = settings.App;
        options.Cookie.HttpOnly = true;
        options.ExpireTimeSpan = TimeSpan.FromSeconds(10);
        options.LoginPath = "/Identity/Account/Login";
        options.LogoutPath = "/Identity/Account/Logout";
        options.Events = new CookieAuthenticationEvents()
        {
          OnRedirectToLogin = context =>
          {
           //這裏區分當訪問/api 如果cookie過期那麼 不重定向到login登錄界面
            if (context.Request.Path.Value.StartsWith("/api"))
            {
              context.Response.Clear();
              context.Response.StatusCode = 401;
              return Task.FromResult(0);
            }
            context.Response.Redirect(context.RedirectUri);
            return Task.FromResult(0);
          }
        };
        //options.AccessDeniedPath = "/Identity/Account/AccessDenied";
      });        

startup.cs

下面userscontroller 認證方式

重點:我簡化了refreshtoken的實現方式,原本規範的做法是通過第一次登錄返回一個token和一個唯一的隨機生成的refreshtoken,下次token過期后需要重新發送過期的token和唯一的refreshtoken,同時後台還要比對這個refreshtoken是否正確,也就是說,第一次生成的refreshtoken必須保存到數據庫里,這裏我省去了這個步驟,這樣做是不嚴謹的的.

[ApiController]
  [Route("api/users")]
  public class UsersEndpoint : ControllerBase
  {
    private readonly ILogger<UsersEndpoint> _logger;
    private readonly ApplicationDbContext _context;
    private readonly UserManager<ApplicationUser> _manager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly SmartSettings _settings;
    private readonly IConfiguration _config;

    public UsersEndpoint(ApplicationDbContext context,
      UserManager<ApplicationUser> manager,
      SignInManager<ApplicationUser> signInManager,
      ILogger<UsersEndpoint> logger,
      IConfiguration config,
      SmartSettings settings)
    {
      _context = context;
      _manager = manager;
      _settings = settings;
      _signInManager = signInManager;
      _logger = logger;
      _config = config;
    }
    [Route("authenticate")]
    [AllowAnonymous]
    [HttpPost]
    public async Task<IActionResult> Authenticate([FromBody] AuthenticateRequest model)
    {
      try
      {
        //Sign user in with username and password from parameters. This code assumes that the emailaddress is being used as the username. 
        var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, true, true);

        if (result.Succeeded)
        {
          //Retrieve authenticated user's details
          var user = await _manager.FindByNameAsync(model.UserName);

          //Generate unique token with user's details
          var accessToken = await GenerateJSONWebToken(user);
          var refreshToken = GenerateRefreshToken();
          //Return Ok with token string as content
          _logger.LogInformation($"{model.UserName}:JWT登錄成功");
          return Ok(new { accessToken = accessToken, refreshToken = refreshToken });
        }
        return Unauthorized();
      }
      catch (Exception e)
      {
        return StatusCode(500, e.Message);
      }
    }
    [Route("refreshtoken")]
    [AllowAnonymous]
    [HttpPost]
    public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest model)
    {
      var principal = GetPrincipalFromExpiredToken(model.AccessToken);
      var nameId = principal.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
      var user = await _manager.FindByNameAsync(nameId);
      await _signInManager.RefreshSignInAsync(user);

        //Retrieve authenticated user's details
        //Generate unique token with user's details
        var accessToken = await GenerateJSONWebToken(user);
        var refreshToken = GenerateRefreshToken();
        //Return Ok with token string as content
        _logger.LogInformation($"{user.UserName}:RefreshToken");
        return Ok(new { accessToken = accessToken, refreshToken = refreshToken });


    }

    private async Task<string> GenerateJSONWebToken(ApplicationUser user)
    {
      //Hash Security Key Object from the JWT Key
      var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
      var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

      //Generate list of claims with general and universally recommended claims
      var claims = new List<Claim>  {
           new Claim(ClaimTypes.NameIdentifier, user.UserName),
           new Claim(ClaimTypes.Name, user.UserName),
                new Claim(JwtRegisteredClaimNames.Sub, user.Email),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(ClaimTypes.NameIdentifier, user.Id),
                //添加自定義claim
                new Claim(ClaimTypes.GivenName, string.IsNullOrEmpty(user.GivenName) ? "" : user.GivenName),
                new Claim(ClaimTypes.Email, user.Email),
                new Claim("http://schemas.microsoft.com/identity/claims/tenantid", user.TenantId.ToString()),
                new Claim("http://schemas.microsoft.com/identity/claims/avatars", string.IsNullOrEmpty(user.Avatars) ? "" : user.Avatars),
                new Claim(ClaimTypes.MobilePhone, user.PhoneNumber)
      };
      //Retreive roles for user and add them to the claims listing
      var roles = await _manager.GetRolesAsync(user);
      claims.AddRange(roles.Select(r => new Claim(ClaimsIdentity.DefaultRoleClaimType, r)));
      //Generate final token adding Issuer and Subscriber data, claims, expriation time and Key
      var token = new JwtSecurityToken(_config["Jwt:Issuer"]
          , _config["Jwt:Issuer"],
          claims,
          null,
          expires: DateTime.Now.AddDays(30),
          signingCredentials: credentials
      );

      //Return token string
      return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public string GenerateRefreshToken()
    {
      var randomNumber = new byte[32];
      using (var rng = RandomNumberGenerator.Create())
      {
        rng.GetBytes(randomNumber);
        return Convert.ToBase64String(randomNumber);
      }
    }

    private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
    {
      var tokenValidationParameters = new TokenValidationParameters
      {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_config["Jwt:Key"])),
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidIssuer = _config["Jwt:Issuer"],
        ValidAudience = _config["Jwt:Issuer"],
      };

      var tokenHandler = new JwtSecurityTokenHandler();
      SecurityToken securityToken;
      var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
      var jwtSecurityToken = securityToken as JwtSecurityToken;
      if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
      {
        throw new SecurityTokenException("Invalid token");
      }

      return principal;
    }
....
}
}

ControllerBase

下面是測試

獲取token

 refreshtoken

 

獲取數據

 

 這裏獲取數據的時候,其實可以不用填入token,因為調用authenticate或refreshtoken是已經記錄了cookie到客戶端,所以在postman測試的時候都可以不用加token也可以訪問

 推廣一下我的開源項目

基於領域驅動設計(DDD)超輕量級快速開發架構

https://www.cnblogs.com/neozhu/p/13174234.html

源代碼

https://github.com/neozhu/smartadmin.core.urf

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!