阅读本文你的收获
- 了解JWT身份认证的流程
- 了解基于JWT身份认证和Session身份认证的区别
- 学习如何在ASP.NET Core WebAPI项目中封装JWT认证功能
在上文ASP.NET Core高级之认证与授权(一)–JWT入门-颁发、验证令牌中演示了JWT认证的一个入门案例,本文是一个基于JWT认证的完整的前后端实现代码案例。
一、基于JWT的用户认证
JWT身份认证的流程
在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。
无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema。
上图流程说明:
- 用户携带用户名和密码请求认证
- 服务器校验用户账号密码,成功则提供一个token给客户端
- 客户端存储token,并且在随后的每一次请求中都带着它
- 服务器校验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认证进行了一个完整的封装演示。如果本文对你有帮助的话,请点赞+评论+关注,或者转发给需要的朋友。