C#实现JWT无状态验证的实战应用解析
本文将讲解如何使用C#实现JWT无状态验证的实战应用。
什么是JWT?
JWT (Json Web Token)是一种用于身份验证的开放标准(RFC 7519)。它是一种轻量级的身份验证协议,通过在服务端签署一个 JSON 数据块生成一个令牌(Token),以表明身份和认证的有效性。该令牌包含了用户身份、令牌过期时间等信息。在服务端的每个请求中,该Token被使用以验证用户的身份和操作的有效性。JWT作为一种用于Web应用的跨域认证及数据传递的极佳解决方案已被广泛应用。
JWT原理
JWT由三部分组成,分别是:Header、Payload和Signature。其中,Header和Payload都是JSON对象,Signature是将Header和Payload使用相同的加盐算法和密钥生成的Hash字符串。
Header示例:
{
"alg": "HS256",
"typ": "JWT"
}
Payload示例:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Signature示例:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
其中,Base64Url编码遵循RFC 4648标准,其实现方式与Base64编码略有不同。
JWT优缺点
优点
- 无状态
- 前后端分离
- 扩展性好
缺点
- 无法立即废除Token
- 不支持单点登录
JWT的使用
生成Token
对于ASP.NET Core项目,可以使用Microsoft.AspNetCore.Authentication.JwtBearer
Nuget包,快速实现生成Token的功能。
以ASP.NET Core WebAPI项目为例,首先需要添加以下依赖:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.13" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.11.1" />
</ItemGroup>
接着,在Startup.cs
中的ConfigureServices
方法中,需要添加JWT验证的配置:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
...
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
在上述代码中,我们配置了JWT的发行者(Issuer)和接受者(Audience),以及用于签署Token的密钥(Key)。
接着,在Configure
方法中,需要启用JWT验证:
app.UseAuthentication();
此时,我们就可以使用System.IdentityModel.Tokens.Jwt
Nuget包提供的JwtSecurityTokenHandler
类生成Token:
using System.IdentityModel.Tokens.Jwt;
...
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("mysupersecret_secretkey!123");
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = "localhost",
Audience = "localhost",
Subject = new ClaimsIdentity(new[]
{
new Claim("id", "123456"),
new Claim("username", "john.doe")
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
在上述代码中,我们指定了Token的发行者(Issuer)、接受者(Audience)、过期时间(Expires)、用于签署Token的密钥(key)以及Payload信息(Subject)。
验证Token
在客户端向服务端发送请求时,需要在请求头中附带Token:
GET /api/values HTTP/1.1
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
服务端验证Token的方法与生成Token类似。在ConfigureServices
方法中,我们可以使用AddJwtBearer
方法添加JWT验证的配置。
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
在Configure
方法中,我们启用JWT验证:
app.UseAuthentication();
此时,我们就可以在Controller的Action中使用Authorize
属性来保护需要认证的API:
[Authorize]
[HttpGet]
public IActionResult Get()
{
var currentUser = HttpContext.User;
var userId = currentUser.FindFirst(ClaimTypes.NameIdentifier).Value;
var username = currentUser.FindFirst(ClaimTypes.Name).Value;
return Ok(new { UserId = userId, Username = username });
}
在上述代码中,我们使用Authorize
属性来指定只有经过验证的用户才能访问该API,在Controller中可以使用HttpContext.User
获取当前用户的信息。
示例应用
本节将介绍一个实现JWT身份验证的示例应用。该应用为一个简单的ASP.NET Core WebAPI项目,在前端使用Vue.js进行展示。
在该应用中,我们将完成以下功能的实现:
- 登录、注册、登出
- 权限管理
- 鉴权
数据库
在实现示例应用前,我们需要先通过Entity Framework Core创建数据库。根据以上需求,我们需要创建的表包括Users
、Roles
和UserRoles
。
在Package Manager Console中输入以下命令,创建数据模型:
Scaffold-DbContext "Server=(localdb)\mssqllocaldb;Database=MyDatabase;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir DataModels -f
在上述命令中,MyDatabase
是我们要创建的数据库名称,可以根据需求进行修改。
执行上述命令后,我们在项目中会看到生成了DataModels
文件夹,其中包含了数据库对应的实体类。
用户认证
在实例应用中,我们将使用JWT实现用户认证。对于需要认证的API,我们将使用Authorize
属性来标记,以保护对该API的访问。
在项目中,我们需要实现以下内容:
- 客户端向服务端发送登录请求
- 服务端验证用户的登录信息,并返回Token
- 客户端在后续的请求中携带Token
- Token验证成功后,服务端返回JWT的Payload信息
后台API
在后台API中,我们需要添加以下依赖:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.13" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.11.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.13" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.13" />
</ItemGroup>
接着,在Startup.cs
中的ConfigureServices
方法中,需要添加JWT验证的配置:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
}
在上述代码中,我们配置了JWT的发行者(Issuer)和接受者(Audience),以及用于签署Token的密钥(Key)。
接着,在Configure
方法中,需要启用JWT验证:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
在用户登录时,应检验其提供的凭据,并返回Token。在本示例应用中,我们使用用户名和密码作为登录凭据,返回的Token中携带了用户姓名、用户角色等信息。Token的生成使用了System.IdentityModel.Tokens.Jwt
Nuget包提供的JwtSecurityTokenHandler
类。
[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user != null && await _userManager.CheckPasswordAsync(user, request.Password))
{
var userRoles = await _userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim("userId", user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
foreach (var userRole in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
expires: DateTime.UtcNow.AddHours(1),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256Signature)
);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
在用户重新登录时,其原Token将失效。因此,需要使其在重新登录时获取新Token。该功能可以通过实现Token的黑名单机制来实现,即服务端维护一个Token黑名单,使其失效。具体而言,可以在登录成功后,将旧Token加入黑名单,当客户端发起请求时,服务端检测Token是否在黑名单中存在,存在则拒绝访问。
前端实现
在前端中,我们使用Vue.js框架实现用户登录。在登录后,我们将在LocalStorage中保存Token,以便在后续请求中发送。为简化页面设计,我们使用了Element UI框架提供的组件。
在methods
中,我们实现了登录请求:
methods: {
login() {
axios.post('/api/account/login', {
username: this.username,
password: this.password
}).then(res => {
localStorage.setItem('token', res.data.token);
this.$message.success('Login success');
setTimeout(() => {
window.location.href = '/';
}, 1000);
}).catch(err => {
this.$message.error(err.response.data);
});
}
}
在每次发起请求时,我们首先检查LocalStorage中是否存在Token,如果存在则将其放入请求头中:
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
Promise.reject(error);
}
);
鉴权
在本示例应用中,我们使用Authorize
属性来实现鉴权。对于需要鉴权的API,我们使用角色来授权,需要访问该API的用户权限必须包含指定的角色。
在Controller中,我们使用[Authorize(Roles = "admin")]
属性进行鉴权。具体而言,仅有具有admin
角色的用户才能访问该API:
[Authorize(Roles = "admin")]
[HttpGet]
public IEnumerable<string> GetAdminData()
{
return new string[] { "value1", "value2" };
}
将上述API返回的数据展示到前端,在前端中我们使用了v-if
指令,检测用户的角色包含admin
时才显示数据。
<div v-if="roles.indexOf('admin') != -1">
<el-row :gutter="10">
<el-col :span="8" v-for="item in adminData" :key="item">
<el-card>{{ item }}</el-card>
</el-col>
</el-row>
</div>
示例代码
完整的示例代码可以在Github中获取。
总结
本文介绍了如何使用C#实现了JWT无状态验证的实战应用,并使用了两个示例来进一步说明JWT的使用方法和鉴权的实现。当使用JWT时,需要注意以下内容:
- JWT适用于无状态的Web应用
- Token失效机制需要自行实现
- Token不能立即废除
在实现时,应注意保证密钥及其生成过程的安全性,同时验证客户端提交的信息,防止XSS攻击以及SQL注入等漏洞的出现。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:C#实现JWT无状态验证的实战应用解析 - Python技术站