【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

时间:2023-03-09 17:21:48
【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

系列目录

一.  创建项目并集成swagger

  1.1 创建

  1.2 完善

二. 搭建项目整体架构

三. 集成轻量级ORM框架——SqlSugar

  3.1 搭建环境

  3.2 实战篇:利用SqlSugar快速实现CRUD

  3.3 生成实体类

四. 集成JWT授权验证

五. 实现CORS跨域

六. 集成泛型仓储

七. 授权认证进阶篇


源码已上传上传GitHub:https://github.com/WangRui321/RayPI

该篇是第四篇“实战!带你半小时实现接口的授权认证”的进阶篇。

先说一下之前的版本:

之前在第四篇的时候曾经试着集成过一次JWT授权认证,当时搭的第一版是JWT本身的授权认证机制,但是为了实现“令牌”的滑动过期效果,结果最后改成了使用缓存机制。

所以写到最后发现,其实就是变相的Session认证机制,因为发放“令牌”的时候完全可以不用JWT,直接生成一个GUID也是可以的。

后来想了一下,这样为了实现令牌滑动过期而破坏了授权认证的独立性,感觉得不偿失。于是就决定”进阶“下,在授权认证模块去掉缓存机制,只使用JWT本身的验证机制。

另外,这次还添加了一些关于对身份验证的优化。之前一个接口只能标明允许一种身份的用户访问,修改后可以实现一个接口同时允许多个身份访问(比如同时允许客户端和后台管理员两种身份的令牌访问)。

BTW,为了完整性考虑,下面有部分内容和之前第四篇相同,有需要的可以跳着看。

1. 根

根据*定义,JWT(读作 [/dʒɒt/]),即JSON Web Tokens,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)规范。

JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。它是一种用于双方之间传递安全信息的表述性声明规范。

JWT作为一个开放的标准(RFC ),定义了一种简洁的、自包含的方法,从而使通信双方实现以JSON对象的形式安全的传递信息。

以上是JWT的官方解释,可以看出JWT并不是一种只能权限验证的工具,而是一种标准化的数据传输规范。所以,只要是在系统之间需要传输简短但却需要一定安全等级的数据时,都可以使用JWT规范来传输。规范是不因平台而受限制的,这也是JWT做为授权验证可以跨平台的原因。

如果理解还是有困难的话,我们可以拿JWT和JSON类比:

JSON是一种轻量级的数据交换格式,是一种数据层次结构规范。它并不是只用来给接口传递数据的工具,只要有层级结构的数据都可以使用JSON来存储和表示。当然,JSON也是跨平台的,不管是Win还是Linux,.NET还是Java,都可以使用它作为数据传输形式。

该篇的主要目的是实战,所以关于JWT本身的优点,以及使用JWT作为系统授权验证的优缺点,这里就不细说了,感兴趣的可以自己去查阅相关资料。

1.1 在授权验证系统中,JWT是怎么工作的呢?

如果将JWT运用到Web Api的授权验证中,那么它的工作原理是这样的:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

1)客户端向授权服务系统发起请求,申请获取“令牌”。

2)授权服务根据用户身份,生成一张专属“令牌”,并将该“令牌”以JWT规范返回给客户端

3)客户端将获取到的“令牌”放到http请求的headers中后,向主服务系统发起请求。主服务系统收到请求后会从headers中获取“令牌”,并从“令牌”中解析出该用户的身份权限,然后做出相应的处理(同意或拒绝返回资源)

可以看出,JWT授权服务是可以脱离我们的主服务系统而作为一个独立系统存在的。

1.2 令牌是什么?JWT就是令牌吗?

前面说了其实把JWT理解为一种规范更为贴切,但是往往大家把根据JWT规则生成的加密字符串也叫作JWT,还有人直接称呼JWT为令牌。本文为了阐述方便,特此做了一些区分:

1.2.1 JWT:

本文所说的JWT皆指的是JWT规范

1.2.2 JWT字符串:

本文所说的“JWT字符串”是指通过JWT规则加密后生成的字符串,它由三部分组成:Header(头部)、Payload(数据)、Signature(签名),将这三部分由‘.’连接而组成的一长串加密字符串就成为JWT字符串。

1)Header

由且只由两个数据组成,一个是“alg”(加密规范)指定了该JWT字符串的加密规则,另一个是“typ”(JWT字符串类型)。例如:

{
"alg": "HS256",
"typ": "JWT"
}

将这组JSON格式的数据通过Base64Url格式编码后,生成的字符串就是我们JWT字符串的第一个部分。

2)Payload

由一组数据组成,它负责传递数据,我们可以添加一些已注册声明,比如“iss”(JWT字符串的颁发人名称)、“exp”(该JWT字符串的过期时间)、“sub”(身份)、“aud”(受众),除了这些,我们还可根据需要添加自定义的需要传输的数据,一般是发起请求的用户的信息。例如:

{
“iss”:"RayPI",
"sub": "Client",
"name": "张三",
"uid":
}

将该JSON格式的数据通过Base64Url格式编码后,生成的字符串就是我们JWT字符串的第二部分。

3)Signature

数字签名,由4个因素所同时决定:编码后的header字符串,编码后的payload字符串,之前在头部声明的加密算法,我们自定义的一个秘钥字符串(secret)。例如:

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

所以签名可以安全地验证一个JWT的合法性(有没有被篡改过)。

最后,给一个实际生成后的JWT字符串的完整样例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJDbGllbnQiLCJqdGkiOiIwZTRjYzVkNC0yMmIzLTQwYzUtOTBjMy0wOTk0MjFjNWRjMjkiLCJpYXQiOiIyMDE4LzcvMyAyOjE3OjQ5IiwiZXhwIjoxNTMwNjI3NDY5LCJpc3MiOiJSYXlQSSJ9.98pAaDVhNwVfiSHQVeXKhYE2ML6WK_f9rYC-iwyQEpU

我们可以拿着这个JWT字符串到https://jwt.io/#debugger试着解析出前两部分的内容。

1.2.3 令牌:

本文的“令牌”指的是用于http传输headers中用于验证授权的JSON数据,它是key和value两部分组成,在本文中,key为“Authorization”,value为“Bearer {JWT字符串}”,其中value除了JWT字符串外,还在前面添加了“Bearer ”字符串,这里可以把它理解为大家约约定俗成的规定即可,没有实际的作用。例如:

{ "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJDbGllbnQiLCJqdGkiOiIwZTRjYzVkNC0yMmIzLTQwYzUtOTBjMy0wOTk0MjFjNWRjMjkiLCJpYXQiOiIyMDE4LzcvMyAyOjE3OjQ5IiwiZXhwIjoxNTMwNjI3NDY5LCJpc3MiOiJSYXlQSSJ9.98pAaDVhNwVfiSHQVeXKhYE2ML6WK_f9rYC-iwyQEpU" }

1.3 授权?认证?傻傻分不清楚

先问一个问题:认证和授权是一回事吗?

答案显然是否定的。因为大家都喜欢把这两个词放在一起说,所以很容易就混淆了他们含义。在.NET Core中,认证的单词是“Authentication”,而授权的单词是“Authorization”。

认证,验证身份的意思。即验证当前请求的用户是否为合法用户(放在当前场景下,就是验证用户携带的令牌是否为一个合法令牌);

授权,给用户颁发权限的意思。即给验证通过的用户授予相应的权限(放在当前场景下,就是根据令牌中解析出的用户身份,赋予该http请求,该http请求使用该身份就可以访问对应的接口)

所以,我们下面要实现的总体思路是:

在每个接口上都标明该接口允许什么样的身份访问(比如“Client”代表客户端,“Admin”代表后台管理员)。

在用户登录成功后,我们将该用户的身份(是Client还是Admin)等信息生成JWT规范的令牌返回。客户端将返回的令牌存储好(一般是存在Cookie中),以后每次调用接口都要将该令牌携带上。

服务端收到请求后,提取令牌,先进行认证,如果不合法(比如被篡改),将驳回请求。如果认证通过,则从令牌中提取身份,进行授权操作,将该身份赋予http请求,放行请求。

请求进到接口后会和接口标明的允许访问身份进行匹配,如果该接口允许该身份访问,则返回相应请求资源,如果不允许,则驳回请求。

思路明白了,下面实战起来就不会乱了。

2. 道

搭建完的项目架构应该是这样的:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

这里有三块工作区域:

1)JwtHelper是一个Jwt帮助类,里面有两个函数,一个函数帮助生成Jwt字符串并返回,一个帮助从Jwt字符串逆向解析出数据。

2)JwtAuthorizationFilter.cs是一个授权中间件

3)Startup中添加了认证服务和授权服务

2.1 JwtHelper

using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
//
using RayPI.Model.ConfigModel; namespace RayPI.Helper
{
public class JwtHelper
{
/// <summary>
/// 颁发JWT字符串
/// </summary>
/// <param name="tokenModel"></param>
/// <returns></returns>
public static string IssueJWT(TokenModel tokenModel)
{
var dateTime = DateTime.UtcNow;
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Jti,tokenModel.Uid.ToString()),//用户Id
new Claim("Role", tokenModel.Role),//身份
new Claim("Project", tokenModel.Project),//身份
new Claim(JwtRegisteredClaimNames.Iat,dateTime.ToString(),ClaimValueTypes.Integer64)
};
//秘钥
var jwtConfig = new JwtAuthConfigModel();
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.JWTSecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
//过期时间
double exp = ;
switch (tokenModel.TokenType)
{
case "Web":
exp = jwtConfig.WebExp;
break;
case "App":
exp = jwtConfig.AppExp;
break;
case "MiniProgram":
exp = jwtConfig.MiniProgramExp;
break;
case "Other":
exp = jwtConfig.OtherExp;
break;
}
var jwt = new JwtSecurityToken(
issuer: "RayPI",
claims: claims, //声明集合
expires: dateTime.AddHours(exp),
signingCredentials: creds); var jwtHandler = new JwtSecurityTokenHandler();
var encodedJwt = jwtHandler.WriteToken(jwt); return encodedJwt;
} /// <summary>
/// 解析
/// </summary>
/// <param name="jwtStr"></param>
/// <returns></returns>
public static TokenModel SerializeJWT(string jwtStr)
{
var jwtHandler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
object role = new object(); ;
object project = new object();
try
{
jwtToken.Payload.TryGetValue("Role", out role);
jwtToken.Payload.TryGetValue("Project", out project);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
var tm = new TokenModel
{
Uid = long.Parse(jwtToken.Id),
Role = role.ToString(),
Project = project.ToString()
};
return tm;
}
} /// <summary>
/// 令牌
/// </summary>
public class TokenModel
{
/// <summary>
/// 用户Id
/// </summary>
public long Uid { get; set; }
/// <summary>
/// 身份
/// </summary>
public string Role { get; set; }
/// <summary>
/// 项目名称
/// </summary>
public string Project { get; set; }
/// <summary>
/// 令牌类型
/// </summary>
public string TokenType { get; set; }
}
}

JwtHelper

其中JwtAuthConfigModel是一个存储配置文件的类,里面读取了配置文件中的jwt秘钥和几个过期时间。这里就不放了,可以在代码里直接写死。

2.2. JwtAuthorizationFilter

using Microsoft.AspNetCore.Http;
using RayPI.Bussiness;
using RayPI.Helper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks; namespace RayPI.AuthHelp
{
/// <summary>
/// 授权中间件
/// </summary>
public class JwtAuthorizationFilter
{
private readonly RequestDelegate _next; /// <summary>
///
/// </summary>
/// <param name="next"></param>
public JwtAuthorizationFilter(RequestDelegate next)
{
_next = next;
} /// <summary>
///
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public Task Invoke(HttpContext httpContext)
{
//检测是否包含'Authorization'请求头,如果不包含则直接放行
if (!httpContext.Request.Headers.ContainsKey("Authorization"))
{
return _next(httpContext);
}
var tokenHeader = httpContext.Request.Headers["Authorization"];
tokenHeader = tokenHeader.ToString().Substring("Bearer ".Length).Trim(); TokenModel tm = JwtHelper.SerializeJWT(tokenHeader); //BaseBLL.TokenModel = tm;//将tokenModel存入baseBll //授权
var claimList = new List<Claim>();
var claim = new Claim(ClaimTypes.Role, tm.Role);
claimList.Add(claim);
var identity = new ClaimsIdentity(claimList);
var principal = new ClaimsPrincipal(identity);
httpContext.User = principal; return _next(httpContext);
}
}
}

JwtAuthorizationFilter

2.3 Startup

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

完整代码:

using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.PlatformAbstractions;
using Swashbuckle.AspNetCore.Swagger;
using RayPI.Model.ConfigModel;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using RayPI.AuthHelp; namespace RayPI
{
/// <summary>
///
/// </summary>
public class Startup
{
/// <summary>
///
/// </summary>
/// <param name="env"></param>
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); this.Configuration = builder.Build(); BaseConfigModel.SetBaseConfig(Configuration);
}
/// <summary>
///
/// </summary>
public IConfiguration Configuration { get; } /// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";//设置时间格式
}); #region Swagger
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Version = "v1.1.0",
Title = "Ray WebAPI",
Description = "框架集合",
TermsOfService = "None",
Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "RayWang", Email = "2271272653@qq.com", Url = "http://www.cnblogs.com/RayWang" }
});
//添加注释服务
var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var apiXmlPath = Path.Combine(basePath, "APIHelp.xml");
var entityXmlPath = Path.Combine(basePath, "EntityHelp.xml");
c.IncludeXmlComments(apiXmlPath, true);//控制器层注释(true表示显示控制器注释)
c.IncludeXmlComments(entityXmlPath); //添加控制器注释
//c.DocumentFilter<SwaggerDocTag>(); //添加header验证信息
//c.OperationFilter<SwaggerHeader>();
var security = new Dictionary<string, IEnumerable<string>> { { "Bearer", new string[] { } }, };
c.AddSecurityRequirement(security);//添加一个必须的全局安全信息,和AddSecurityDefinition方法指定的方案名称要一致,这里是Bearer。
c.AddSecurityDefinition("Bearer", new ApiKeyScheme
{
Description = "JWT授权(数据将在请求头中进行传输) 参数结构: \"Authorization: Bearer {token}\"",
Name = "Authorization",//jwt默认的参数名称
In = "header",//jwt默认存放Authorization信息的位置(请求头中)
Type = "apiKey"
});
});
#endregion #region 认证
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
JwtAuthConfigModel jwtConfig=new JwtAuthConfigModel();
o.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "RayPI",
ValidAudience = "wr",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.JWTSecretKey)), /***********************************TokenValidationParameters的参数默认值***********************************/
RequireSignedTokens = true,
// SaveSigninToken = false,
// ValidateActor = false,
// 将下面两个参数设置为false,可以不验证Issuer和Audience,但是不建议这样做。
ValidateAudience = false,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
// 是否要求Token的Claims中必须包含 Expires
RequireExpirationTime = true,
// 允许的服务器时间偏移量
// ClockSkew = TimeSpan.FromSeconds(300),
// 是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
ValidateLifetime = true
};
});
#endregion #region 授权
services.AddAuthorization(options =>
{
options.AddPolicy("RequireClient", policy => policy.RequireRole("Client").Build());
options.AddPolicy("RequireAdmin", policy => policy.RequireRole("Admin").Build());
options.AddPolicy("RequireAdminOrClient", policy => policy.RequireRole("Admin,Client").Build());
});
#endregion #region CORS
services.AddCors(c =>
{
c.AddPolicy("Any", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}); c.AddPolicy("Limit", policy =>
{
policy
.WithOrigins("localhost:8083")
.WithMethods("get", "post", "put", "delete")
//.WithHeaders("Authorization");
.AllowAnyHeader();
});
});
#endregion
} /// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} #region Swagger
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiHelp V1");
});
#endregion //认证
app.UseAuthentication(); //授权
app.UseMiddleware<JwtAuthorizationFilter>(); app.UseMvc(); app.UseStaticFiles();//用于访问wwwroot下的文件
}
}
}

Startup

Tips:

这里有一个坑,不太了解依赖注入和中间件的人很容易踩到(其实就是我自己了)

在Startup.cs的Configure函数中,里面每个app.UseXXXXX();是有一定顺序。可以理解为,这里添加中间件的顺序就是客户端发起http请求时所经过的顺序。

之前我因为把“app.UseMvc();”写到了认证授权上面去了,结果导致怎么Debug都找不到问题。。。

所以一定要先app.UseAuthentication()认证,然后app.UseMiddleware<JwtAuthorizationFilter>()授权,最后再app.UseMvc()

3.果

搭建完成之后,下面就是测试了。

选择一个测试控制器,在其头上标注[Authorize]属性

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

(Policy和Roles两种写法是一个意思,但是必须要是Startup中已经申明的,否则Swagger会直接报错)

F5运行,在swagger ui中调用一个需要授权验证的接口(根据Id获取学生信息)

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

输入1,先不进行任何授权认证的操作,直接点击Excute尝试调用,返回结果如下:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

状态码500,还返回了一大段html代码,我们可以将接口的完整地址输入到浏览器地址栏进行访问,就可以看到这段html代码的页面了:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

可以看到接口返回了一个错误页,原因就是中间件在http请求的头部(headers)中没有找到“Authorization"字段里的”令牌“。

现在,我们先调用获取JWT接口(实际项目中不应该有该接口,分发令牌的功能应该集成到登陆功能中,但是这里为了简单直观,我将分发令牌的功能直接写成了接口,以供测试),输入相应的客户端信息,Excute:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

接口会生成”令牌“,返回JWT字符串:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

我们要复制这串JWT字符串,然后将其添加到http请求的Headers中去。测试方法有两个:

1)可以新建一个html页面,模拟前端写个ajax调用接口,在ajax添加headers字段,如下:

$.ajax({
url: "http://localhost:3608/api/Admin/Student/1",
type: ”get“,
dataType: "json",
//data: {},
async: false,
          //手动高亮
headers: { "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJBZG1pbiIsImp0aSI6IjhjMDEwMzI2LTE4M2MtNGQ5ZC1iMDFjLWFjM2EzNTIzODYxOCIsImlhdCI6IjIwMTgvNy8yIDE1OjAzOjQ4IiwiZXhwIjoxNTMwNTg3MDI4LCJpc3MiOiJSYXlQSSJ9.1Bb7hwoDD12n8ymcQsu79Xm-GDq14GERhS9b-1l1kmg" },
success: function (d) {
alert(JSON.stringify(d));
},
error: function (d) {
alert(JSON.stringify(d))
}
});

2)如果你的swagger像我一样,集成了添加Authrize头部功能,那么可以点击这个按钮进行添加(如果你的swagger看不到这个按钮,可以参考我之前的章节【从零开始搭建自己的.NET Core Api框架】(一)创建项目并集成swagger:1.2 完善,对swagger进行相关的设置)

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

这里除了JWT字符串外,前面还需要手动写入“Bearer ”(有一个空格)字符串。点击Authorize保存"令牌"。

再次调用刚才的”根据id获取学生信息“接口,发现获取成功:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

可以看到swagger向http请求的headers中添加了我们刚才保存的”令牌“。

参考内容:

https://jwt.io/

https://www.cnblogs.com/webenh/p/9039322.html