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。

上图流程说明:

  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的参数,如过期时间,密钥等。

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

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

csharp 复制代码
/// <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配置选项:

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

2. 配置Jwt身份认证方式

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

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

csharp 复制代码
//配置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令牌的帮助类

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

csharp 复制代码
/// <summary>
/// 存放Token 跟过期时间的模型类
/// </summary>
public class TnToken
{
    /// <summary>
    /// token字符串
    /// </summary>
    public string TokenStr { get; set; }
    /// <summary>
    /// token过期时间
    /// </summary>
    public DateTime Expires { get; set; }
}
csharp 复制代码
/// <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; }
}
csharp 复制代码
/// <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);

}

以上接口的实现类如下:

csharp 复制代码
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

csharp 复制代码
//定义实例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服务:

csharp 复制代码
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令牌

javascript 复制代码
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令牌字符串

javascript 复制代码
// 在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认证进行了一个完整的封装演示。如果本文对你有帮助的话,请点赞+评论+关注,或者转发给需要的朋友。

相关推荐
Pandaconda9 分钟前
【Golang 面试题】每日 3 题(三十九)
开发语言·经验分享·笔记·后端·面试·golang·go
编程小筑44 分钟前
R语言的编程范式
开发语言·后端·golang
技术的探险家1 小时前
Elixir语言的文件操作
开发语言·后端·golang
ss2731 小时前
【2025小年源码免费送】
前端·后端
Ai 编码助手1 小时前
Golang 中强大的重试机制,解决瞬态错误
开发语言·后端·golang
齐雅彤2 小时前
Lisp语言的区块链
开发语言·后端·golang
齐雅彤2 小时前
Lisp语言的循环实现
开发语言·后端·golang
梁雨珈2 小时前
Lisp语言的物联网
开发语言·后端·golang
邓熙榆3 小时前
Logo语言的网络编程
开发语言·后端·golang
羊小猪~~7 小时前
MYSQL学习笔记(四):多表关系、多表查询(交叉连接、内连接、外连接、自连接)、七种JSONS、集合
数据库·笔记·后端·sql·学习·mysql·考研