ASP.NET Core高级之认证与授权(二)--JWT认证前后端完整实现

阅读本文你的收获

  1. 了解JWT身份认证的流程
  2. 了解基于JWT身份认证和Session身份认证的区别
  3. 学习如何在ASP.NET Core WebAPI项目中封装JWT认证功能

在上文ASP.NET Core高级之认证与授权(一)–JWT入门-颁发、验证令牌中演示了JWT认证的一个入门案例,本文是一个基于JWT认证的完整的前后端实现代码案例。

一、基于JWT的用户认证

JWT身份认证的流程

在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。

无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema。

JWT认证流程上图流程说明:

  1. 用户携带用户名和密码请求认证
  2. 服务器校验用户账号密码,成功则提供一个token给客户端
  3. 客户端存储token,并且在随后的每一次请求中都带着它
  4. 服务器校验token有效则返回数据,无效则返回401状态码;

二、基于JWT身份认证和Session身份认证的区别

基于Session的身份认证的缺点

在这里插入图片描述

  • 会话信息会占用服务器

    每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。

  • 难以扩展

    由于Session是在服务器的内存中的,这就带来一些扩展性的问题。

  • 跨域共享难

    当我们想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享(如使用Redis)问题。当使用AJAX调用从另一个域名下获取资源时,我们可能会遇到禁止请求的问题。

  • 安全性差

    客户端要存Cookie来保存SessionId,所以 用户很容易受到CSRF攻击。

基于JWT令牌的身份认证的优缺点

在这里插入图片描述
优点:

  • 简单轻巧

    JWT生成的token字符串是轻量级,json风格,比较简单。

  • 减轻服务器压力

    它是无状态的,JWT方式将用户状态分散到了客户端中,服务器或者Session中不会存储任何用户信息。明显减轻服务端的内存压力。

  • 容易做分布式

    没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置;

缺点:

  • JWT token一旦签发,无法修改
  • 无法更新token有效期,用户登录状态刷新较难实现
  • 无法销毁一个token,服务端不能对用户状态进行绝对控制
  • 不包含权限控制

三、JWT前后端完整实现关键代码

开发环境:

操作系统: Windows 10 专业版
平台版本是:.NET 6
开发框架:ASP.NET Core WebApi、Vue2+ElementUI
开发工具:Visual Studio 2022

1. JWT的选项配置

在appsetting.json文件中,配置JWT选项参数,这样做的好处是,使用者在发布后任然可以修改JWT的参数,如过期时间,密钥等。

  "JWTTokenOption": {
    "Issuer": "WLW",                        //Token发布者
    "Audience": "EveryTestOne",             //Token接受者
    "IssuerSigningKey": "WLW!@#%^99825949", //秘钥可以构建服务器认可的token;签名秘钥长度最少16
    "AccessTokenExpiresMinutes": "30"       //过期时间30分钟
  }

为了在ASP.NET Core WebAPI项目中读出JWT选项参数,首先定义一个用于保存JWT选项的模型类:

/// <summary>
/// 用来保存jwt的配置信息
/// </summary>
public class JwtTokenOption
{
    /// <summary>
    /// Token发布者
    /// </summary>
    public string Issuer { get; set; }
    /// <summary>
    /// oken接受者
    /// </summary>
    public string Audience { get; set; }
    /// <summary>
    /// 秘钥
    /// </summary>
    public string IssuerSigningKey { get; set; }
    /// <summary>
    /// 过期时间
    /// </summary>
    public int AccessTokenExpiresMinutes { get; set; }
}

在Program.cs中通过以下方式,读取JWT配置选项:

//获取jwt配置项
var jwtTokenConfig = builder.Configuration.GetSection("JWTTokenOption").Get<JwtTokenOption>();
builder.Services.AddSingleton(jwtTokenConfig); //注册单例服务,以便后续调用

2. 配置Jwt身份认证方式

在Program.cs中进行服务注册,配置身份验证的模式为JwtBearer。

安装Microsoft.AspNetCore.Authentication.JwtBearer这个nuget包


//配置JwtBearer身份认证服务
builder.Services.AddAuthentication(opts=>
{
    opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; //认证模式
    opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;    //质询模式
})
.AddJwtBearer(   //对JwtBearer进行配置
    x =>
    {
        x.RequireHttpsMetadata = true; //设置元数据地址或权限是否需要HTTP
        x.SaveToken = true;
        //Token验证参数
        x.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuer = true,  //是否验证Issuer
            ValidIssuer = jwtTokenConfig.Issuer,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtTokenConfig.IssuerSigningKey)),
            ValidateAudience = true,
            ValidAudience = jwtTokenConfig.Audience,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(1)  //对token过期时间验证的允许时间
        };
        //如果jwt过期,在返回的header中加入Token-Expired字段为true,前端在获取返回header时判断
        x.Events = new JwtBearerEvents()
        {
            OnAuthenticationFailed = context =>
            {
                if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                {
                    context.Response.Headers.Add("Token-Expired", "true");
                }
                return Task.CompletedTask;
            }
        };
    }
);

3. 编写生成JWT令牌的帮助类

首先,定义相关的一些模型类,如下:

/// <summary>
/// 存放Token 跟过期时间的模型类
/// </summary>
public class TnToken
{
    /// <summary>
    /// token字符串
    /// </summary>
    public string TokenStr { get; set; }
    /// <summary>
    /// token过期时间
    /// </summary>
    public DateTime Expires { get; set; }
}
/// <summary>
/// 返回信息模型类
/// </summary>
public class ResponseModel
{
    /// <summary>
    /// 返回码
    /// </summary>
    public int Code { get; set; }
    /// <summary>
    /// 消息
    /// </summary>
    public string Msg { get; set; }
    /// <summary>
    /// 数据
    /// </summary>
    public object Data { get; set; }
    /// <summary>
    /// Token信息
    /// </summary>
    public TnToken TokenInfo { get; set; }
}
/// <summary>
/// token工具类的接口,方便使用依赖注入,很简单提供两个常用的方法
/// </summary>
public interface ITokenHelper
{
    /// <summary>
    /// 根据一个对象通过反射提供负载,生成token  
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="user"></param>
    /// <returns></returns>
    TnToken CreateToken<T>(T entity) where T : class;

    /// <summary>
    /// 根据键值对提供负载,生成token
    /// </summary>
    /// <param name="keyValuePairs"></param>
    /// <returns></returns>
    TnToken CreateToken(Dictionary<string, string> keyValuePairs);

}

以上接口的实现类如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Newtonsoft.Json;
using System.Security.Cryptography;

/// <summary>
/// Token帮助类
/// </summary>
public class TokenHelper : ITokenHelper
{
    //依赖注入配置项
    //private readonly IOptions<JwtTokenOption> _options;
    private readonly JwtTokenOption _options;
    /// <summary>
    /// 构造方法
    /// </summary>
    /// <param name="options"></param>
    public TokenHelper(JwtTokenOption options)
    {
        _options = options;
    }

    /// <summary>
    /// 根据一个对象通过反射提供负载,生成token  
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="user"></param>
    /// <returns></returns>
    public TnToken CreateToken<T>(T entity) where T : class
    {
        //定义声明的集合
        List<Claim> claims = new List<Claim>();

        //用反射把数据提供给它
        foreach (var item in entity.GetType().GetProperties())
        {
            object obj = item.GetValue(entity);
            string value = "";
            if(obj != null)
            {
                value = obj.ToString();
            }

            claims.Add(new Claim(item.Name, value));
        }

        //根据声明 生成token字符串
        return CreateTokenString(claims);
    }

    /// <summary>
    /// 根据键值对提供负载,生成token
    /// </summary>
    /// <param name="keyValuePairs"></param>
    /// <returns></returns>
    public TnToken CreateToken(Dictionary<string, string> keyValuePairs)
    {
        //定义声明的集合
        List<Claim> claims = new List<Claim>();

        foreach (var item in keyValuePairs)
        {
            claims.Add(new Claim(item.Key, item.Value));
        }

        //根据声明 生成token字符串
        return CreateTokenString(claims);
    }

    /// <summary>
    /// 私有方法,用于生成Token字符串
    /// </summary>
    /// <param name="claims"></param>
    /// <returns></returns>
    private TnToken CreateTokenString(List<Claim> claims)
    {
        //过期时间
        DateTime expires = DateTime.Now.AddMinutes(_options.AccessTokenExpiresMinutes);

        var token = new JwtSecurityToken(
            issuer: _options.Issuer,
            audience: _options.Audience,
            claims: claims,           //携带的荷载
            notBefore: DateTime.Now,  //token生成时间
            expires: expires,         //token过期时间
            signingCredentials: new SigningCredentials(
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.IssuerSigningKey)), SecurityAlgorithms.HmacSha256
                )
            );

        return new TnToken
        {
            Expires = expires,
            TokenStr = new JwtSecurityTokenHandler().WriteToken(token)
        };
    }
}

4. 在用户登录方法中,签发Token

//定义实例tokenHelper实例
private readonly ITokenHelper _tokenHelper;
//构造函数注入ITokenHelper实例 略

/// <summary>
/// 登录功能
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
[HttpPost]
public IActionResult Login(UserLoginDto user) //(1) 后端登录:账号密码的验证
{
    //对输入参数user进行验证,略
 	
    //定义一个响应信息的对象
    ResponseModel res = new ResponseModel();
    
    //检查用户是否存在(MD5对密码进行加密)
    var userInfo = _userService.CheckUserAndPwd(user.LoginName, user.Pwd);
    if (userInfo != null)
    {
        Dictionary<string, string> keyValuePairs = new Dictionary<string, string>
            {
                { "LoginName", user.LoginName },
                { "SuperAdmin", "true"} //假定用户属于SuperAdmin角色
            };
        res.Code = 200;
        res.Msg = "登录成功";
        
        //(2) 后端:帮助类来生成JWT字符串,JWT字符串返回给浏览器
        res.TokenInfo = _tokenHelper.CreateToken(keyValuePairs);
        return Ok(res);
    }
    else
    {
        res.Code = 401;
        res.Msg = "用户名或密码不正确";
        return Unauthorized(res);  //401的错误码
    }
}

5. 给API接口加身份授权锁

在Api控制器或者方法上,加[Authorize]特性,需要引用命名空间:

using Microsoft.AspNetCore.Authorization;

  • [Authorize]加在控制器上,则该控制器下所有API方法需要身份授权后才能访问;
  • [Authorize]加在方法上,则仅该方法需要身份授权后才能访问。

另外,有一个[AllowAnonymous]特性,加在Api控制器或者方法上,允许匿名访问该Api或者控制器下所有Api方法。

Api资源被锁保护起来之后,如果没有登录直接访问,则会报401的错误。在Swagger中测试截图如下:
在这里插入图片描述

6. Swagger中进行Token的测试

(1)在Program.cs的配置Swagger服务:

builder.Services.AddSwaggerGen(c =>{
	var basePath = AppContext.BaseDirectory;  //获取应用程序的所在目录
    //或者用下面的方式也能获取
    var basePath2 =Path.GetDirectoryName(typeof(Program).Assembly.Location);
    
	var xmlPath = System.IO.Path.Combine(basePath, "XfTech.Demo.xml"); //拼接XML文件所在路径
    
	//让Swagger显示方法、类的XML注释信息
	c.IncludeXmlComments(xmlPath, true);
	//设置Swagger文档参数
	c.SwaggerDoc("v1", new OpenApiInfo
	{
		Title = "XfTech.Demo",
		Version = "v1",
		Description = "Asp.Net Core6 WebApi开发实战",  //描述信息
		Contact = new OpenApiContact()                //开发者信息
		{
			Name = "物联网大联盟",               //开发者姓名
			Email = "99825949@qq.com",    //email地址
			Url = new Uri("https://blog.csdn.net/ousetuhou?type=blog") //作者的主页网站
		}
	});
	//开启Authorize权限按钮
	c.AddSecurityDefinition("JWTBearer", new OpenApiSecurityScheme()
	{
		Description = "这是方式一(直接在输入框中输入认证信息,不需要在开头添加Bearer) ",
		Name = "Authorization",        //jwt默认的参数名称
		In = ParameterLocation.Header,  //jwt默认存放Authorization信息的位置(请求头中)
		Type = SecuritySchemeType.Http,
		Scheme = "Bearer"
	});
	//定义JwtBearer认证方式二
	//options.AddSecurityDefinition("JwtBearer", new OpenApiSecurityScheme()
	//{
	//    Description = "这是方式二(JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格))",
	//    Name = "Authorization",//jwt默认的参数名称
	//    In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
	//    Type = SecuritySchemeType.ApiKey
	//});
	//声明一个Scheme,注意下面的Id要和上面AddSecurityDefinition中的参数name一致
	var scheme = new OpenApiSecurityScheme
	{
		Reference = new OpenApiReference()
		{
			Id = "JWTBearer",  //这个名字与上面的一样
			Type = ReferenceType.SecurityScheme
		}
	};
	//注册全局认证(所有的接口都可以使用认证)
	c.AddSecurityRequirement(new OpenApiSecurityRequirement
	{
		{ scheme, Array.Empty<string>() }
	});
});
#endregion

(2) 测试登录接口,输入正确的账号和密码,接口返回JWT令牌
在这里插入图片描述(3)点击“Authorize”按钮,在对话框中输入上一步获取到的令牌
在这里插入图片描述在这里插入图片描述(4)输入令牌后关闭对话框,右边的小锁为锁住的状态。接下来调用业务模块的Api会自动讲JWT令牌携带上。
在这里插入图片描述

四、Vue前台使用JWT认证进行安全防护

登录页面的布局略。以下只演示如何获取并缓存JWT令牌,以及在每个请求的时候携带上JWT令牌。

1. 登录成功后,用sessionStorage保存JWT令牌

this.$http({
	url:"/api/Account/Login",
	method:"post",
	data:this.loginForm,
}).then((res)=>{
	if(res.data.code>0){
	   this.$message(res.data.msg);
	   sessionStorage.setItem('jwtToken',res.data.tokenInfo.access_token) //保存到浏览器缓存
	   this.$router.push('home') //跳转到首页
	}
})

2. 发送请求前,用axios拦截器添加以下请求头

Authorization: Bearer jwt令牌字符串

// 在main.js中 添加请求拦截器
axios.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  var tkn = sessionStorage.getItem("jwtToken");
  if(tkn!="")
    config.headers.Authorization = 'Bearer ' + tkn
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});

本次对这个JWT认证进行了一个完整的封装演示。如果本文对你有帮助的话,请点赞+评论+关注,或者转发给需要的朋友。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/307942.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

docker 安装redis (亲测有效)

目录 1 安装 1 安装 1 将redis 的 tar 包 上传到服务器 上传之后tar 包&#xff0c;将他变成镜像 输入docker images,发现目前是没有镜像的&#xff0c;现在将tar 包变成镜像 docker load -i redis.tar以上就将tar 包变成镜像了 现在在宿主机找一个地方&#xff0c;存放数据…

跟我学java|Stream流式编程——并行流

什么是并行流 并行流是 Java 8 Stream API 中的一个特性。它可以将一个流的操作在多个线程上并行执行&#xff0c;以提高处理大量数据时的性能。 在传统的顺序流中&#xff0c;所有的操作都是在单个线程上按照顺序执行的。而并行流则会将流的元素分成多个小块&#xff0c;并在多…

DHSP和DNS

一、服务程序 1.1DHCP定义 DHCP&#xff08;动态主机配置协议&#xff09;是一个局域网的网络协议。指的是由服务器控制一段IP地址范围&#xff0c;客户机登录服务器时就可以自动获得服务器分配的IP地址和子网掩码。默认情况下&#xff0c;DHCP作为Windows Server的一个服务组…

java基础之Java8新特性-方法引入

目录 1.简介 2.方法引入 方法引入遵循规范 方法引入种类 1.静态方法引入 2.对象方法引入 3.实例方法引入 4.构造函数引入 1.简介 方法引用是 Java 8 中引入的另一个重要特性&#xff0c;它提供了一种简洁的语法来直接引用现有方法或构造函数。方法引用可以看作是 Lambd…

复试 || 就业day14(2024.01.10)算法篇

文章目录 前言字符串中第二大的数字字符串中不同整数的数目判断句子是否为全字母句长度为三且各字符不同的子字符串检查是否区域内所有整数都被覆盖*重新分配字符使所有字符串都相等可以输入的最大单词数检查是否所有字符出现次数相同差的绝对值为 K 的数对数目至少在两个数组中…

设计模式之外观模式【结构型模式】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档> 学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某…

数组计算模块NumPy

NumPy是Python数组计算、矩阵运算和科学计算的核心库。 提供了高性能的数组对象提供了大量的函数和方法NumPy使用机器学习中的操作变得简单NumPy是通过C语言实现的 NumPy的安装 pip install numpy 数组的分类 一维数组 跟Python列表的形状一样&#xff0c;区别在于数组的…

MongoDB—SQL到MongoDB映射图表

一、术语和概念 下表显示了各种 SQL 术语和概念 以及相应的 MongoDB 术语和概念。 SQL Terms/Concepts MongoDB Terms/Concepts database database table collection row document or BSON document column field index index table joins $lookup, embedded docu…

RV1126边缘计算AI盒子,支持4-6路1080p视频,2T 算力

1 产品概述 信迈推出基于瑞芯微Rockchip RV1126架构的AI边缘计算主板&#xff0c;RV1126芯片是四核ARM Cortex-A7,1.5GHz&#xff0c; RSIC-V 200MHz CPU &#xff0c;NPU2.0Tops。AI边缘计算主板外围接口丰富&#xff0c;拥有超强扩展性&#xff0c;可广泛应用在智慧安防、工…

贝锐蒲公英云智慧组网解读:实现工业设备远程调试、异地PLC互联

这个时候&#xff0c;使用异地组网是非常有效的解决方案。在12月28日贝锐官方的直播中&#xff0c;请到了贝锐蒲公英的技术研发经理&#xff0c;为大家分享了贝锐蒲公英云智慧组网解决方案&#xff0c;以及蒲公英二层组网相关的技术和应用。 搜索“贝锐”官方视频号&#xff0c…

AI数据和测试评价

1数据收集和清洗 为了训练AI模型,需要收集和准备足够的数据。数据应该涵盖各种情况和场景,以确保系统在各种情况下都能准确地运行。数据原始来源应该是真实的,并且应该涵盖系统预计的使用情况。数据应该根据特定的需求进行采样和处理,可以来自各种来源,例如公共数据集、第…

Oracle数据库19c OCP 1z0-082考场真题解析第24题

考试科目&#xff1a;1Z0-082 考试题量&#xff1a;90 通过分数&#xff1a;60% 考试时间&#xff1a;150min 本文为云贝教育郭一军guoyJoe原创&#xff0c;请尊重知识产权&#xff0c;转发请注明出处&#xff0c;不接受任何抄袭、演绎和未经注明出处的转载。【云贝教育】Orac…

Matlab 之数据分布拟合

文章目录 Part.I IntroductionPart.II Distribution Fitter APP 的使用Chap.I APP 简介Chap.II 简单使用 Part.III 通过代码实现分布拟合Chap.I 基于 fitdist 函数Chap.II 获取数据的频率分布后进行曲线拟合 Reference Part.I Introduction 本文主要介绍了如何使用 Matlab 对数…

【Verilog】期末复习——分别画出下面两个程序综合后的电路图/reg型数据和wire型数据的区别

系列文章 数值&#xff08;整数&#xff0c;实数&#xff0c;字符串&#xff09;与数据类型&#xff08;wire、reg、mem、parameter&#xff09; 运算符 数据流建模 行为级建模 结构化建模 组合电路的设计和时序电路的设计 有限状态机的定义和分类 期末复习——数字逻辑电路分…

设计模式-空对象模式

设计模式专栏 模式介绍模式特点应用场景空对象模式和单例模式的区别代码示例Java实现空对象模式Python实现空对象模式 空对象模式在spring中的应用 模式介绍 空对象模式是一种设计模式&#xff0c;用于处理对象不存在的情况。它通过返回一个空对象来代替 null 值&#xff0c;从…

视频号小店发展趋势如何?适合新手吗?

我是电商珠珠 视频号团队在22年7月发展了自己的电商平台-视频号小店。截止到目前为止&#xff0c;也发展了不过一年的时间&#xff0c;所以各项平台政策还不太严谨。 一个新兴平台所做的第一步就是招揽更多的商家来入驻&#xff0c;就会将红利全部倾向商家&#xff0c;而在今…

6.1.2捕捉图像(3)

6&#xff0e;文字捕捉 除了可以捕捉图像外&#xff0c;HyperSnap6还有一个非常神奇、非常实用的功能——文字捕捉。利用文字捕捉&#xff0c;可以把一段不可复制的文字捕捉下来&#xff0c;以便于重新编辑。 (1)右单击桌面上的“我的电脑”&#xff0c;在弹出的快捷菜单中选…

Kubernetes(K8S)云服务器实操TKE

一、 Kubernetes(K8S)简介 Kubernetes源于希腊语,意为舵手,因为首尾字母中间正好有8个字母,简称为K8S。Kubernetes是当今最流行的开源容器管理平台,是 Google 发起并维护的基于 Docker 的开源容器集群管理系统。它是大名鼎鼎的Google Borg的开源版本。 K8s构建在 Docker …

世微 AP6608 DC-DC转换器 1.2MHz 2-24V 2A升压转换IC

FEATURES? Integrated 80mΩ Power MOSFET ? 2V to 24V Input Voltage ? 1.2MHz Fixed Switching Frequency ? Internal 4A Switch Current Limit ? Adjustable Output Voltage ? Internal Compensation ? Up to 28V Output Voltage ? Automatic Pulse Frequency Modul…

聚道云软件连接器助力某电商企业实现电商业务与财务系统的数据互通

客户介绍&#xff1a; 某电商企业是一家集电商平台、仓储物流、售后服务等业务于一体的综合性电商企业。公司业务遍布全国多个城市&#xff0c;拥有庞大的客户群和销售额。 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 客户痛点&#xff1a; 有赞商…