提出问题

在上一篇我们搭建了一个基础的项目框架,并介绍了怎么向其中引入jwt鉴权,不知小伙伴们有没有注意到我们用于生成token的代码片段:

[HttpGet("login")]public ActionResult Login(string username, string password){    if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))    {        // token中的claims用于储存自定义信息,如登录之后的用户id等        var claims = new[]        {            new Claim("userId", username)        };        // 获取SecurityKey        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Authentication")["SecurityKey"]));        var token = new JwtSecurityToken(            issuer: _configuration.GetSection("Authentication")["Issure"],                    // 发布者            audience: _configuration.GetSection("Authentication")["Audience"],                // 接收者            notBefore: DateTime.Now,                                                          // token签发时间            expires: DateTime.Now.AddMinutes(30),                                             // token过期时间            claims: claims,                                                                   // 该token内存储的自定义字段信息            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)    // 用于签发token的秘钥算法        );        // 返回成功信息,写出token        return Ok(new { code = 200, message = "登录成功", data = new JwtSecurityTokenHandler().WriteToken(token) });    }    // 返回错误请求信息    return BadRequest(new { code = 400, message = "登录失败,用户名或密码为空" });}

在这段代码里,我们着重看下面这一段:

// token中的claims用于储存自定义信息,如登录之后的用户id等var claims = new[]{    new Claim("userId", username)};// 获取SecurityKeyvar key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Authentication")["SecurityKey"]));var token = new JwtSecurityToken(    issuer: _configuration.GetSection("Authentication")["Issure"],                    // 发布者    audience: _configuration.GetSection("Authentication")["Audience"],                // 接收者    notBefore: DateTime.Now,                                                          // token签发时间    expires: DateTime.Now.AddMinutes(30),                                             // token过期时间    claims: claims,                                                                   // 该token内存储的自定义字段信息    signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)    // 用于签发token的秘钥算法);// 返回成功信息,写出tokenreturn Ok(new {     code = 200,     message = "登录成功",     data = new JwtSecurityTokenHandler().WriteToken(token) });

从上面代码可以看出,要想生成一个完整的token,我们至少需要知道6个类:

  • Claim:向token中添加自定义信息
  • SymmetricSecurityKey:使用对称方法生成秘钥
  • JwtSecurityToken:初始化JwtToken
  • SigningCredentials:使用秘钥以及算法生成加密证书
  • SecurityAlgorithms:保存了加密方法字符串的常量
  • JwtSecurityTokenHandler:JwtToken处理器

理论上来说,框架封装时对外暴露的类型及方法应越少越好(使用者只要知道尽可能少的几个类就可以实现预期的功能),基于此出发点,我们可以使用设计模式对这个生成token的过程进行改造。

开始改造

先说一说什么是单例模式:

单例模式是GoF总结的23种常见设计模式之一,它保证了在整个程序的运行过程中,有且只有一个调用类的实例。

接下来,就使用单例模式来创建新的Token生成器——JwtGenerator

创建JwtGenerator类

在解决方案中右键项目,创建Services目录,并在其下创建JwtGenerator.cs文件,代码如下:

using System;using System.Security.Claims;using Microsoft.IdentityModel.Tokens;using Microsoft.AspNetCore.Authentication.JwtBearer;using Microsoft.AspNetCore.Authentication;using System.IdentityModel.Tokens.Jwt;using System.Collections.Generic;using System.Text;namespace JwtTest.Services{    public class JwtGenerator    {        // static保证了本类的对象只有一个,且封装在本类内部        private static JwtGenerator _generator = null;        // 用于产生JwtToken的本体生成器        private JwtSecurityToken token = null;        // token自定义的Claim信息        private IEnumerable<Claim> Claims { get; set; } = null;        // 定义token基础信息        // token的颁发者        private string Issuer { get; set; } = string.Empty;        // token的接收者        private string Audience { get; set; } = string.Empty;        // 用于颁布token的秘钥        private string SecurityKey { get; set; } = string.Empty;        // token的秘钥算法        private string Alg { get; set; } = SecurityAlgorithms.HmacSha256;        // token的颁发时间,默认取当前时间        private DateTime NotBefore { get; set; } = DateTime.Now;        // token的过期时间,默认30分钟后        private DateTime Expires { get; set; } = DateTime.Now.AddMinutes(30);        // 构造函数使用private定义,这样外面就无法通过new构造函数来实例化JwtGenerator类        private JwtGenerator() { }        // 这里使用了C#新版本的写法,等同于:        // public static JwtGenerator GetInstance() {        //     if (_generator == null) {        //         _generator = new JwtGenerator();        //     }        //     return _generator;        // }        // 第一次调用GetInstance时,会实例化static标注的_generator对象,后续调用会返回已经实例化的对象,从而保证本类只有一个对象        public static JwtGenerator GetInstance() => _generator ??= new JwtGenerator();        // 添加自定义信息        public JwtGenerator AddClaims(IEnumerable<Claim> claims)        {            _generator.Claims = claims;            return _generator;        }        // 添加发布者        public JwtGenerator AddIssuer(string issuer)        {            _generator.Issuer = issuer;            return _generator;        }        // 添加接收者        public JwtGenerator AddAudience(string audience)        {            _generator.Audience = audience;            return _generator;        }        // 添加发布时间        public JwtGenerator AddNotBefore(DateTime notBefore)        {            _generator.NotBefore = notBefore;            return _generator;        }        // 添加过期时间        public JwtGenerator AddExpires(DateTime expires)        {            _generator.Expires = expires;            return _generator;        }        // 添加用于生成token的秘钥        public JwtGenerator AddSecurityKey(string securityKey)        {            _generator.SecurityKey = securityKey;            return _generator;        }        // 添加token生成算法        public JwtGenerator AddAlgorithm(string securityAlgorithm)        {            _generator.Alg = securityAlgorithm;            return _generator;        }        // 生成token        public string Generate()        {            // 必备参数,若没有初始化,则抛出空指针异常            if (string.IsNullOrEmpty(_generator.SecurityKey)) throw new NullReferenceException("SecurityKey is null");            if (string.IsNullOrEmpty(_generator.Issuer)) throw new NullReferenceException("Issuer is null");            if (string.IsNullOrEmpty(_generator.Audience)) throw new NullReferenceException("Audience is null");            // 调用Generate方法之前,已经调用过上面的Add方法添加了对应的初始化token的参数            _generator.token = new JwtSecurityToken(                issuer: this.Issuer,                audience: this.Audience,                claims: this.Claims,                notBefore: this.NotBefore,                expires: this.Expires,                // 创建token颁发证书                signingCredentials: new SigningCredentials(                    // 使用秘钥字符串跟加密算法生成加密token的对称加密秘钥                    key: new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.SecurityKey)),                    algorithm: this.Alg                )            );            // 调用Token处理器,写出token字符串            return new JwtSecurityTokenHandler().WriteToken(token);        }    }}

上述代码只是对token生成器的简单封装,token的接收者可能有很多个,这时我们可以参考上面的AddXXXX方法,添加AddAudiences功能,其他功能也一样可以自定义。

接下来我们来体验一下刚刚创建的JwtGenerator

使用JwtGenerator

打开AuthController,添加一个新方法Signin()

// GET: api/auth/signin[HttpGet("signin")][AllowAnonymous]public ActionResult Signin(string username, string password){    if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))    {        // 调用JwtGenerator,生成一个新的token        string token = JwtGenerator.GetInstance()                                   .AddIssuer(_configuration.GetSection("Authentication")["Issure"])                                   .AddAudience(_configuration.GetSection("Authentication")["Audience"])                                   .AddSecurityKey(_configuration.GetSection("Authentication")["SecurityKey"]);                                   .AddClaims(new[]                                   {                                       new Claim("userId", username)                                   }).Generate();        return Ok(new { code = 200, message = "登录成功", data = new { token } });    }    return BadRequest(new { code = 400, message = "登录失败,用户名或密码不能为空" });}

运行程序,使用Postman测试GET /api/auth/signin接口吧。

再次思考

经过上面步骤,我们已经封装好了JwtGenerator,代码调用虽然简单了,但整体看上去并没有什么很大的优化。

请跟随我的脚步继续思考

实际开发过程中我们经常会遇到这样一个场景:

用户登录时颁发一个token、用户需要重置密码时颁发一个token、敏感资源访问时又颁发一个token

token经常需要携带允许访问的动作信息供后端校验,来保证自身不被用于其他接口

也就是说在整个后端代码中可能有不少地方我们都会生成新的token,难道每次我们都需要这么写?

string token = JwtGenerator.GetInstance()                           .AddIssuer(_configuration.GetSection("Authentication")["Issure"])                           .AddAudience(_configuration.GetSection("Authentication")["Audience"])                           .AddSecurityKey(_configuration.GetSection("Authentication")["SecurityKey"]);                           .AddClaims(new[]                           {                               new Claim("userId", username)                           }).Generate();

其实不然,仔细思考一下,不管新的token怎么变,总是有一些参数不会变的,比如说秘钥。

那么由此,我们就可以得到下面的写法

打开Startup.cs,在ConfigureServices方法中添加下面代码:

// 配置Token生成器JwtGenerator.GetInstance()            .AddIssuer(_configuration.GetSection("Authentication")["Issure"])            .AddAudience(_configuration.GetSection("Authentication")["Audience"])            .AddSecurityKey(_configuration.GetSection("Authentication")["SecurityKey"]);

因为我这个项目,Issuer跟Audience,SecurityKey都不会变,所以我在Startup中拿到JwtGenerator的实例,并初始化了其中不会改变的配置。

这样我就可以在需要的地方这么调用它:

// 登录鉴权动作string token = JwtGenerator.GetInstance()                           .AddClaims(new[]                           {                               new Claim("action", "login"),                               new Claim("uid", username)                           }).Generate();// 重置密码动作string token = JwtGenerator.GetInstance()                           .AddClaims(new[]                           {                               new Claim("action", "reset"),                               new Claim("uid", username)                           }).Generate();// 敏感资源访问动作string token = JwtGenerator.GetInstance()                           .AddClaims(new[]                           {                               new Claim("action", "oauth"),                               new Claim("source", "/api/admin/xxx")                           }).Generate();

怎么样。这token生成操作可还行?