前言

这是StarBlog系列在2023年的第二篇更新

这几个月都在忙,更新变得很不勤快,但是拖着不更新我的心里更慌,很久没写,要开头就变得很难

说回正题,之前的文章里,我们已经把博客关键的接口都开发完成了,但还少了一个最关键的「认证授权」,少了这东西,网站就跟筛子一样,谁都可以来添加和删除数据,乱套了~

关于「认证授权」的知识,会比较复杂,要学习这块的话,建议分几步:

  • 基础概念
  • AspNetCore 的 Identity 框架
  • 其他框架,如 IdentityServer

关于基础概念可以看看我之前写的这篇: Asp-Net-Core学习笔记:身份认证入门

PS:Identity 框架的还没写好

为了避免当复读机,本文就不涉及太多概念的东西了,建议先看完上面那篇再来开始使用JWT~

JWT

前面介绍文章的CRUD接口时,涉及到修改的接口,都加了 [Authorize] 特性,表示需要登录才能访问,本文就以最简单的方式来实现这个登录认证功能。

在 AspNetCore 中,使用 JWT 的工作流程大概如下:

  • JWT就是一个Base64编码的字符串,分为 head/payload/sign 三个部分(sign签名是使用特定秘钥生成的,别人无法伪造,所以就算修改了payload部分的信息,后端校验也不会通过)
  • 用户登录时,后端可以在里面存一些类似用户ID、邮箱、手机号之类的数据,然后把这串东西返回给前端存储,注意不要把不能被客户端知道的信息放在里面(也可以对payload进行加密)
  • 之后调用需要登录的接口时,都要带上这个JWT(一般是放在 HTTP Header 里面)
  • 这串东西只有后端能解析,后端拿到之后就知道用户的身份了

JWT 还有其他一些特性,比如说是没有状态的,这就很符合我们用的 RESTFul 接口了,不像传统使用 session 和 cookies 那样,原版 JWT 只要签发之后,在有效期结束前就不能取消,用户也没法注销,为了避免泄露 JWT token 导致安全问题,一般过期时间都设置得比较短。(这个不能取消的问题,也是可以通过曲线救国解决的,不过不在本文的讨论范围哈)

初步接触 JWT

OK,说了那么多,还是开始来写代码吧

生成 JWT

要生成的话很简单,不需要什么额外的配置,几行代码就搞定了

public LoginToken GenerateLoginToken(User user) {
var claims = new List<Claim> {
new(JwtRegisteredClaimNames.Sub, user.Id), // User.Identity.Name
new(JwtRegisteredClaimNames.GivenName, user.Name),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("jwt key"));
var signCredential = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var jwtToken = new JwtSecurityToken(
issuer: "jwt issuer 签发者",
audience: "jwt audience 接受者",
claims: claims,
expires: DateTime.Now.AddDays(7),
signingCredentials: signCredential); return new LoginToken {
Token = new JwtSecurityTokenHandler().WriteToken(jwtToken),
Expiration = TimeZoneInfo.ConvertTimeFromUtc(jwtToken.ValidTo, TimeZoneInfo.Local)
};
}

最开始的 claims 就是前面说的后端往JWT里面存的数据

"The set of claims associated with a given entity can be thought of as a key. The particular claims define the shape of that key; much like a physical key is used to open a lock in a door. In this way, claims are used to gain access to resources." from MSDN

Claim 的构造方法可以接收 keyvalue 参数,都是字符串

对于 key ,.Net 提供了一些常量,在 JwtRegisteredClaimNamesClaimTypes 类里边,这俩的区别就是后者是老的,一般在Windows体系下使用,比如说同样是 Name 这个 key

  • ClaimTypes.Name = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
  • JwtRegisteredClaimNames.Name = "name"

我们是在 JWT 里面设置 Claim,用 JwtRegisteredClaimNames 就好了

参考:https://stackoverflow.com/questions/50012155/jwt-claim-names

从 JWT 中读取信息

也就是读取放在里面的各个 Claim

在正确配置 Authentication 服务和 JwtBearer 之后,已登录的客户端请求过来,后端可以在 Controller 里面拿到 JWT 数据

像这样

var name = HttpContext.User.FindFirst(JwtRegisteredClaimNames.Name)?.Value;

还可以用 System.Security.Claims.PrincipalExtensions 的扩展方法 FindFirstValue 直接拿到字符串值。

吐槽:如果对应的 Claim 不存在的话,这个扩展方法返回的值是 null,但不知道为啥,他源码用的是 string 作为返回值类型,而不是 string? ,真是令人遗憾

使用 JWT 保护接口

了解 JWT 的使用方式之后,终于可以把 JWT 应用到博客项目中了~

配置JWT参数

为了避免硬编码,我们把 JWT 需要的 Issuer, Audience, Key 三个参数写在配置里面

形式如下

"Auth": {
"Jwt": {
"Issuer": "starblog",
"Audience": "starblog-admin-ui",
"Key": "F2REaFzQ6xA9k77EUDLf9EnjK5H2wUot"
}
}

接着需要定义一个类来方便映射配置。

StarBlog.Web/Models/Config 下添加 Auth.cs

public class Auth {
public Jwt Jwt { get; set; }
} public class Jwt {
public string Issuer { get; set; }
public string Audience { get; set; }
public string Key { get; set; }
}

注册一下

builder.Services.Configure<Auth>(configuration.GetSection(nameof(Auth)));

配置 Authentication 服务

这部分代码比较多,写成扩展方法,避免 Program.cs 文件代码太多

添加 StarBlog.Web/Extensions/ConfigureAuth.cs 文件

public static class ConfigureAuth {
public static void AddAuth(this IServiceCollection services, IConfiguration configuration) {
services.AddScoped<AuthService>();
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options => {
var authSetting = configuration.GetSection(nameof(Auth)).Get<Auth>();
options.TokenValidationParameters = new TokenValidationParameters {
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidIssuer = authSetting.Jwt.Issuer,
ValidAudience = authSetting.Jwt.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSetting.Jwt.Key)),
ClockSkew = TimeSpan.Zero
};
});
}
}

然后在 Program.cs 里,需要使用这个扩展方法来注册服务

builder.Services.AddAuth(builder.Configuration);

还得配置一下中间件,这个顺序很重要,需要使用身份认证保护的接口或资源,必须放到这俩 Auth... 中间件的后面。

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// ...
app.MapControllerRoute(...);
app.Run();

封装登录逻辑

还是那句话,为了方便使用balabala……

新建 StarBlog.Web/Services/AuthService.cs 文件

public class AuthService {
private readonly Auth _auth;
private readonly IBaseRepository<User> _userRepo; public AuthService(IOptions<Auth> options, IBaseRepository<User> userRepo) {
_auth = options.Value;
_userRepo = userRepo;
} public LoginToken GenerateLoginToken(User user) {
var claims = new List<Claim> {
new(JwtRegisteredClaimNames.Sub, user.Id), // User.Identity.Name
new(JwtRegisteredClaimNames.GivenName, user.Name),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_auth.Jwt.Key));
var signCredential = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var jwtToken = new JwtSecurityToken(
issuer: _auth.Jwt.Issuer,
audience: _auth.Jwt.Audience,
claims: claims,
expires: DateTime.Now.AddDays(7),
signingCredentials: signCredential); return new LoginToken {
Token = new JwtSecurityTokenHandler().WriteToken(jwtToken),
Expiration = TimeZoneInfo.ConvertTimeFromUtc(jwtToken.ValidTo, TimeZoneInfo.Local)
};
}
}

因为篇幅关系,只把关键的生成 JWT 代码贴出来,还有一些获取用户信息啥的代码,还不是最终版本,接下来随时会修改,而且也比较简单,就没有放出来~

再来写个登录接口

添加 StarBlog.Web/Apis/AuthController.cs 文件

[ApiController]
[Route("Api/[controller]")]
[ApiExplorerSettings(GroupName = ApiGroups.Auth)]
public class AuthController : ControllerBase {
private readonly AuthService _authService; public AuthController(AuthService authService) {
_authService = authService;
} /// <summary>
/// 登录
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(ApiResponse<LoginToken>), StatusCodes.Status200OK)]
public async Task<ApiResponse> Login(LoginUser loginUser) {
var user = await _authService.GetUserByName(loginUser.Username);
if (user == null) return ApiResponse.Unauthorized("用户名不存在");
if (loginUser.Password != user.Password) return ApiResponse.Unauthorized("用户名或密码错误");
return ApiResponse.Ok(_authService.GenerateLoginToken(user));
}
}

之后我们请求这个接口,如果用户名和密码正确的话,就可以拿到 JWT token 和过期时间

{
"statusCode": 200,
"successful": true,
"message": "Ok",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR123I6IkpXVCJ9.eyJ1c2VybmFtZSI6ImRlYWxpIiwibmFC1kYJ9.DaJEmBAVdXks8MOedVee4xxrB-RvUSg2wIJGc30HGkk",
"expiration": "2023-05-04T22:29:04+08:00"
},
"errorData": null
}

接下来,请求添加了 [Authorize] 的接口时,需要在 HTTP header 里面加上:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR123I6IkpXVCJ9.eyJ1c2VybmFtZSI6ImRlYWxpIiwibmFC1kYJ9.DaJEmBAVdXks8MOedVee4xxrB-RvUSg2wIJGc30HGkk

配置swagger支持

加了 [Authorize] 之后,在swagger里就没法调试接口了,得用 postman 之类的工具,添加 HTTP header

不过swagger这么好用的工具肯定不会那么蠢,它是可以配置支持 JWT 的

添加 nuget 包 Swashbuckle.AspNetCore.Filters

然后编辑 StarBlog.Web/Extensions/ConfigureSwagger.cs 来配置一下(上一篇关于swagger的还没忘记吧?)

AddSwaggerGen 里面,添加配置代码

var security = new OpenApiSecurityScheme {
Description = "JWT模式授权,请输入 \"Bearer {Token}\" 进行身份验证",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey
};
options.AddSecurityDefinition("oauth2", security);
options.AddSecurityRequirement(new OpenApiSecurityRequirement {{security, new List<string>()}});
options.OperationFilter<AddResponseHeadersFilter>();
options.OperationFilter<AppendAuthorizeToSummaryOperationFilter>();
options.OperationFilter<SecurityRequirementsOperationFilter>();

搞定。这样swagger页面右上角就多了个锁头图标,点击就可以输入 JWT token

不过有一点不方便的是,每个接口分组都要输入一次,切换了就得重新输入了…

但至少不用postman了~

参考资料

系列文章

基于.NetCore开发博客项目 StarBlog - (27) 使用JWT保护接口的更多相关文章

  1. 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 ... 基于. ...

  2. 基于.NetCore开发博客项目 StarBlog - (3) 模型设计

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  3. 基于.NetCore开发博客项目 StarBlog - (4) markdown博客批量导入

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  4. 基于.NetCore开发博客项目 StarBlog - (5) 开始搭建Web项目

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  5. 基于.NetCore开发博客项目 StarBlog - (6) 页面开发之博客文章列表

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  6. 基于.NetCore开发博客项目 StarBlog - (7) 页面开发之文章详情页面

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  7. 基于.NetCore开发博客项目 StarBlog - (8) 分类层级结构展示

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  8. 基于.NetCore开发博客项目 StarBlog - (9) 图片批量导入

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  9. 基于.NetCore开发博客项目 StarBlog - (10) 图片瀑布流

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  10. 基于.NetCore开发博客项目 StarBlog - (11) 实现访问统计

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

随机推荐

  1. 网络数据请求get&post

  2. for in循环的坑

    num本来数个数组,但是for in把数组原型上的也遍历(偶尔会)记录一下坑,数组还是for循环,for in还是用在对象上好

  3. 西电oj245 成绩统计 结构体数组使用

    #include<stdio.h> struct student{ //定义一个结构体数组 int num; char name[11]; float g1; float g2; floa ...

  4. AttributeError: module 'torchvision' has no attribute 'transforms'

    代码:maskrcnn-benchmark Python 3.6.13 |Anaconda, Inc Traceback (most recent call last): File "too ...

  5. Hadoop-HA节点介绍

    设计思想 hadoop2.x启用了主备节点切换模式(1主1备) 当主节点出现异常的时候,集群直接将备用节点切换成主节点 要求备用节点马上就要工作 主备节点内存几乎同步 有独立的线程对主备节点进行监控健 ...

  6. Javaweb基础复习------Cookie+Session案例的实现(登录注册案例)

    Cookie对象的创建--Cookie cookie=new Cookie("key","value"); 发送Cookie:resp.addCookie(); ...

  7. 21.C++的对象模型

    程序1: #pragma warning(disable:4996) //2022年9月21日19:20:29 #include <iostream> using namespace st ...

  8. Flex布局原理【转载】

    引言 CSS3中的 Flexible Box,或者叫flexbox,是用于排列元素的一种布局模式. 顾名思义,弹性布局中的元素是有伸展和收缩自身的能力的. 相比于原来的布局方式,如float.posi ...

  9. Pycharm 搭建 Django 项目

    1. 安装需求 在使用 python 框架 Django 需要注意下面事项 Pycharm 版本是专业版而不是社区版本 Pycharm 配置好了 python 解释器 (一般我们现在用的都是pytho ...

  10. 5.Web信息收集

    Web信息收集 目录 Web信息收集 1.whois查询 2.服务器操作系统的识别 3.服务器加固 4.服务版本识别 5.常见组合: 6.指纹识别 7.敏感路径识别 8.历史漏洞信息收集 1.whoi ...