前言

这是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. 将后端的application/json的格式数据类型转换成前端需要的类型格式

    前提:后端返回的数据内容 但是红框的数据对于前端来说是不正确的数据 所以我感觉前端处理这个数据本身这个操作都很傻X 但是我尝试进行转换代码如下: 得到的数据: 点击查看代码 const interfa ...

  2. 日志注解,基于ruoyi的后置切面改进而来

    有次接口响应时间太长,想知道具体接口执行的时间是多少,于是决定通过注解来实现这个想法,刚好ruoyi本身就提供了完善的日志注解,虽然是采用后置通知,但是完全不影响我们改造它. 想要实现接口耗时的功能, ...

  3. lc.209 长度最小的子数组

    题目 给定一个含有 n 个正整数的数组和一个正整数 target . 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, n ...

  4. java的死锁与解决方法

    一.什么是死锁? 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无限等待. 二.产生死锁的原因与四个条件 2.1 死锁原因 竞争资 ...

  5. docker 容器版本问题

    LoggerFactory is not a Logback LoggerContext but Logback is on the classpath springboot docker 容器中运行 ...

  6. 基于Centos7 minimal 加固

    一.用户帐号和环境-----------------------. 2 二.系统访问认证和授权--------------------- 3 三.核心调整--------------------- 4 ...

  7. 10.4 提高叠加处理速度(2) (harib07d)

    ps:能力有限,若有错误及纰漏欢迎指正.交流 sheet_refreshsub void sheet_refreshsub(struct SHTCTL *ctl, int vx0, int vy0, ...

  8. PicGo+Typora+Github图床配置步骤(一键上传本地图片)

    PicGo+Typora+Github图床配置步骤(一键上传本地图片) 一.配置前的准备 首先你需要有一个Github账号[GitHub]. 然后下载PicGo图片上传工具[PicGo]和Typora ...

  9. 咕咕list

    做完以后会留在榜上一天,这样显得咕咕list长一些 CF666E Forensic Examination(done on 2023.2.6) dp选做

  10. Solon2 自定义 slf4j 日志添加器

    本案需要引入已适配的 slf4j 日志框架(solon.logging.simple 或 logback-solon-plugin 或 log4j-solon-plugin). 1.自定义添加器入门 ...