最近,移动开发的劲头越来越足,学校搞的各种比赛都需要用手机 APP 来撑场面,所以,作为写后端的,很有必要改进一下以往的基于 Session 的身份认证方式了,理由如下:
所以我选择了使用 Jwt (Json Web Token) 这个技术。Jwt 是一种无状态的分布式的身份验证方式,与 Session 相反,Jwt 将用户信息存放在 Token 的 payload 字段保存在客户端,通过 RSA 加密的方式,保证数据不会被篡改,验证数据有效性。 下面是一个使用 Jwt 的系统的身份验证流程:
可以看出,用户的信息保存在 Token 中,而 Token 分布在用户的设备中,所以服务端不再需要在内存中保存用户信息了 身份认证的 Token 传递时以一种相当简单的格式保存在 header 中,方便客户端对其进行操作
Jwt 形式的 token 一般分为 3 个部分,分别是 Header,Payload,Signature,这三个部分使用
分隔。其中前两部分使用 Base64 编码,未经加密处理,第三个部分使用 RSA 加密。 所以一个 Jwt 看起来大概是这个样子:
- .
- header.payload.signature
下面是一个真实的 Jwt:
- eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6InplZWtvIiwicm9sZSI6IiIsIm5hbWVpZCI6MSwianRpIjoiNjNjN2Q3OWY2N2VhMDhjYjRiYzNjMmNkOTJiY2JkNTgiLCJuYmYiOjE0OTQ0MDMwMjQsImV4cCI6MTQ5NTAwNzgyMywiaWF0IjoxNDk0NDAzMDI0LCJpc3MiOiJUZXN0SXNzdWVyIiwiYXVkIjoiVGVzdEF1ZGllbmNlIn0.V7Mfi3FGOTLYV0O5DmOWju7LkDJwZNO6HZN19CHb3ekYxcoVbP51YjYAr0fUHc3RPIp3gxITzziHY - 07xZ2swCaV0K - hiF5IbwpDuvyxsnlgaRxS94wKDGKSJkArC82KukCtm7IuFBxnNr6kxe7tGcebVhqtaqgnxEUg5lKtDtVI85kd17YtzBp9Vxnc3Ie0r - 6KPgUa2HacCf2Pc3hkvY7tZdWZ6ininZlZ - EbcyZI2KTx - vOqdK63MS2JYSw7W2qwf89tsRsORwbB2P4dOBBFK8YSXJpeyGeJWFEMjAMkiH3AeMmW2w_H7r_6Pn - jh5gozzBei4JoHTU6RVDUg1A
Header 部分一般用来记录加密算法跟 Token 类型 举个例子:
- {
- "alg": "HS256",
- "typ": "JWT"
- }
Payload 存放的是一些不敏感的用户数据,因为这一部分仅仅只是使用 Base64 加密,所以不应该用来保存用户的密码之类的信息。
一个例子:
- {
- "sub": "1234567890",
- "name": "John Doe",
- "admin": true
- }
这一部分是 Jwt 最重要的部分,使用 header 中记录的算法进行了加密,加密方式如下:
- HMACSHA256(
- base64UrlEncode(header) + "." +
- base64UrlEncode(payload),
- secret)
所以这个部分可以用来保证用户信息不被篡改,起到验证用户身份的作用
在开发过程中,可以访问 https://jwt.io 来调试 Token 当然,为了更快的访问速度,还可以使用 这个网站
因为 Jwt 本身的特点,所以用来签发 Token 的服务器可以跟应用服务器不是同一台,这样就可以搞微服务之类的东西(反正我不懂。。。) 因此,在这篇博客中,将会创建两个 Web 应用:
首先来搭建我们的 Token 签发服务器吧!
由于要使用到 RSA 加密,所以先创建一个辅助类来帮助简化调用:
RSAUtils.cs
- using System.IO;
- using System.Security.Cryptography;
- using Newtonsoft.Json;
- namespace JwtUtils
- {
- public static class RSAUtils
- {
- /// <summary>
- /// 从本地文件中读取用来签发 Token 的 RSA Key
- /// </summary>
- /// <param name="filePath">存放密钥的文件夹路径</param>
- /// <param name="withPrivate"></param>
- /// <param name="keyParameters"></param>
- /// <returns></returns>
- public static bool TryGetKeyParameters(string filePath, bool withPrivate, out RSAParameters keyParameters)
- {
- string filename = withPrivate ? "key.json" : "key.public.json";
- keyParameters = default(RSAParameters);
- if (Directory.Exists(filePath) == false) return false;
- keyParameters = JsonConvert.DeserializeObject<RSAParameters>(File.ReadAllText(Path.Combine(filePath, filename)));
- return true;
- }
- /// <summary>
- /// 生成并保存 RSA 公钥与私钥
- /// </summary>
- /// <param name="filePath">存放密钥的文件夹路径</param>
- /// <returns></returns>
- public static RSAParameters GenerateAndSaveKey(string filePath)
- {
- RSAParameters publicKeys, privateKeys;
- using (var rsa = new RSACryptoServiceProvider(2048))
- {
- try
- {
- privateKeys = rsa.ExportParameters(true);
- publicKeys = rsa.ExportParameters(false);
- }
- finally
- {
- rsa.PersistKeyInCsp = false;
- }
- }
- File.WriteAllText(Path.Combine(filePath, "key.json"), JsonConvert.SerializeObject(privateKeys));
- File.WriteAllText(Path.Combine(filePath, "key.public.json"), JsonConvert.SerializeObject(publicKeys));
- return privateKeys;
- }
- }
- }
这个工具类能够帮助我们生成 RSA 密钥,并把生成的私钥跟公钥保存在两个文件中,还能从文件中读取密钥。
然后定义一个数据类,用来帮助我们在应用的各个地方获取加密相关的信息:
JWTTokenOptions.cs
- using Microsoft.IdentityModel.Tokens;
- namespace JwtUtils {
- public class JWTTokenOptions {
- public string Audience {
- get;
- set;
- }
- public RsaSecurityKey Key {
- get;
- set;
- }
- public SigningCredentials Credentials {
- get;
- set;
- }
- public string Issuer {
- get;
- set;
- }
- }
- }
接下来在 Startup.cs 中配置 Jwt 的加密选项:
- public void ConfigureServices(IServiceCollection services)
- {
- // 省略了其他的东西
- // 从文件读取密钥
- string keyDir = PlatformServices.Default.Application.ApplicationBasePath;
- if (RSAUtils.TryGetKeyParameters(keyDir, true, out RSAParameters keyParams) == false)
- {
- keyParams = RSAUtils.GenerateAndSaveKey(keyDir);
- }
- _tokenOptions.Key = new RsaSecurityKey(keyParams);
- _tokenOptions.Issuer = "TestIssuer"; // 签发者名称
- _tokenOptions.Credentials = new SigningCredentials(_tokenOptions.Key, SecurityAlgorithms.RsaSha256Signature);
- // 添加到 IoC 容器
- services.AddSingleton(_tokenOptions);
- services.AddMvc();
- }
接下来创建一个控制器,用来提供签发 Token 的 API
TokenController.cs
- namespace JwtIssuer.Controllers { [Route("api/[controller]")] public class TokenController: Controller {
- private readonly JWTTokenOptions _tokenOptions;
- private readonly AuthDbContext _dbContext;
- public TokenController(JWTTokenOptions tokenOptions, AuthDbContext dbContext) {
- _tokenOptions = tokenOptions;
- _dbContext = dbContext;
- }
- /// <summary>
- /// 生成一个新的 Token
- /// </summary>
- /// <param name="user">用户信息实体</param>
- /// <param name="expire">token 过期时间</param>
- /// <param name="audience">Token 接收者</param>
- /// <returns></returns>
- private string CreateToken(User user, DateTime expire, string audience) {
- var handler = new JwtSecurityTokenHandler();
- string jti = audience + user.Username + expire.GetMilliseconds();
- jti = jti.GetMd5(); // Jwt 的一个参数,用来标识 Token
- var claims = new[] {
- new Claim(ClaimTypes.Role, user.Role ? ?string.Empty),
- // 添加角色信息
- new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), // 用户 Id ClaimValueTypes.Integer32),
- new Claim("jti", jti, ClaimValueTypes.String) // jti,用来标识 token
- };
- ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user.Username, "TokenAuth"), claims);
- var token = handler.CreateEncodedJwt(new SecurityTokenDescriptor {
- Issuer = "TestIssuer",
- // 指定 Token 签发者,也就是这个签发服务器的名称
- Audience = audience,
- // 指定 Token 接收者
- SigningCredentials = _tokenOptions.Credentials,
- Subject = identity,
- Expires = expire
- });
- return token;
- }
- /// <summary>
- /// 用户登录
- /// </summary>
- /// <param name="user">用户登录信息</param>
- /// <param name="audience">要访问的网站</param>
- /// <returns></returns>
- [HttpPost("{audience}")] public IActionResult Post([FromBody] User user, string audience) {
- DateTime expire = DateTime.Now.AddDays(7);
- // 在这里来验证用户的用户名、密码
- var result = _dbContext.Users.First(u = >u.Username == user.Username && u.Password == user.Password);
- if (result == null) {
- return Json(new {
- Error = "用户名或密码错误"
- });
- }
- return Json(new {
- Token = CreateToken(result, expire, audience)
- });
- }
- }
- }
现在,访问这个 API(http://localhost:port/api/token/TestAudience) 就可以获取用户的 Token 了
在 Startup.cs 中注册 Jwt 相关的服务:
- public void ConfigureServices(IServiceCollection services)
- {
- // 省略了其他的内容
- // 从文件读取密钥
- string keyDir = PlatformServices.Default.Application.ApplicationBasePath;
- if (RSAUtils.TryGetKeyParameters(keyDir, false, out RSAParameters keyparams) == false)
- {
- _tokenOptions.Key = default(RsaSecurityKey);
- }
- else
- {
- _tokenOptions.Key = new RsaSecurityKey(keyparams);
- }
- _tokenOptions.Issuer = "TestIssuer"; // 设置签发者
- _tokenOptions.Audience = "TestAudience"; // 设置签收者,也就是这个应用服务器的名称
- _tokenOptions.Credentials = new SigningCredentials(_tokenOptions.Key, SecurityAlgorithms.RsaSha256Signature);
- services.AddAuthorization(auth =>
- {
- auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
- .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
- .RequireAuthenticatedUser()
- .Build());
- });
- // Add framework services.
- services.AddMvc();
- }
然后在 Startup.cs 添加 Jwt 认证中间件:
- public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
- // 省略了其他的内容
- app.UseJwtBearerAuthentication(new JwtBearerOptions {
- TokenValidationParameters = new TokenValidationParameters {
- IssuerSigningKey = _tokenOptions.Key,
- ValidAudience = _tokenOptions.Audience,
- // 设置接收者必须是 TestAudience
- ValidIssuer = _tokenOptions.Issuer,
- // 设置签发者必须是 TestIssuer
- ValidateLifetime = true
- }
- });
- }
接着随便创建一个 API 控制器
- namespace JwtAudience.Controllers
- {
- [Route("api/[controller]")]
- public class ValuesController : Controller
- {
- // GET api/values
- [HttpGet]
- [Authorize]
- public IEnumerable<string> Get()
- {
- return new string[] { "value1", "value2" };
- }
- }
- }
首先编译一下应用服务器,但是不要急着运行。因为应用服务器验证 Token 是需要公钥的,所以现在去之前的签发服务器的 build 目录
可以看到生成了两个 json 文件,将其中的 key.public.json 拷贝到应用服务器的对应的目录下面,然后运行应用服务器。
如果我们直接访问应用服务器的 API,就会被挡在外面:
所以现在去把之前拿到的 token 复制出来,然后给这个请求加个请求头——Authorization 值是
- Bearer你的Token
这样,基本的身份验证就完成了~
有兴趣的话还可以把这个 Token 放在前面提到的用来调试 Jwt 网站上,我的 Token 的解析结果是:
这里面的 iss 指的就是签发者,aud 指的是接收者,对于我们的应用服务器来说,这两个参数错了任意一个都将无法通过验证(这里就不演示了,等会儿会有测试代码~)
至此,我们已经把 Jwt 的身份认证基本实现了,但是仔细想想,却发现存在一个很严重的问题————用户的 Token 在过期时间之内根本无法手动设置失效,随之而来的还有重放攻击等等问题!
Jwt 官方也没有提供很好的应对方法,现在就只有一条路可以走,就是把失效的 Token 加入黑名单。只要能够让 Token 失效,之后应对这些安全问题就只是策略上的选择。
在 Jwt 的官方说明中,
这个参数就是用来标识 Token 的。所以,让一个 Token 失效只需要把这个 Token 中的
- jti
加入应用服务器的数据库的黑名单就好了。
- jti
得益于微软对 Identity 良好的设计,我们可以很容易的拓展默认的 Jwt 认证规则
首先创建一个 ValidJtiRequirement 类
- public class ValidJtiRequirement : IAuthorizationRequirement
- {
- }
嗯,他的结构就是这么简单。。。
然后创建一个用来验证这个 Requirement 的 ValidJtiHandler
- public class ValidJtiHandler : AuthorizationHandler<ValidJtiRequirement>
- {
- private readonly AudienceDbContext _dbContext;
- public ValidJtiHandler(AudienceDbContext dbContext)
- {
- _dbContext = dbContext;
- }
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidJtiRequirement requirement)
- {
- // 检查 Jti 是否存在
- var jti = context.User.FindFirst("jti")?.Value;
- if (jti == null)
- {
- context.Fail(); // 显式的声明验证失败
- return Task.CompletedTask;
- }
- // 检查 jti 是否在黑名单
- var tokenExists = _dbContext.BlackRecords.Any(r => r.Jti == jti);
- if (tokenExists)
- {
- context.Fail();
- }
- else
- {
- context.Succeed(requirement); // 显式的声明验证成功
- }
- return Task.CompletedTask;
- }
- }
最后,稍微的修改一下注册服务时的代码
- services.AddAuthorization(auth =>
- {
- auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
- .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
- .RequireAuthenticatedUser()
- .AddRequirements(new ValidJtiRequirement()) // 添加上面的验证要求
- .Build());
- });
- // 注册验证要求的处理器,可通过这种方式对同一种要求添加多种验证
- services.AddSingleton<IAuthorizationHandler, ValidJtiHandler>();
最后再来提供一个使 Token 失效的 API
- namespace JwtAudience.Controllers
- {
- [Route("api/[controller]")]
- public class TokenController : Controller
- {
- private readonly AudienceDbContext _dbContext;
- public TokenController(AudienceDbContext dbContext)
- {
- _dbContext = dbContext;
- }
- [HttpGet]
- public IActionResult Get() => Json(_dbContext.BlackRecords);
- /// <summary>
- /// 使用户的 Token 失效
- /// </summary>
- /// <returns></returns>
- [Authorize("Bearer")]
- [HttpDelete]
- public IActionResult Delete()
- {
- // 从 payload 中提取 jti 字段
- var jti = User.FindFirst("jti")?.Value;
- var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
- if (jti == null)
- {
- HttpContext.Response.StatusCode = 400;
- return Json(new { Result = false });
- }
- // 把这个 jti 加入数据库
- _dbContext.BlackRecords.Add(new BlackRecord { Jti = jti, UserId = userId });
- _dbContext.SaveChanges();
- return Json(new {Result = true});
- }
- }
- }
这里需要注意的是,因为拓展了默认的验证策略,所以需要在
这个特性钦定使用
- Authorize
策略:
- Bearer
- [Authorize("Bearer")]
但是这样就容易在编码的时候出现拼写错误,所以来创建一个继承自这个特性的
类。
- BearerAuthorize
- namespace JwtAudience
- {
- /// <summary>
- /// Jwt 验证
- /// </summary>
- public class BearerAuthorizeAttribute : AuthorizeAttribute
- {
- public BearerAuthorizeAttribute() : base("Bearer") { }
- }
- }
现在我们就可以使用
来替代
- [BearerAuthorize]
- [Authorize]
至此,使 token 失效的能力就具备了。
然后附带一份测试代码,用来检验认证过程是否符合我们的预期: https://coding.net/u/zeeko/p/JwtApplication/git/blob/master/Test/Test.cs
来源: http://www.cnblogs.com/JacZhu/p/6837676.html