Asp.net Core 3.1 Web API添加jwt验证(二):用单例模式简单封装token生成器JwtGenerator

提出问题

在上一篇我们搭建了一个基础的项目框架,并介绍了怎么向其中引入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)
};
// 获取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) 
});

从上面代码可以看出,要想生成一个完整的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 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 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生成操作可还行?

你可能感兴趣的:(设计模式,c#,.net-core,asp.net,jwt)