1. 程式人生 > 實用技巧 >完美解決asp.net core 3.1 兩個AuthenticationScheme(cookie,jwt)共存在一個專案中

完美解決asp.net core 3.1 兩個AuthenticationScheme(cookie,jwt)共存在一個專案中

內容

在我的專案中有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(); //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();
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 = ;
return Task.FromResult();
}
context.Response.Redirect(context.RedirectUri);
return Task.FromResult();
}
};
//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(, 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(),
signingCredentials: credentials
); //Return token string
return new JwtSecurityTokenHandler().WriteToken(token);
} public string GenerateRefreshToken()
{
var randomNumber = new byte[];
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