在我的项目中有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";
});
下面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;
}
....
}
}
下面是测试
获取token
refreshtoken
获取数据
这里获取数据的时候,其实可以不用填入token,因为调用authenticate或refreshtoken是已经记录了cookie到客户端,所以在postman测试的时候都可以不用加token也可以访问