• 完美解决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(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

  • 相关阅读:
    .net中实现运行时从字符串动态创建对象
    C# 用 VB.net 函數庫 實現全角與半角轉換
    實現.net 加載插件方式
    VS2008下載
    Lotus Notes Send EMail from VB or VBA
    用C#写vs插件中的一些Tip
    SQL2005中异常处理消息框可直接使用
    C#路径/文件/目录/I/O常见操作汇总
    利用.net反射动态调用指定程序集的中的方法
    说说今年的计划
  • 原文地址:https://www.cnblogs.com/neozhu/p/13181074.html
Copyright © 2020-2023  润新知