完美解决asp.net core 3.1 两个AuthenticationScheme(cookie,jwt)共存在一个项目中
- 2020 年 6 月 23 日
- 筆記
- Asp.Net, 设计模式Pattern
内容
在我的项目中有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("//schemas.microsoft.com/identity/claims/tenantid", user.TenantId.ToString()), new Claim("//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)超轻量级快速开发架构
//www.cnblogs.com/neozhu/p/13174234.html
源代码
//github.com/neozhu/smartadmin.core.urf