.net5 core webapi项目实战之十六:身份验证(下篇)
上一篇介绍了JWT身份认证的原理及.net core webapi中如何使用JWT。
本篇继续介绍如何在客户端设置JWT认证的Token信息以及Web服务器如何去解析Token中的内容并正确识别出用户身份。
注:这里的客户端可以是浏览器、桌面应用、手机APP、小程序等。
本项目中的认证流程是这样的:
1. 用户访问登录接口API(http://localhost:52384/api/users/login) → 2. 得到 JWT Token → 3. 后续API访问的请求Header中加上此 Token
一、客户端调用登录接口生成Token字符串,代码如下:
1 [Route("login")] 2 [HttpGet] 3 public ContentResult LoginUser() 4 { 5 string acc = Request.Query["acc"]; //接收查询参数acc 6 string pwd = Request.Query["pwd"]; //接收查询参数pwd 7 8 //如果参数为空返回提示信息 9 if (string.IsNullOrEmpty(acc) || string.IsNullOrEmpty(pwd)) 10 { 11 return Content("{'result':'account or password is empty!'}"); 12 } 13 14 //生成 Token 信息 15 string token = GenerateJwtToken(); 16 17 //将 Token 信息返回给客户端 18 return Content(token); 19 } 20 21 private string GenerateJwtToken() 22 { 23 // 1. 设置加密算法 24 string algorithm = SecurityAlgorithms.HmacSha256; 25 26 // 2. 生成签名证书,注意密钥长度至少为16位,否则会报错 27 SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdefghijklmn1234567890")); 28 SigningCredentials signing = new SigningCredentials(key, algorithm); 29 30 // 3. 构造Claims(任意添加几个值,实际项目根据需要来设置要传递的信息) 31 Claim[] claims = new[] 32 { 33 new Claim(JwtRegisteredClaimNames.Sub, "aaa000"), 34 new Claim(ClaimTypes.Name, "ccc333"), 35 }; 36 37 // 4. 生成令牌 38 string issuer = null; 39 string audience = null; 40 DateTime notBefore = new DateTime(2020, 2, 1); 41 DateTime expires = new DateTime(2020, 3, 1); 42 JwtSecurityToken jwtToken = new JwtSecurityToken(issuer, audience, claims, notBefore, expires, signing); 43 44 // 5. 将令牌实例转换成字符串 45 string strToken = new JwtSecurityTokenHandler().WriteToken(jwtToken); 46 47 return strToken; 48 }
说明:
1 . [Route("login")] 表示访问 LoginUser( ) 这个终结点时的路径应该是 http://www.xxx.com/api/uses/login ;
2 . 用户的账号密码是以查询字符串的形式传递的 ,如 ?acc=123&pwd=456 ;
3 . 为了方便,这里仅判断了账号/密码不为空,实际项目中可能需要和数据库中的值做比较 ;
4 . 编码时要添加 System.IdentityModel.Tokens.Jwt 和 System.Security.Claims 这两个引用 ;
二、在终结点上加上属性 [Authorize] 启用身份认证,代码如下:
1 [HttpGet] 2 [Authorize] 3 public ContentResult ManageUsers() 4 { 5 //... 6 //... 7 }
注:UsersController这个控制器中还有多个终结点,为了避免逐一添加的麻烦,可以将属性 [Authorize] 加在类名上面,
使身份验证的功能对该控制器中的每个终结点都生效,因 LoginUser( ) 这个终结点是不需要身份验证的,
故加上属性 [AllowAnonymous] 就可以了,代码如下:
1 [Route("api/[controller]")] 2 [ApiController] 3 [Authorize] 4 public class UsersController : ControllerBase 5 { 6 private ILogger<UsersController> _logger; 7 private IUserDao _userDao; 8 public UsersController(ILogger<UsersController> logger, IUserDao userDao) 9 { 10 _logger = logger; 11 _userDao = userDao; 12 } 13 14 15 16 [Route("login")] 17 [HttpGet] 18 [AllowAnonymous] 19 public ContentResult LoginUser() 20 { 21 string acc = Request.Query["acc"]; //接收查询参数acc 22 string pwd = Request.Query["pwd"]; //接收查询参数pwd 23 24 //... 25 //... 26 } 27 28 }
三、在Startup.cs的 ConfigureServices( ) 方法中设置身份认证的参数,代码如下:
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddControllers(); 4 5 services.AddScoped<IUserDao, MySqlUserDao>(); 6 7 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer( 8 //设置Token验证的参数 9 options => 10 { 11 options.TokenValidationParameters = new TokenValidationParameters() 12 { 13 ValidateIssuer = false, //不验证 issuer,因为 GenerateJwtToken() 方法生成的Token中issuer = null 14 ValidateAudience = false, //同上 15 16 //如果希望 Token 在规定时间后失效请设置成 true 17 //本例中 GenerateJwtToken()方法设置的值是 new DateTime(2020, 3, 1) 18 ValidateLifetime = false, 19 20 //需要验证签名key, 同时IssuerSigningKey的值与 GenerateJwtToken() 方法中的key值保持一致 21 ValidateIssuerSigningKey = true, 22 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdefghijklmn1234567890")) 23 }; 24 } 25 ); 26 }
四、测试 LoginUser( ) 和 ManageUsers( ) 这两个终结点。
4.1 重新编译整个项目后打开POSTMAN访问网址 http://localhost:52384/api/users/login,结果如下:
可以访问且显示账号或密码为空的信息。
4.2 加上查询字符串后访问网址:http://localhost:52384/api/users/login?acc=111&pwd=222,结果如下:
如我们所愿,生成了一个 JWT token字符串。
4.3 访问网址 http://localhost:52384/api/users ,结果如下:
无法访问,提示401没有权限的信息。
4.4 在请求头中加上4.2中生成的Token字符串后重新访问,结果如下:
可以正常访问并得到期望的结果。
注:请求头中加的 KEY 名称是 Authorization ,VALUE 值是 Bearer + 空格 + token字符串, 空格个数不限,
但 Authorization 和 Bearer 是关键字,不能写错。
========================================== 分割线 ==========================================
补充:
1 . Claim[ ]数组中添加元素用JwtRegisteredClaimNames和ClaimTypes的区别
访问网址 https://jwt.io/ , 在页面中输入4.2中生成的 token 解析出来的值如下:
JwtRegisteredClaimNames.Sub 生成的key名和属性名保持一致,
ClaimTypes.Name生成的key带了一个网址前缀。
2 . 在其他终结点中如何得到 "ccc333"这个值
添加终结点 DecodeToken( ) 并设置路由属性为 [Route("decode")](对应网址为http://localhost:52384/api/users/decode),代码如下:
1 [Route("decode")] 2 [HttpGet] 3 public ContentResult DecodeToken() 4 { 5 //读取 token 的值 6 string token = Request.Headers["Authorization"]; 7 token = token.Replace(" ","").Substring(6); 8 9 //得到 token 中payload部分的值 10 JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); 11 JwtSecurityToken jwtToken = handler.ReadJwtToken(token); 12 JwtPayload payload = jwtToken.Payload; 13 14 //1. 官方字段可直接取值 15 string sub = payload.Sub; 16 string nbf = payload.Nbf.ToString(); 17 string exp = payload.Exp.ToString(); 18 19 //2. 非官方字段反序列化成字典对象后取值 20 Dictionary<string,object> dic = JsonSerializer.Deserialize<Dictionary<string, object>>(payload.SerializeToJson()); 21 string name = dic["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"].ToString(); 22 23 //输出 24 string jwt1 = "@@@@ " + payload.SerializeToJson() + Environment.NewLine; 25 string jwt2 = "@@@@ sub=" + sub + "; nbf=" + nbf + "; exp=" + exp + "; name=" + name + Environment.NewLine; 26 27 return Content("result:" + Environment.NewLine + jwt1 + jwt2); 28 }
访问网址 http://localhost:52384/api/users/decode , 结果如下:
实际项目中可以将读取Token内容的功能封装成一个Utility类,方便在其他方法中调用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人