最近,团队的小伙伴们在做项目时,需要用到JWT认证。遂根据自己的经验,整理成了这篇文章,用来帮助理清JWT认证的原理和代码编写操作。
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
JWT是什么,看上面这段网上抄来的话。
关于JWT以及优缺点,网上有很多详细的说法,我这儿就不重复了。
我们只需要知道以下的事实:
在一般的系统中,我们有时候会做个用户登录。用户登录完成进到系统后,需要根据用户的权限,来控制一些功能可用,而另一些功能不可用。
在SOA/AOP架构中,做为最重要的API端,其实也需要有类似登录或认证的内容,用来区分哪些用户可以使用某个API,哪些用户不行。
同时,我们希望这个登录或类似登录的过程,只发生在一个固定位置。这样,在我们写代码时,建立好这样一个过程后,在我们后边写代码时,简单引用即可,而不需要每个API程序都开发一次认证。这个需求,其实就是OAuth的由来。
最重要的是,这样的代码写出来,显得 高大上 。
下面进入正题。
认证这个操作,就像我们最近的日子。
首先,我们要有一个出入证,或者绿码。这个证,我们称作令牌(Token)。我们去领这个证,这个操作称为发行(Issue)。
我们拿着这个证,去到一个地方。有专人会检查这个证,这称为用户身份验证(Authentication)。验证通过放行,称为授权(Authorization),验证不通过,叫作未授权错误(Unauthorized)。
如果这个证过期了,你就需要去重新办一个证。这个过程叫做刷新(RefreshToken)。
简言之,这就是认证的全部流程。
下面,我用一个Demo项目,来逐步完成这个过程。
这个Demo的开发环境是:Mac + VS Code + Dotnet Core 3.1.2。
$ dotnet --info .NET Core SDK (reflecting any global.json): Version: 3.1.201 Commit: b1768b4ae7 Runtime Environment: OS Name: Mac OS X OS Version: 10.15 OS Platform: Darwin RID: osx.10.15-x64 Base Path: /usr/local/share/dotnet/sdk/3.1.201/ Host (useful for support): Version: 3.1.3 Commit: 4a9f85e9f8 .NET Core SDKs installed: 3.1.201 [/usr/local/share/dotnet/sdk] .NET Core runtimes installed: Microsoft.AspNetCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
首先,在这个环境下建立工程:
% dotnet new sln -o demo The template "Solution File" was created successfully.
% cd demo % dotnet new webapi -o demo The template "ASP.NET Core Web API" was created successfully. Processing post-creation actions... Running 'dotnet restore' on demo/demo.csproj... Restore completed in 179.13 ms for demo/demo.csproj. Restore succeeded.
% dotnet sln add demo/demo.csproj Project `demo/demo.csproj` added to the solution.
% dotnet add package Swashbuckle.AspNetCore log : Restore completed in 2.75 sec for demo/demo.csproj.
% dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer log : Restore completed in 3.09 sec for demo/demo.csproj.
五步做完,基础项目就建完了。
看一下整个的目录结构:
% tree . . ├── demo │ ├── Controllers │ │ └── WeatherForecastController.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── WeatherForecast.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── demo.csproj │ └── obj │ ├── demo.csproj.nuget.dgspec.json │ ├── demo.csproj.nuget.g.props │ ├── demo.csproj.nuget.g.targets │ ├── project.assets.json │ └── project.nuget.cache └── demo.sln
在ConfigureServices方法中加入以下代码:
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo", Version = "V1" }); c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.ApiKey, Scheme = "Bearer", BearerFormat = "JWT", In = ParameterLocation.Header, Description = "", }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] {} } }); });
在Configure方法中加入以下代码
app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "Demo V1"); });
关于Swagger的详细配置,这里不做说明,留着以后写。
签发Token是认证的第一步。
用户进到系统,在验证用户帐号密码后,需要根据用户的数据,把Token返回给用户。
这个过程其实跟认证没什么关系,只是一个普通的API功能。
using System; namespace demo.DTOModels { public class LoginRequestDTO { public string username { get; set; } public string password { get; set; } } }
using Microsoft.AspNetCore.Mvc; using demo.DTOModels; namespace demo.Controllers { public class AuthenticationController : ControllerBase { [HttpPost, Route("requesttoken")] public ActionResult RequestToken([FromBody] LoginRequestDTO request) { //这儿待完善 return Ok(); } } }
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "tokenParameter": { "secret": "123456123456123456", "issuer": "WangPlus", "accessExpiration": 120, "refreshExpiration": 1440 } }
这里,tokenParameter节是我们设置的参数。一般来说,是这几个:
secret: JWT加密的密钥。现在主流用SHA256加密,需要256位以上的密钥,unicode是16个字符以上,尽量复杂一些。密钥泄露,Token就会被破解,所以,你懂的。
issuer: 签发人的名称,如果没人注意,你可以把大名写在上面。
accessExpiration: Token的有效分钟数。过了这个时间,这个Token会过期。
refreshExpiration: refreshToken的有效分钟数。过了这个时间,用户需要重新登录。
Token过期后,可以让用户重新登录认证拿Token。但这个方式会比较Low。高大上的方式是签发Token的时候,同时也签发一个refreshToken给用户。用户Token过期后,可以拿refreshToken去申请新的Token,同时刷新refreshToken。如果用户长时间未使用系统,refreshToken也过期了,才让用户重新登录认证。
refreshToken可以用JWT生成,也可以自己生成,不影响认证。
using System; namespace demo.Models { public class tokenParameter { public string Secret { get; set; } public string Issuer { get; set; } public int AccessExpiration { get; set; } public int RefreshExpiration { get; set; } } }
using Microsoft.AspNetCore.Mvc; using demo.DTOModels; using Microsoft.Extensions.Configuration; using System; using System.Text; using demo.Models; using Microsoft.IdentityModel.Tokens; using System.Security.Claims; using System.IdentityModel.Tokens.Jwt; namespace demo.Controllers { public class AuthenticationController : ControllerBase { private tokenParameter _tokenParameter = new tokenParameter(); public AuthenticationController() { var config = new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile("appsettings.json") .Build(); _tokenParameter = config.GetSection("tokenParameter").Get<tokenParameter>(); } [HttpPost, Route("requestToken")] public ActionResult RequestToken([FromBody] LoginRequestDTO request) { //这儿在做用户的帐号密码校验。我这儿略过了。 if (request.username == null && request.password == null) return BadRequest("Invalid Request"); //生成Token和RefreshToken var token = GenUserToken(request.username, "testUser"); var refreshToken = "123456"; return Ok(new[] { token, refreshToken }); } //这儿是真正的生成Token代码 private string GenUserToken(string username, string role) { var claims = new[] { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Role, role), }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenParameter.Secret)); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var jwtToken = new JwtSecurityToken(_tokenParameter.Issuer, null, claims, expires: DateTime.UtcNow.AddMinutes(_tokenParameter.AccessExpiration), signingCredentials: credentials); var token = new JwtSecurityTokenHandler().WriteToken(jwtToken); return token; } } }
这个类里,验证帐号密码的代码我略过了。还有,refreshToken给了一个固定串。真实项目这儿就按需要做就好。
(未完待续)
微信公众账号:老王Plus 如果你想及时得到个人文章以及内容的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号)。 本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 |