一、JWT介绍
ASP.NET Core Web API用户身份验证的方法有很多,本文只介绍JWT方法。JWT实现了服务端无状态,在分布式服务、会话一致性、单点登录等方面凸显优势,不占用服务端资源。简单来说,JWT的验证过程如下所示:
(1)通过用户名和密码获取一个Token。
(2)访问API时,加上这个Token。
Token包含过期时间、用户角色等信息,可以在多种场合灵活使用。
二、基本认证
2.1 场景描述
在基本认证的场景中,我们假设有一个Controller,代码如下所示:
[ApiController][Route("test")]public class TestController : ControllerBase{ [HttpGet] public string Get() { return "success"; }}
这个Controller非常简单,就是访问之后返回"success"这个字符串。
现在,我们希望用户登录之后(提供用户名和密码)才能使用此API。
2.2 开发步骤
1、在NuGet中添加Microsoft.AspNetCore.Authentication.JwtBearer。
2、在Program.cs中,对builder.Services添加认证服务:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opt => { opt.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = false, ValidateIssuerSigningKey = false }; });
在上面的参数中,可以根据使用场景进行灵活配置,这个后面会介绍。在此,使用最简单的配置。
3、同样在Program.cs中,添加以下语句:
app.UseAuthentication();
需要注意的是,此句需要在页面路由之前,例如是app.UseHttpsRedirection();之前,如下所示:
app.UseAuthentication();app.UseHttpsRedirection();app.UseAuthorization();app.MapControllers();app.Run();
4、在需要认证的Controller或某个具体方法上加上[Authorize]标记。例如我们在一开始介绍的API上加上标记:
[Authorize][HttpGet]public string Get(){ return "success";}
此时,如果再去访问此API,就会返回401没有权限的错误。
5、创建登录的Controller,也就是通过用户名和密码获取Token。
[Route("api/auth")][ApiController]public class AuthController : ControllerBase{ [HttpPost] public string Gettoken(string username, string password) { if (username == "admin" && password == "123456") { var claims = new[]{ new Claim(ClaimTypes.Name, username) }; var jwttoken = new JwtSecurityToken( claims: claims ); var token = new JwtSecurityTokenHandler().WriteToken(jwttoken); return token; } return "用户不存在或密码错误"; }}
上面的用户认证简单的做了字符串比对,实际上可以通过查数据库的方法验证用户是否合法。
至此,用户验证的后端就开发完成了。
三、前端访问API
3.1 通过Swagger测试
很多时候,我们使用Swagger来测试API,但默认的Swagger配置没有用户验证,需要进行添加。
打开Program.cs,把builder.Services.AddSwaggerGen();这一句修改为:
builder.Services.AddSwaggerGen(c =>{ c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() { In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Description = "Bearer Token", Name = "Authorization", BearerFormat = "JWT", Scheme = "Bearer" }); c.AddSecurityRequirement(new OpenApiSecurityRequirement() { { new OpenApiSecurityScheme() { Reference=new OpenApiReference() { Type=ReferenceType.SecurityScheme, Id="Bearer" } },new string[]{ } } });});
此时,打开Swagger,就会看到右上角有一个Authorize的按钮,如下图所示:
点击按钮,会要求输入一个Token。我们从上面写的登录Controller(AuthController)获取。如下图所示:
最下面的响应字符串就是我们要的Token。把这个Token填入到上面弹出的对话框中,注意需要在字符串前加上“Bearer ”。如下图所示:
此时,再去访问TestController,就会返回成功结果。
3.2 前端访问方法
在无用户验证要求的时候,前端访问TestController的代码如下所示:
fetch("https://localhost:28911/test", { method: "GET"}).then(resp => { return resp.text();}).then(data => { console.log(data);}).catch(err => { console.log(err)});
API前加上[Authorize]之后,上述调用也会返回401错误。上述调用需要把Token加入到Header中。
fetch("https://localhost:28911/test", { method: "GET", headers: { "Authorization": "Bearer eyJhbGciOiJ...太长,Token后面省略" }}).then(resp => { return resp.text();}).then(data => { console.log(data);}).catch(err => { console.log(err)});
此时,即可再次成功获取结果。
四、常见用户身份验证场景
4.1 Token有效期
一般情况下,通过用户名和密码得到了Token之后,我们不希望这个Token是永久有效的。也就是说,过了一段时间之后,Token失效,用户需要重新登录。
需要增加有效期,有几处地方需要修改。
首先是Program.cs中,原来的配置如下所示:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opt => { opt.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = false, ValidateIssuerSigningKey = false }; });
需要把ValidateLifetime改为true:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opt => { opt.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30), }; });
然后在AuthController的Gettoken方法里,加上有效期:
var claims = new[]{ new Claim(JwtRegisteredClaimNames.Nbf,new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()) , new Claim(JwtRegisteredClaimNames.Exp,new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds().ToString()), new Claim(ClaimTypes.Name, username)};
上面配置的有效期是30分钟。
至此,当获得一个新的Token之后,经过30分钟,这个Token就无法使用了。
4.2 第三方验证
很多网站上提供QQ登录、微信登录、微博登录等功能,我们以微信登录进行说明。如果我们需要使用微信登录,需要到微信开发者平台注册,拿到一个叫AppKey的东西。这个AppKey相当于一个私钥,是不能让别人看到的。因为我们要使用微信的登录验证服务,这个服务不能向任何人提供,而且需要区分是哪个第三方在使用服务。
为了实现上述功能,认证时加入了一个IssUser的概念,也就是哪些APP可以调用服务。
在Program.cs中,修改配置:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opt => { opt.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true, ValidateAudience = false, ValidateLifetime = false, ValidateIssuerSigningKey = true, ValidIssuer = "App名称", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("App私钥")) }; });
然后,在Gettoken时,进行修改:
var claims = new[]{ new Claim(ClaimTypes.Name, username)};var m5dkey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("App私钥"));var creds = new SigningCredentials(m5dkey, SecurityAlgorithms.HmacSha256);var jwttoken = new JwtSecurityToken( issuer: "App名称", claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds);var token = new JwtSecurityTokenHandler().WriteToken(jwttoken);return token;
4.3 角色或权限
很多时候,用户登录之后,并不是可以访问所有的API。用户可以分出不同的角色,不同的角色可以访问不同的资源。
要做到这一点,首先在Gettoken时,把用户的角色加入到Token中。
var claims = new[]{ new Claim(ClaimTypes.Name, username), new Claim("Role", "admin")};
然后,在API前,指明此资源需要什么角色。
[Authorize(Roles = "admin")][HttpGet]public string Get(){ return "success";}
这样,按角色分配资源的功能就完成了。