@H_301_1@
最近,团队的小伙伴们在做项目时,需要用到JWT认证。遂根据自己的经验,整理成了这篇文章,用来帮助理清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]
?
首先,在这个环境下建立工程:
- 创建Solution
%?dotnet?new?sln?-o?demo The?template?@H_301_102@"Solution?File"?was?created?successfully.
- 用Webapi模板创建项目
%?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.
- 把Demo项目加到Solution中
%?dotnet?sln?add?demo/demo.csproj Project?`demo/demo.csproj`?added?to?the?solution.
- 安装Swagger(这步非必须,我习惯用Swagger,不习惯用Postman)
%?dotnet?add?package?Swashbuckle.AspNetCore log??:?Restore?completed?in?2.75?sec?for?demo/demo.csproj.
- 安装JWT认证支持库(必须引入)
%?dotnet?add?package?Microsoft.AspNetCore.Authentication.JwtBearer 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
?
- 在Startup.cs中补充代码,以启用Swagger
在ConfigureServices方法中加入以下代码:
services.AddSwaggerGen(c?=> { ????????c.SwaggerDoc("v1",?new?OpenApiInfo?{?Title?=?"Demo",?Version?=?"V1"?});
????????c.AddSecurityDefinition("Bearer",1); word-wrap: inherit !important; word-break: inherit !important">new?OpenApiSecurityScheme ????????{ ????????????????Name?=?"Authorization", ????????????????Type?=?SecuritySchemeType.ApiKey, ????????????????Scheme?=?"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是认证的第一步。
用户进到系统,在验证用户帐号密码后,需要根据用户的数据,把Token返回给用户。
这个过程其实跟认证没什么关系,只是一个普通的API功能。
- 工程下加一个目录DTOModels,创建一个LoginRequestDTO的类,用于定义API的输入参数。
using?System;
namespace?demo.DTOModels { ????public?class?@H_502_336@LoginRequestDTO ????{ ????????public?string?username?{?get;?set;?} ????????string?password?{?get;?set;?} ????} }
- 创建一个控制器AuthenticationController,并在控制器里创建一个API方法RequestToken。
using?Microsoft.AspNetCore.Mvc; using?demo.DTOModels;
namespace?demo.Controllers { ????AuthenticationController?:?ControllerBase ????{ ????????[HttpPost,?Route("requesttoken")] ????????public?ActionResult?RequestToken([FromBody]?LoginRequestDTO?request) ????????{ ?????????????? ????????????return?Ok(); ????????} ????} }
- 生成JWT Token需要预设一些参数。我们在appsetting.json里先设置好。
{ ??"Logging":?{ ????"LogLevel":?{ ??????"Default":?"Information", ??????"Microsoft":?"Warning",1); word-wrap: inherit !important; word-break: inherit !important">"Microsoft.Hosting.Lifetime":?"Information" ????} ??}, ??"AllowedHosts":?"*",1); word-wrap: inherit !important; word-break: inherit !important">"tokenParameter":?{ ????"secret":?"123456123456123456", ????"issuer":?"WangPlus",1); word-wrap: inherit !important; word-break: inherit !important">"accessExpiration":?120,1); word-wrap: inherit !important; word-break: inherit !important">"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生成,也可以自己生成,不影响认证。
- 建一个Models目录,创建一个映射tokenParameter的类。这个类不是必须,只是为了写着方便。不想这样写,也可以直接读配置,再转成数据。
namespace?demo.Models { ????tokenParameter ????{ ????????string?Secret?{?get;?string?Issuer?{?get;?public?int?AccessExpiration?{?get;?int?RefreshExpiration?{?get;?set;?} ????} }
- 在前边建好的API - RequestToken中,完成Token和refreshToken的生成和返回。
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;
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,1); word-wrap: inherit !important; word-break: inherit !important">"requestToken")] ????????([FromBody]?LoginRequestDTO?request) ????????{ ???????????? ????????????if?(request.username?==?null?&&?request.password?==?null) ????????????????return?BadRequest("Invalid?Request");
?????????????? ????????????var?token?=?GenUserToken(request.username,1); word-wrap: inherit !important; word-break: inherit !important">"testUser"); ????????????var?refreshToken?=?"123456";
????????????return?Ok(new[]?{?token,?refreshToken?}); ????????}
?????????? ??????????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给了一个固定串。真实项目这儿就按需要做就好。
(未完待续)
?
?
|