上一篇我们使用IOC容器解决了依赖问题,同时简单配置了WebApi环境,本章我们使用一下Swagger,并通过Jwt完成认证
一、Swagger的使用
1、什么是Swagger
前后端分离项目中,后端人员开发完成后通常会编写API接口文档,说明方法对应的功能、参数等信息,也就是说前后端唯一的联系就是API接口,书写良好规范的API接口能极大的减缓前后端人员之间扯皮的频率。swagger则是能够让后端开发人员更加便捷的书写规范API文档的一款框架。
2、Swagger相关配置
- 使用NuGet搜索Swashbuckle.AspNetCore,安装在BlogSystem.Core项目中,如下:
-
打开Startup类,在ConfigureServices 方法中添加服务,如下:
//注册Swagger服务 services.AddSwaggerGen(options => { options.SwaggerDoc("V1",new OpenApiInfo { Version = "V1", Title = "BlogSystem API Doc-V1", Description = "BlogSystem API接口文档-V1版", Contact = new OpenApiContact { Name = "BlogSystem", Email = "xxx@xx.com" }, }); options.OrderActionsBy(x=>x.RelativePath); });
-
在打开Startup类的Configure方法中添加中间件,这里可以配置在开发环境,如下:
- 运行项目,成功显示Swagger页面,但是报了一个Not Found /swagger/v1/swagger.json错误,检查发现是版本号大小写不一致导致,将中间件配置中的v1改成大写后解决
3、Swagger注释功能
上面操作完成后,由于没有备注信息,不熟悉的人无法知道每个接口对应的功能,下面我们加上备注信息功能;
1、右击项目名称,选择属性—生成,勾选XML文档文件;
2、保存后发现多了很多成员注释的警告,身为强迫症坚决不能忍,再次打开生成页面,在取消显示警告中添加1591,保存,解决...这里的注释需要在方法名上使用三个左斜杠进行标注,我们可以在之前添加的注册方法上添加备注
3、这个时候还没完,在ConfigureServices 方法中注册服务指向刚刚添加的生成的xml文件路径,如下图
4、这个时候运行一下,发现ViewModel的注释没有显示,直接对Model层做上述相同操作,但是输出目录要选到当前BlogSystem.Core项目对应的路径,服务注册时不用带上第二个参数。完成后运行,终于OK
二、认证授权与Jwt
注:6月11日晚发现文章中的认证和授权的说法完全搞反了,向接收到错误讯息的朋友致歉,后面发文章时会仔细检查,避免误导大家
1、认证与授权
1.1、认证与授权的概念
首先,要区分认证与授权的概念。简单来说,认证:通过一些信息确认其身份;授权:确认该身份对应的权限
举个例子:一栋办公楼,只有工作人员才能进入,那么工作人员可以凭借员工卡证明自己的身份,进入大楼,即为认证;如果员工张三是办公楼内A公司的员工,那么他只能进出A公司,而李四是办公楼物管处的员工,那么他可以进出整栋办公楼的所有公司,即为授权。
1.2、实现的几种方法
Web应用程序是基于Http请求的,但是Http请求是无状态的,无法记住我们的登录状态,怎么办呢?
1、Cookie:常用的处理办法是将用户的登录信息以key-value的形式存在客户端浏览器的Cookie中,客户端每次发送请求时服务器端会判断并验证客户端有无对应的Cookie,存在则表示你是认证过的用户,可以进行认证后的操作;
2、Session方法:它是存储于服务器内存中key-value集合;客户端向服务器发送请求,如果请求头中没有SessionId,服务器会分配一个给客户端,并存放在客户端的Cookie中;如果请求头中有SessionId,则会带着该值一起发送到服务器,服务器根据Session找到认证信息,进行认证判断操作。其实现也需要基于客户端的Cookie
3、令牌验证:即在Http请求信息中加入令牌的信息,将用户的信息和令牌设定通过算法加密后存入Http请求信息中,客户端发送请求时也带上令牌信息来表明自己的身份,服务端进行权限的验证
这三种方法各有利弊,可以根据业务需求进行选择使用,这里我使用jwt仅仅是练习...
2、JWT介绍及问题
2.1、Jwt简介
Jwt是基于令牌的方法,网上资料很多,这里简单说明下,它由Header、Payload、Signature三部分组成。
-
Header包含了加密算法和加密的对象类型,它会经过BASE64编码后存入token中;
-
Payload用来存放一些声明信息,数据格式为键值对形式,官方定义了部分字段如签发人,签发时间,生效时间,过期时间等,当然我们也可以自定义添加,它同样会经过BASE64编码后存入token中;
-
Signature为密钥,这部分需要绝对保密,系统默认会使用Header中声明的算法将三者结合起来产生一串字符
2.2、使用Jwt的流程
-
用户成功登录后,服务器后端根据设定的令牌信息和用户信息进行加密生成一串加密的字符,返回给前端;
-
前端将后端返回的信息进行保存,一般会选择客户端浏览器中的Cookie或localStorage进行存放;
-
客户端发送请求前,会验证本地Cookie或localStorage中是否存在Jwt的信息,存在则加入Http请求头中;
-
服务器接受Http请求时,对Http请求头中的Jwt信息进行验证,验证通过后则认证成功
2.3、JWT存在的问题
1、安全性:有的朋友会说,将信息加入请求头,万一别人拦截复制我请求头中的信息使用怎么办呢?
- 任何方案都是有利有弊的,传统的session+cookie 方案,如果泄露了 sessionId同样会存在此类问题。其实只要做到以下几点就可以极大程度的避免此类情况:①使用https 加密Web应用;②将jwt存入cookie中;③返回 jwt 给客户端时设置 httpOnly=true,能有效阻止XSS 攻击和 CSRF 攻击
2、注销和修改密码问题:传统的 session+cookie 方案用户点击注销,服务端清空 session 即可;但是Jwt是无状态的,且服务端没有保存,即使客户端删除了JWT,它仍然是在有效期内的,相应的解决办法如下
- ①删除客户端的Cookie,但如果用户通过某种手段记住且在请求头中添加了JWT,在有效期内仍然是可以访问的,即使是在这段时间内修改了密码的情况下;②将JWT令牌中的Secret设置为和用户相关的动态数值,用户注销后改变Secret的值,但这样JWT是不变的,使用原先的JWT会无法登录;③借助第三方,如NoSql数据库存储JWT的状态,但这违背了JWT无状态的特性
3、续签问题:payload中会存储一个有效期时间,时间一到就无法访问了;传统的 session+cookie 是会自动续签的,所有没有这个问题。对应的几个解决方案如下:
- ①快要过期的时候刷新 jwt,这个只有快到期时用户访问了网站才有机会触发;②第三方记录过期时间,每次访问刷新过期时间;③每次请求刷新过期时间,有点暴力...会有性能影响;④将第一、第三条方案中和一下,每次访问都判断过期时间是否在预设的一个时间段内,在就刷新
三、配置使用JWT
这里我们先完成认证部分的功能,注销、续签、以及授权验证功能等后续再进一步完善
1、配置文件
首先我们在appsettings.json文件中加上Payload需要用到的字段信息,如下:
2、注册Jwt
ASP.NET Core中已经封装了认证方法,在Action方法上方添加[Authorize],即表示需要认证才能访问;如何进行认证呢?那就需要我们来定义了,使用NuGet包安装下方插件,在StartUp类中进行服务的注册,如下Microsoft.AspNetCore.Authentication.JwtBearer
3、启用中间件
同时我们需要在Startup另一个方法中启用认证中间件,这里把授权中间件一并加上,需要注意其顺序
4、JWT方法封装
接下来的问题是用户信息如何封装为Jwt令牌,我们在BlogSystem.Core项目下建一个Helpers文件夹,再新建一个JwtHelper类,添加对应的封装方法和解析方法。功能如下:
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace BlogSystem.Mvc.Helpers
{
public static class JwtHelper
{
private static IConfiguration _configuration;
//获取Startup构造函数中的Configuration对象
public static void GetConfiguration(IConfiguration configuration)
{
_configuration = configuration;
}
/// <summary>
/// Jwt加密
/// </summary>
/// <param name="tokenModel"></param>
/// <returns></returns>
public static string JwtEncrypt(TokenModelJwt tokenModel)
{
//获取配置文件中的信息
var iss = _configuration["JwtTokenManagement:issuer"];
var aud = _configuration["JwtTokenManagement:audience"];
var secret = _configuration["JwtTokenManagement:secret"];
//设置声明信息
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Jti, tokenModel.UserId.ToString()),//Jwt唯一标识Id
new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"),//令牌签发时间
new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,//不早于的时间声明
new Claim(JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddHours(24)).ToUnixTimeSeconds()}"),//令牌过期时间
new Claim(ClaimTypes.Expiration, DateTime.Now.AddHours(24).ToString(CultureInfo.CurrentCulture)),//令牌截至时间
new Claim(JwtRegisteredClaimNames.Iss,iss),//发行人
new Claim(JwtRegisteredClaimNames.Aud,aud),//订阅人
new Claim(ClaimTypes.Role,tokenModel.Level)//权限——目前只支持单权限
};
//密钥处理,key和加密算法
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
//封装成jwt对象
var jwt = new JwtSecurityToken(
claims: claims,
signingCredentials: cred
);
//生成返回jwt令牌
return new JwtSecurityTokenHandler().WriteToken(jwt);
}
/// <summary>
/// Jwt解密
/// </summary>
/// <param name="jwtStr"></param>
/// <returns></returns>
public static TokenModelJwt JwtDecrypt(string jwtStr)
{
if (string.IsNullOrEmpty(jwtStr)||string.IsNullOrWhiteSpace(jwtStr))
{
return new TokenModelJwt();
}
jwtStr = jwtStr.Substring(7);//截取前面的Bearer和空格
var jwtHandler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
jwtToken.Payload.TryGetValue(ClaimTypes.Role, out object level);
var model = new TokenModelJwt
{
UserId = Guid.Parse(jwtToken.Id),
Level = level == null ? "" : level.ToString()
};
return model;
}
}
/// <summary>
/// 令牌包含的信息
/// </summary>
public class TokenModelJwt
{
public Guid UserId { get; set; }
public string Level { get; set; }
}
}
其中我们把Configuration对象在startup构造函数中传递了过来
5、Jwt加密功能测试
1、修改注册功能和添加登录功能,如下:
/// <summary>
/// 用户注册
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost(nameof(Register))]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (!await _userService.Register(model))
{
return Ok("用户已存在");
}
return Ok("创建成功");
}
/// <summary>
/// 用户登录
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost("Login", Name = nameof(Login))]
public async Task<IActionResult> Login(LoginViewModel model)
{
//判断账号密码是否正确
var userId = await _userService.Login(model);
if (userId == Guid.Empty) return Ok("账号或密码错误!");
//登录成功进行jwt加密
var user = await _userService.GetOneByIdAsync(userId);
TokenModelJwt tokenModel = new TokenModelJwt { UserId = user.Id, Level = user.Level.ToString() };
var jwtStr = JwtHelper.JwtEncrypt(tokenModel);
return Ok(jwtStr);
}
2、这里我们使用Swagger接口进行测试,输入账号密码,成功拿到加密字符,如下:
6、认证测试
1、为了测试认证,我们新增一个方法,在上方标注【Authorize】,如下:
[Authorize]
[HttpPost("Test")]
public ActionResult Test()
{
return Ok("测试");
}
2、测试如下,401错误未认证,无法访问
3、我们使用PostMan在请求头中插入Token,注意前面加了Bearer和一个空格,返回200成功执行
7、Swagger配置认证功能
上面可以看到,需要测试认证功能时,我们只能通过PostMan在请求头插入Token信息,Swagger其实也是可以添加认证功能的,下面我们来配置一下环境
1、使用NuGet安装如下插件,如下:
2、在StartUp类的ConfigureService方法的Swagger注册服务中添加如下信息,其中方法名一定要为oauth2,不知道为什么
3、运行一下,右上角出现了锁,使用登录得到加密字符后,点击右上角Authorize输入Bearer+空格+加密字符,表示已认证后,选择上面建的测试方法,点击执行,成功返回200状态码
本章完~
本人知识点有限,若文中有错误的地方请及时指正,方便大家更好的学习和交流。
本文部分内容参考了网络上的视频内容和文章,仅为学习和交流,地址如下:
老张的哲学,系列教程一目录:.netcore+vue 前后端分离
徐靖峰,深入理解JWT的使用场景和优劣