ASP.NET Core使用JWT+标识框架(identity)实现登录验证

这篇具有很好参考价值的文章主要介绍了ASP.NET Core使用JWT+标识框架(identity)实现登录验证。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

最近阅读了《ASP.NET Core 技术内幕与项目实战——基于DDD与前后端分离》(作者杨中科)的第八章,对于Core入门的我来说体会颇深,整理相关笔记。

JWT:全称“JSON web toke”,目前流行的跨域身份验证解决方案;

标识框架(identity):由ASP.NET Core提供的框架,它采用RBAC(role-based access control)策略,内置了对用户、角色等表的管理即相关接口,从而简化了系统开发,使用EF Core对数据库进行操作。

注意:本书全篇采用“模型驱动开发”

一、JWT 实现登录的流程如下:

1、使用标识框架(identity)生成数据库

2、客户端向服务器端发送用户名、密码等请求登录

3、服务器端校验用户名、密码,如果校验成功,则从数据库中取出这个用户的 ID、角色等用户相关信息。

4、服务器端采用只有服务器端才知道的密钥来对用户信息的JSON字符串进行签名,形成签名数据。

5、服务器端把用户信息的JSON 字符串和签名拼接到一起形成JWT,然后发送给客户端。

6、客户端保存服务器端返回的 JWT,并且在客户端每次向服务器端发送请求的时候都带上这个JWT。每次服务器端收到浏览器请求中携带的JWT后,服务器端用密钥对JWT 的签名进行校验,如果校验成功,服务器端则从JWT 中的JSON 字符串中读取出用户的信息。这样服务器端就知道这个请求对应的用户了,也就实现了登录的功能。

二、实现过程及代码如下:

1、通过nuget安装必须的包:

Microsoft.AspNetCore.Identity.EntityFrameworkCore  ---如果该包安装报错,请切换低版本

Microsoft.AspNetCore.EntityFrameworkCore.SqlServer

Microsoft.AspNetCore.EntityFrameworkCore.Tools

Microsoft.AspNetCore.Authentication.JwtBearer

2、通过标识框架(identity)配置生成数据库

(1)创建User类和Role类分别再继承IdentityUser<long>和IdentityRole<long>

public class User:IdentityUser<long>
    {
        //创建时间
        public DateTime CreationTime { get; set; }
        //昵称
        public string? NickName { get; set; }
        //JWT版本(解决JWT撤回问题)
        public long JWTVersion { get; set; }
    }
public class Role:IdentityRole<long>{}

(2)新建IdContext类,通过该类操作数据库

public class IdDbContext : IdentityDbContext<User, Role, long>
    {
        public IdDbContext(DbContextOptions<IdDbContext> options) : base(options)
        {

        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
        }
    }

(3)在Program.cs中向依赖注入容器中注册与标识框架相关的服务,并对其选项进行配置(该代码参考了文章:https://www.cnblogs.com/nullcodeworld/p/16717260.html)

IServiceCollection services = builder.Services;
//对IdDbContext进行配置
services.AddDbContext<IdDbContext>(opt =>
{
    string connStr = builder.Configuration.GetConnectionString("Default");
    Console.WriteLine("字符连接:"+connStr);
    opt.UseSqlServer(connStr);
});
services.AddDataProtection();
//调用AddIdentityCore添加标识框架的一些重要的基础服务
//(我们没有调用AddIdentity方法,因为AddIdentity方法实现的初始化
// 比较适合传统的MVC模式的项目,而现在我们推荐用前后端分离开发模式。)
services.AddIdentityCore<User>(options =>
{
    // 对密码复杂度苛刻设置
    options.Password.RequireDigit = false;
    options.Password.RequireLowercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireUppercase = false;
    options.Password.RequiredLength = 6;
    options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
    options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
});
var idBuilder = new IdentityBuilder(typeof(User), typeof(Role), services);
//因为UserManager、RoleManager等服务被创建的时候需要注入非常多的服务,
//所以我们在使用标识框架的时候也需要注入和初始化非常多的服务
idBuilder.AddEntityFrameworkStores<IdDbContext>()
    .AddDefaultTokenProviders()
    .AddRoleManager<RoleManager<Role>>()
    .AddUserManager<UserManager<User>>();

(4)我们不要忘记配置在appsetting.json中配置数据库连接字符串(这里我们在连接字符串后面加上了:Encrypt=False;以解决下一步数据库迁移中出现的报错:“.....证书链是由不受信任的颁发机构颁发的”)

"ConnectionStrings": {
    "Default": "Data Source=.;Database=Identity;User ID=sa;Password=123456;MultipleActiveResultSets=True;Encrypt=False"
  }

(5)在【程序包管理器控制台】中执行命令:Add-Migration Init,再执行Update-Database执行数据库迁移代码,如果在这一步中出现错误请先仔细检查以上步骤(可能会提示“No Dbcontext was found in assembly”等错误),检查确定没有步骤上设置问题,我们新建一个MyDesignTimeDbContextFactory类继承IDesignTimeDbContextFactory<IdDbContext>,具体代码如下

class MyDesignTimeDbContextFactory : IDesignTimeDbContextFactory<IdDbContext>
{
    public IdDbContext CreateDbContext(string[] args)
    {
        DbContextOptionsBuilder<IdDbContext> builder = new();
        string connStr = Environment.GetEnvironmentVariable("ConnectionStrings:Default");
        builder.UseSqlServer(connStr);
        return new IdDbContext(builder.Options);
    }
}

到这我们已经完成了标识框架的配置

3、使用JWT实现登录操作

(1)在配置appsetting.json中创建JWt的密钥:SigningKey、过期时间:ExpireSeconds两个配置项;再创建一个对应该节点的配置类JWTOptions

"JWT": {
    "SigningKey": "这里请自定义输入一串复杂的密钥",
    "ExpireSeconds": "86400"
  }
public class JWTOptions
    {
        public string SigningKey { get; set; }
        public int ExpireSeconds { get; set; }
    }

(2)在Program.cs中对JWT进行配置(注意该代码请添加在builder.Build之前)

//jwt验证授权
services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"));//获取配置文件的JWT的key和过期时间放到JWTOptions类中
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
    var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>();
    byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey);
    var secKey = new SymmetricSecurityKey(keyBytes);
    x.TokenValidationParameters = new()
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = secKey
    };
});

(3)在Program.cs中的app.UseAuthorization之前添加app.UseAuthentication

app.UseAuthentication();

(4)创建JWTController类,在其中增加登录并且创建JWT的操作方法(PS:这里的[FromServices]特性实现对 Controller.Action 单独注入,当只有单个方法需要该依赖,可以采用这个特性)

[Route("api/[controller]/[action]")]
    [ApiController]
    public class JWTController : ControllerBase
    {
        private readonly UserManager<User> _userManager;

        public JWTController(UserManager<User> userManager)
        {
            _userManager = userManager;
        }
        /// <summary>
        /// 生成token的方法
        /// </summary>
        /// <param name="claims"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        private static string BuildToken(IEnumerable<Claim> claims, JWTOptions options)
        {
            //token到期时间
            DateTime expires = DateTime.Now.AddSeconds(options.ExpireSeconds);
            //取出配置文件的key
            byte[] keyBytes = Encoding.UTF8.GetBytes(options.SigningKey);
            //对称安全密钥
            var secKey = new SymmetricSecurityKey(keyBytes);
            //加密证书
            var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
            //jwt安全token
            var tokenDescriptor = new JwtSecurityToken(expires: expires, signingCredentials: credentials, claims: claims);

            return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
        }
        /// <summary>
        /// 前端获取token的接口
        /// </summary>
        /// <param name="req"></param>
        /// <param name="jwtOptions"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> Login2(LoginRequest req, [FromServices] IOptions<JWTOptions> jwtOptions)
        {
            string userName = req.UserName;
            string password = req.Password;
            var user = await _userManager.FindByNameAsync(userName);
            if (user == null)
            {
                return NotFound($"用户名不存在{userName}");
            }
            var success = await _userManager.CheckPasswordAsync(user, password);
            if (!success)
            {
                return BadRequest("Failed");
            }
            user.JWTVersion++;//版本号
            await _userManager.UpdateAsync(user);//先把数据库用户版本号更新!!!!
            var claims = new List<Claim>();
            claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
            claims.Add(new Claim(ClaimTypes.Name, user.UserName));
            claims.Add(new Claim(ClaimTypes.Version, user.JWTVersion.ToString()));
            var roles = await _userManager.GetRolesAsync(user);
            foreach (string role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }
            string jwtToken = BuildToken(claims, jwtOptions.Value);
            return Ok(jwtToken);
        }
    }

(5)创建Test2Controller,实现一个测试方法,并且在控制器类上添加[Authorize],这个特性表示该控制器类下所有操作方法都需要登录后才能访问,也可以单独添加在方法上表示该方法需要登录后访问,这是很重要的一步

[Route("api/[controller]")]
    [ApiController]
    [Authorize]
    public class Test2Controller : ControllerBase
    {
        [HttpGet]
        public IActionResult Hello()
        {
            string id = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
            string userName = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
            IEnumerable<Claim> roleClaims = this.User.FindAll(ClaimTypes.Role);
            string roleNames = string.Join(',', roleClaims.Select(c => c.Value));
            return Ok($"id={id},userName={userName},roleNames ={roleNames}");
        }
    }

(6)Swagger没有提供设置自定义HTTP请求报文头(也就是JWT生成的token)的方式,因此传递Authoriation报文接口,我们可以通过对OpenAPI进行配置,使其可以传递Authoriation报文,至此也可以使用Postman这种软件工具调试

builder.Services.AddSwaggerGen(c =>
{
    var scheme = new OpenApiSecurityScheme()
    {
        Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'",
        Reference = new OpenApiReference
        {
            Type = ReferenceType.SecurityScheme,
            Id = "Authorization"
        },
        Scheme = "oauth2",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
    };
    c.AddSecurityDefinition("Authorization", scheme);
    var requirement = new OpenApiSecurityRequirement();
    requirement[scheme] = new List<string>();
    c.AddSecurityRequirement(requirement);
});

重启项目,Swagger界面右上角增加了一个【Authorize】按钮,在对话框中输入“Bearer 登录生成获取的token”(注意:Bearer后面加一个空格再粘贴token)

ASP.NET Core使用JWT+标识框架(identity)实现登录验证

 

 到这个时候,我们再去请求Test2接口,顺利获取到内容

ASP.NET Core使用JWT+标识框架(identity)实现登录验证

 

 (7)解决JWT无法提取撤回的问题。我们解决方案思路采用书上的原方案:在用户表中增加一个整数类型的列JWTVersion,它代表最后次发放出去的令牌的版本号;每次登录、发放令牌的时候,我们都让JWTVersion 的值自增,同时将JWTVersion 的值也放到JWT 的负载中当执行禁用用户、撤回用户的令牌等操作的时候,我们让这个用户对应的JWTVersion的值自增,当服务器端收到客户端提交的JWT后,先把JWT中的JWTVersion值和数据库中的JWTVersion值做比较,如果JWT中JWTVersion的值小于数据库中JWTVersion的值,就说明这个JWT过期了,这样我们就实现了JWT的撤回机制。由于我们在用户表中保存了JWTVersion值,因此这种方案本质上仍然是在服务器端保存状态,这是绕不过去的,只不过这种方案是一种缺点比较少的妥协方案。

(在前面的操作中我们已经给User类新增了“JWTVersion”这个版本字段,在前面“JWTController ”控制器中的“Login2”方法中也完成了方案相应操作)

接下来新增一个操作过滤器JWTValidationFilter并且继承IAsyncActionFilter,实现对所有操作方法中JWT的检查操作

public class JWTValidationFilter : IAsyncActionFilter
    {
        private IMemoryCache memCache;
        private UserManager<User> userMgr;

        public JWTValidationFilter(IMemoryCache memCache, UserManager<User> userMgr)
        {
            this.memCache = memCache;
            this.userMgr = userMgr;
        }

        public async Task   OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            var claimUserId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
            //对于登录接口等没有登录的,直接跳过
            if (claimUserId == null)
            {
                await next();
                return;
            }
            long userId = long.Parse(claimUserId!.Value);
            string cacheKey = $"JWTValidationFilter.UserInfo.{userId}";
            User user = await memCache.GetOrCreateAsync(cacheKey, async e => {
                e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5);
                return await userMgr.FindByIdAsync(userId.ToString());
            });
            if (user == null)
            {
                var result = new ObjectResult($"UserId({userId}) not found");
                result.StatusCode = (int)HttpStatusCode.Unauthorized;
                context.Result = result;
                return;
            }
            //jwt数据库中保存的版本号
            var claimVersion = context.HttpContext.User.FindFirst(ClaimTypes.Version);            
            long jwtVerOfReq = long.Parse(claimVersion!.Value);
            //由于内存缓存等导致的并发问题,
            //假如集群的A服务器中缓存保存的还是版本为5的数据,但客户端提交过来的可能已经是版本号为6的数据。因此只要是客户端提交的版本号>=服务器上取出来(可能是从Db,也可能是从缓存)的版本号,那么也是可以的
            if (jwtVerOfReq >= user.JWTVersion)
            {
                await next();
            }
            else
            {
                var result = new ObjectResult($"JWTVersion mismatch");
                result.StatusCode = (int)HttpStatusCode.Unauthorized;
                context.Result = result;
                return;
            }
        }
    }

(8)把过滤器JWTValidationFilter注册到Program.cs中的全局过滤器中,并且不要忘记注册内存缓存

//过滤器
builder.Services.Configure<MvcOptions>(ops =>
{
    ops.Filters.Add<JWTValidationFilter>();
});

//内存缓存
builder.Services.AddMemoryCache();

到这里基本的使用就完成了,如有错误欢迎指正!!

(该代码参考了文章:https://www.cnblogs.com/nullcodeworld/p/16717260.html)

(参考了书籍《ASP.NET Core 技术内幕与项目实战——基于DDD与前后端分离》(作者杨中科)的第八章)文章来源地址https://www.toymoban.com/news/detail-671865.html

到了这里,关于ASP.NET Core使用JWT+标识框架(identity)实现登录验证的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ASP.NET Core MVC 从入门到精通之Identity入门

    随着技术的发展,ASP.NET Core MVC也推出了好长时间,经过不断的版本更新迭代,已经越来越完善,本系列文章主要讲解ASP.NET Core MVC开发B/S系统过程中所涉及到的相关内容,适用于初学者,在校毕业生,或其他想从事ASP.NET Core MVC 系统开发的人员。 经过前几篇文章的讲解,初步

    2024年02月09日
    浏览(40)
  • C# ASP.NET Core Web API 身份授权(JWT)验证(一)

    1.开发环境 VS2022,安装时记得勾选ASP.NET有关的都选上,建议全选,省的麻烦。          2.创建初始工程 TestApi (你自己的工程名称)。    这就创建工程成功了,按 F5 则可以进行调试了。 而在项目中,我们不仅仅会用到基础的api功能,我们一般还会用到  身份授权(J

    2024年02月02日
    浏览(42)
  • ASP.NET Core 6框架揭秘实例演示[37]:重定向的N种实现方式

    在HTTP的语义中,重定向一般指的是服务端通过返回一个状态码为3XX的响应促使客户端像另一个地址再次发起请求,本章将此称为“客户端重定向“。既然有客户端重定向,自然就有服务端重定向,本章所谓的服务端重定向指的是在服务端通过改变请求路径将请求导向另一个终

    2024年02月08日
    浏览(30)
  • 《深入浅出.NET框架设计与实现》笔记6.2——ASP.NET Core应用程序多种运行模式之二——IIS 服务承载

     ASP.NET Core应用程序可以在多种运行模式下运行,包括自宿主(Self-Hosting)、IIS服务承载、桌面应用程序、服务承载。 因此选择和时的模式很重要。 IIS 服务承载 将 ASP.NET Core 应用程序托管在 Internet Information Services (IIS) 中。 利用 IIS 提供的高级功能,如负载均衡、HTTPS 支持和

    2024年04月26日
    浏览(33)
  • 在ASP.NET Core微服务架构下使用RabbitMQ如何实现CQRS模式

    前言 在现代软件开发中,微服务架构和CQRS模式都是备受关注的技术趋势。微服务架构通过将应用程序拆分为一系列小型、自治的服务,提供了更好的可伸缩性和灵活性。而CQRS模式则通过将读操作和写操作分离,优化了系统的性能和可维护性。本文小编将为大家介绍如何在

    2024年01月16日
    浏览(38)
  • ASP.NET Core 6框架揭秘实例演示[36]:HTTPS重定向

    HTTPS是确保传输安全最主要的手段,并且已经成为了互联网默认的传输协议。不知道读者朋友们是否注意到当我们利用浏览器(比如Chrome)浏览某个公共站点的时候,如果我们输入的是一个HTTP地址,在大部分情况下浏览器会自动重定向到对应HTTPS地址。这一特性源于浏览器和

    2024年02月07日
    浏览(30)
  • asp.net core 框架搭建2-搭建MVC后台管理系统

    作者:xcLeigh 文章地址:https://blog.csdn.net/weixin_43151418/article/details/131458964 asp.net core 框架搭建2-搭建MVC后台管理系统 ,本文章介绍asp.net core框架搭建,然后开发一个后台管理系统,将一步步带着大家,实现目标。所有操作过程将展现在本篇文章,下面咋们一起来实现它吧。 使

    2024年02月12日
    浏览(32)
  • 【ASP.NET Core 基础知识】--MVC框架--Models和数据绑定

    Models和数据绑定在ASP.NET Core MVC中扮演着关键的角色,对于构建强大、灵活和可维护的Web应用程序至关重要。这一节我们就来讲一下。 一、Models 1.1 Models的定义和作用 在ASP.NET Core MVC中,Model是应用程序中用于表示数据结构和业务逻辑的一种抽象。Models充当了MVC(Model-View-Contr

    2024年01月23日
    浏览(36)
  • ASP.Net Core Web API结合Entity Framework Core框架(API的创建使用,接口前端权限设置,前端获取API的Get,post方法)(程序包引用以及导入数据库)

    目录 1. Web Api 程序包引用 2. Web Api 的创建与Http类型的介绍 2.1 ASP.Net Core Web API项目的创建 2 .2  API接口的创建 2.3 HttpGet和HttpPost类型的区别 3.接口权限设置 4.HttpGet方法和HttpPOst方法 5.前端中用HttpGet/Poset获取接口数据 6.EF框架——配置数据库链接字符串(即将数据库中的表导入项

    2024年02月08日
    浏览(52)
  • ASP.NET Core 6框架揭秘实例演示[38]:两种不同的限流策略

    承载ASP.NET应用的服务器资源总是有限的,短时间内涌入过多的请求可能会瞬间耗尽可用资源并导致宕机。为了解决这个问题,我们需要在服务端设置一个阀门将并发处理的请求数量限制在一个可控的范围,即使会导致请求的延迟响应,在极端的情况会还不得不放弃一些请求。

    2024年02月08日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包