ASP.NET Core JWT 认证详解:拓展类封装、Swagger集成、自定义Role与授权机制

JWT的组成

  1. header-头部
  2. payload-负载
  3. signature-签名

JWT 全称为JSON web token。重名称来看,这个是使用json格式来保存令牌信息的。
我们虽然讲 JWT,但提一下 Session.
而 JWT 与 Session 的最大区别就是: JWT 是把token保存到客户端的;Session则是把 session 数据保存到服务器。
使用session 时,如果登录用户太多,session数据会占用非常多的内存,而且非常不方便使用分布式环境,这也是 JWT 出现的原因。
而 JWT 适合分布式是因为可以直接从客户端提供的 JWT 中获取当前登录用户的信息,而不用再去通过接收的 session_id 去内存中查用户信息。

头部:保存加密算法的说明
负载: 保存用户ID、用户名、角色等信息
签名:使用密钥,然后根据头部和负载一起算出来的值, 作用是防止他人篡改信息,而签名的密钥只有服务端才知道,我个人感觉这个密钥有点类似于哈希加密里面的 盐值


本文简单的讲解在ASP.NET Core 中 JWT 的基础使用(拿来即用)。
需要安装的包:Microsoft.AspNetCore.Authentication.JwtBearer、Microsoft.IdentityModel.Tokens、System.IdentityModel.Tokens.Jwt
建议先安装第一个包,如果不全再安装后面两个。

配置文件:

注意:如果使用 HS256签名,K则ey 必须 ≥ 32 字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Jwt": {
"Key": "fasdfad&9045dafz222#fadpio@0232aa",
"Issuer": "MyApi",
"Audience": "MyApiUser",
"ExpireMinutes": 60
},
"AllowedHosts": "*"
}

创建一个拓展文件夹Extensions方便管理和复用(以后直接复制这个文件夹就行),然后创建两个拓展类。

JWT认证拓展类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace JWTAspNetCoreTest01x02.Extensions
{
public static class JwtAuthenticationExtensions
{
public static IServiceCollection AddJwtAuthentication(
this IServiceCollection services,
IConfiguration configuration)
{
var jwtConfig = configuration.GetSection("Jwt");
var key = Encoding.UTF8.GetBytes(jwtConfig["Key"]!);

services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;//是否使用https,默认false,不使用,如果是使用https,设置为true
options.SaveToken = true;//是否将 token 保存到当前上下文中,默认 false,不保存,如果是 true,可以通过 HttpContext.GetTokenAsync("access_token") 获取 token

options.TokenValidationParameters = new TokenValidationParameters//token验证参数
{
ValidateIssuer = true,//是否验证Issuer
ValidateAudience = true,//是否验证Audience
ValidateLifetime = true,//是否验证过期时间
ValidateIssuerSigningKey = true,//是否验证SecurityKey

ValidIssuer = jwtConfig["Issuer"],//发行人
ValidAudience = jwtConfig["Audience"],//订阅人
IssuerSigningKey = new SymmetricSecurityKey(key),//SecurityKey

ClockSkew = TimeSpan.Zero//默认5分钟,设置为0表示不允许过期的token被接受
};
});

services.AddAuthorization();

return services;
}
}
}

Swagger-JWT拓展类

方便在 swagger-UI 中 添加 token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

namespace JWTAspNetCoreTest01x02.Extensions
{
public static class SwaggerExtensions
{
public static IServiceCollection AddSwaggerWithJwt(
this IServiceCollection services)
{
services.AddSwaggerGen(options =>
{
// 添加 JWT Bearer 定义
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "输入:token"
});

// 添加全局安全要求
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
});

return services;
}
}
}

JWT服务:

目前这里的 token 中添加的角色只有 Admin,后面如果有需求的话,再自己加一个策略算法(策略模式)进行更改吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace JWTAspNetCoreTest01x02;
public class JwtService
{
private readonly IConfiguration _configuration;

public JwtService(IConfiguration configuration)
{
_configuration = configuration;
}

public string GenerateToken(string userId, string username)
{
var jwtConfig = _configuration.GetSection("Jwt");

var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtConfig["Key"]!)
);

var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, "Admin"),
new Claim("custom", "customValue")
};

var token = new JwtSecurityToken(
issuer: jwtConfig["Issuer"],
audience: jwtConfig["Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(//尽量使用UtcNow
Convert.ToDouble(jwtConfig["ExpireMinutes"])
),
signingCredentials: creds
);

return new JwtSecurityTokenHandler().WriteToken(token);
}
}

program.cs

关于JwtService的生命周期,我为什么选择Scoped,而不是Singleton?
虽然这里使用Singleton也没有问题,但我想要是将来需要 用到 DbContext(生命周期Scoped) 的话,用Singleton会有生命周期冲突。
所以默认推荐使用Scoped。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

using JWTAspNetCoreTest01x02;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Text;
using JWTAspNetCoreTest01x02.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddScoped<JwtService>();//就是这里生命周期

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
//builder.Services.AddSwaggerGen();

//使用拓展方法添加JWT认证
builder.Services.AddJwtAuthentication(builder.Configuration);

//使用拓展方法添加Swagger,并配置JWT支持
builder.Services.AddSwaggerWithJwt();

builder.Services.AddAuthorization();
builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication(); // 必须在 Authorization 前面,先验证,再授权

app.UseAuthorization();

app.MapControllers();

app.Run();

登录接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace JWTAspNetCoreTest01x02.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly JwtService _jwtService;

public AuthController(JwtService jwtService)
{
_jwtService = jwtService;
}

[HttpPost("login")]
public IActionResult Login(string username, string password)
{
// 模拟验证
if (username != "admin" || password != "123456")
{
return Unauthorized("用户名或密码错误");
}

var token = _jwtService.GenerateToken("1", username);

return Ok(new
{
token
});
}
}
}

受保护接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace JWTAspNetCoreTest01x02.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
private readonly IConfiguration _configuration;
public TestController(IConfiguration configuration)
{
_configuration = configuration;
}
[Authorize]
[HttpGet("info")]
public IActionResult GetInfo()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var username = User.FindFirst(ClaimTypes.Name)?.Value;
var role = User.FindFirst(ClaimTypes.Role)?.Value;//使用内置role时取消注释
//var role = User.FindFirst("role")?.Value;//使用自定义role时取消注释
//var roles = User.FindAll("role").Select(c => c.Value);//如果有多个角色,可以使用FindAll获取所有角色
return Ok(new
{
userId,
username,
role,
message = "你已通过JWT认证"
});
}
[HttpPost]
[Authorize(Roles = "Admin")]
public ActionResult<object> DecodeJWT(string jwtToken)
{
if (string.IsNullOrEmpty(jwtToken))
return BadRequest("Token不能为空");

if (jwtToken.StartsWith("Bearer "))
jwtToken = jwtToken.Substring(7);

var handler = new JwtSecurityTokenHandler();

var token = handler.ReadJwtToken(jwtToken);

var claims = token.Claims.Select(c => new
{
c.Type,
c.Value
});

return Ok(new
{
Header = token.Header,
Claims = claims,
Expires = token.ValidTo,
Issuer = token.Issuer,
Audience = token.Audiences
});
}
}
}

测试:

  1. 直接访问两个受保护接口(皆显示未鉴权)

[Authorize]要求:

屏幕截图 2026-02-27 162303

[Authorize(Roles = “Admin”)]”Admin”角色要求:

屏幕截图 2026-02-27 162724

  1. 登录,然后添加token

屏幕截图 2026-02-27 163052

  1. 再次访问受保护接口(皆成功访问)

屏幕截图 2026-02-27 163228

屏幕截图 2026-02-27 163249


在上面的例子中都是使用的的内置的role。如果想要使用自定义的role。

使用自定义role

只需要对 JWT拓展类 和 JwtService 进行修改。建议直接全选复制,粘贴覆盖就行

还有保护接口读取自定义信息也需要进行修改(取消相关注释,代码中标明了)

JWT拓展类:
修改两处:
添加 —>

  1. options.MapInboundClaims = false;//使用自定义role时取消注释 默认为 true,将 claims 映射到用户标识
  2. RoleClaimType = “role”//使用自定义role时取消注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace JWTAspNetCoreTest01x02.Extensions
{
public static class JwtAuthenticationExtensions
{
public static IServiceCollection AddJwtAuthentication(
this IServiceCollection services,
IConfiguration configuration)
{
var jwtConfig = configuration.GetSection("Jwt");
var key = Encoding.UTF8.GetBytes(jwtConfig["Key"]!);

services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;//是否使用https,默认false,不使用,如果是使用https,设置为true
options.SaveToken = true;//是否将 token 保存到当前上下文中,默认 false,不保存,如果是 true,可以通过 HttpContext.GetTokenAsync("access_token") 获取 token

options.MapInboundClaims = false;//使用自定义role时取消注释 默认为 true,将 claims 映射到用户标识

options.TokenValidationParameters = new TokenValidationParameters//token验证参数
{
ValidateIssuer = true,//是否验证Issuer
ValidateAudience = true,//是否验证Audience
ValidateLifetime = true,//是否验证过期时间
ValidateIssuerSigningKey = true,//是否验证SecurityKey

ValidIssuer = jwtConfig["Issuer"],//发行人
ValidAudience = jwtConfig["Audience"],//订阅人
IssuerSigningKey = new SymmetricSecurityKey(key),//SecurityKey

ClockSkew = TimeSpan.Zero,//默认5分钟,设置为0表示不允许过期的token被接受

RoleClaimType = "role"//使用自定义role时取消注释
};
});

services.AddAuthorization();

return services;
}
}
}

JwtService:
修改一处:
修改 —> new Claim(“role”, “Admin”),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace JWTAspNetCoreTest01x02;
public class JwtService
{
private readonly IConfiguration _configuration;

public JwtService(IConfiguration configuration)
{
_configuration = configuration;
}

public string GenerateToken(string userId, string username)
{
var jwtConfig = _configuration.GetSection("Jwt");

var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtConfig["Key"]!)
);

var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),//添加userId
new Claim(ClaimTypes.Name, username),//添加username
new Claim("role", "Admin"),
new Claim("custom", "customValue")
};

var token = new JwtSecurityToken(
issuer: jwtConfig["Issuer"],
audience: jwtConfig["Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(
Convert.ToDouble(jwtConfig["ExpireMinutes"])
),
signingCredentials: creds
);

return new JwtSecurityTokenHandler().WriteToken(token);
}
}

结束

按照步骤做到这个位置,那么 JWt 的基本使用应该都已经完成了(所以如果只是为了实现 JWT 的基本使用,可以退出不看后面的内容了)。后面讲为什么使用自定义信息的时候需要修改。

把 ClaimTypes.Role 改成 “role”,为什么还要改 Jwt 认证配置?

原因:

ASP.NET Core 默认会帮你“偷偷改 Claim 类型”。

一、默认行为:MapInboundClaims = true

在 AddJwtBearer 中:

1
options.MapInboundClaims = true; // 默认值

当它为 true 时:

JWT 里的标准 Claim 会被自动映射成微软内部格式。

例如:

JWT里的原始字段 实际进入 User.Claims 的类型
sub http://schemas...nameidentifier
role http://schemas...role
name http://schemas...name

也就是说,即使你在 JWT 里写的是:

1
2
3
{
"role": "Admin"
}

到了 ASP.NET Core 里:

1
User.FindFirst(ClaimTypes.Role)

依然能读到值,因为它被映射了。

二、当你使用自定义 Claim 时

如果你写:

1
new Claim("role", "Admin")

但是:

  • 你关闭了映射
  • 或者 Claim 不是标准字段
    那么 ASP.NET Core 不知道哪个字段代表“角色”。

这时候:

1
[Authorize(Roles = "Admin")]

会直接失效,因为授权系统找不到“角色”。

三、RoleClaimType 是干什么的?

在这里:

RoleClaimType = “role”

你是在告诉 ASP.NET Core:

以后判断角色的时候,请去找 Claim.Type == “role” 的字段。

否则默认它找的是:

ClaimTypes.Role

也就是:

http://schemas.microsoft.com/ws/2008/06/identity/claims/role

四、为什么要关闭 MapInboundClaims?

options.MapInboundClaims = false;

作用:

不要帮我做自动映射,我要完全按照 JWT 原始字段来。

关闭之后:

JWT 里写什么,User.Claims 里就是什么。

这在微服务场景非常重要。

因为:

  1. 你可能有 Java 服务

  2. 你可能有 Node 服务

  3. 你不希望 .NET 自动“改字段名”

现在这篇文章是真的结束了。