Aspnet-Core-31-Web-API添加jwt验证二用单例模式简单封装token生成器JwtGenerator

34次阅读

共计 7179 个字符,预计需要花费 18 分钟才能阅读完成。

提出问题

在上一篇我们搭建了一个基础的项目框架,并介绍了怎么向其中引入 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<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 生成操作可还行?

正文完
 0