实验环境

  • Microsoft Visual Studio 2019
  • Windows 10 x64 家庭中文版
  • .NET Core 5.0
  • PostMan 6.7.4

准备环境

  • 一个利用VS创建的默认Web API示例项目(如果不了解怎么创建请点击此了解,现在还没加…)

文件结构

image-20210525224810945

开始建立基本的测试项目

首先新建一个Model文件夹,添加一个JWT的配置信息模型,文件名为JwtConfInfo.cs(以下模型类似)

//这个模型看自己后期需要进行完善,目前这个测试用不到
namespace 身份验证功能测试.Model
{
    public class JwtConfInfo
    {
        /// <summary>
        /// JWT签名秘钥,注意保密
        /// </summary>
        public string SecretKey { get; set; }
        /// <summary>
        /// JWT颁发者
        /// </summary>
        public string Issuer { get; set; }
        /// <summary>
        /// JWT受众
        /// </summary>
        public string Audience { get; set; }
        /// <summary>
        /// Token持续时间
        /// </summary>
        public int ExpTime { get; set; }
    }
}

接下来在appsetting.json配置文件中写入JWT的配置信息

{
  "AllowedHosts": "*",
  "Jwt": {
    "SecretKey": "BAE7B29E-CGTY-41E0-BAD6-FDDBASCFCJ2Q",
    "Issuer": "https://localhost:44355",
    "Audience": "api",
    "ExpTime": 60
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
} 

Startup中注入服务,修改Startup.cs文件

//ConfigureServices方法中的修改如下
public void ConfigureServices(IServiceCollection services)
{
    var jwtConfig = Configuration.GetSection("Jwt");// 获取appsetting.json配置文件信息,Jwt为定位参数
    //生成密钥
    var symmetricKeyAsBase64 = jwtConfig.GetValue<string>("SecretKey");// 获取SecretKey
    var keyByteArray = Encoding.UTF8.GetBytes(symmetricKeyAsBase64); // 对原始的SecretKey进行base64编码
    var signingKey = new SymmetricSecurityKey(keyByteArray); // 获取解密信息
    //认证参数
    services.AddAuthentication("Bearer")// 注册验证服务
        .AddJwtBearer(o =>
                      {
                          o.TokenValidationParameters = new TokenValidationParameters
                          {
                              ValidateIssuerSigningKey = true,//是否验证签名,不验证的画可以篡改数据,不安全
                              IssuerSigningKey = signingKey,//解密的密钥
                              ValidateIssuer = false,//是否验证发行人,就是验证载荷中的Iss是否对应ValidIssuer参数
                              ValidIssuer = jwtConfig.GetValue<string>("Issuer"),//发行人
                              ValidateAudience = false,//是否验证订阅人,就是验证载荷中的Aud是否对应ValidAudience参数
                              ValidAudience = jwtConfig.GetValue<string>("Audience"),//订阅人
                              ValidateLifetime = true,//是否验证过期时间,过期了就拒绝访问
                              ClockSkew = TimeSpan.Zero,//这个是缓冲过期时间,也就是说,即使我们配置了过期时间,这里也要考虑进去,过期时间+缓冲,默认好像是7分钟,你可以直接设置为0
                              RequireExpirationTime = true,//是否要求请求头中包含到期时间
                          };
                          o.Events = new JwtBearerEvents
                          {
                              //此处为权限验证失败后触发的事件,目的是修改默认Token失效的返回信息,让返回信息更加的友好
                              OnChallenge = context =>
                              {
                                  //此处代码为终止.Net Core默认的返回类型和数据结果,这个很重要哦,必须
                                  context.HandleResponse();

                                  //自定义自己想要返回的数据结果,我这里要返回的是Json对象,通过引用Newtonsoft.Json库进行转换
                                  var payload = JsonConvert.SerializeObject(new { Code = "401", Message = "很抱歉,您无权访问该接口。" });
                                  //自定义返回的数据类型
                                  context.Response.ContentType = "application/json";
                                  //自定义返回状态码,默认为401 我这里改成 200
                                  context.Response.StatusCode = StatusCodes.Status200OK;
                                  //context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                                  //输出Json数据结果
                                  context.Response.WriteAsync(payload);
                                  return Task.FromResult(0);
                              }
                          };
                      });
    //以下是项目默认生成的注册服务,用于接口的测试,前期建议不删除。
    services.AddControllers();
    services.AddSwaggerGen(c =>
                           {
                               c.SwaggerDoc("v1", new OpenApiInfo { Title = "身份验证功能测试", Version = "v1" });
                           });
}


//Configure方法中的修改如下
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "身份验证功能测试 v1"));
    }
    //注意下面的这三个顺序,不能随意调整,不了解的情况下建议就用这样的顺序即可
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
                     {
                         endpoints.MapControllers();
                     });
}

在Model目录下新建以下几个模型

//LoginRequestInfo.cs 
public class LoginRequestInfo
 {
     /// <summary>
     /// 登录请求的username信息
     /// </summary>
     public string username { get; set; } = "-1"; //用户名
     /// <summary>
     /// 登录请求的userpassword信息
     /// </summary>
     public string userpassword { get; set; } = "-1"; //用户密码
 }
//ResponseMessage.cs 响应消息模型
public class ResponseMessage
{
    /// <summary>
    /// 响应码
    /// </summary>
    public int code { get; set; } = -1;
    /// <summary>
    /// 响应消息
    /// </summary>
    public string message { get; set; } = "-1";
    /// <summary>
    /// 响应数据
    /// </summary>
    public object data { get; set; }
}
//UserInfo.cs
public class UserInfo
{
    /// <summary>
    /// 用户信息类
    /// </summary>
    /// 
    /// <summary>
    /// 用户角色
    /// </summary>
    public string roles { get; set; } = "-1";
    /// <summary>
    /// 用户介绍
    /// </summary>
    public string introduction { get; set; } = "-1";
    /// <summary>
    /// 用户头像
    /// </summary>
    public string avatar { get; set; } = "-1";
    /// <summary>
    /// 用户名
    /// </summary>
    public string name { get; set; } = "-1";
}
//Users.cs
using Newtonsoft.Json;
using System.Collections.Generic;
using 身份验证功能测试.Tool.JWT;

namespace 身份验证功能测试.Model
{
    public class Users
    {
        /// <summary>
        /// Users用户对象,并且实例化
        /// </summary>
        public int uid { get; set; } = -1; //用户ID
        public string username { get; set; } = "-1"; //用户名
        public string userpassword { get; set; } = "-1"; //用户密码

        /// <summary>
        /// 用户登录方法
        /// </summary>
        /// <param name="user">通过LoginController中传过来的Users对象</param>
        /// <returns>包含Token的响应信息</returns>
        public ResponseMessage userLogin(Users user)
        {
            ResponseMessage responseMessage = new ResponseMessage(); //声明一个响应信息类,用来结构化响应返回信息
            if (user.username == "mzs" && user.userpassword == "123456") //进行简单的用户名以及密码的检验
            {
                int uid = 1; //模拟用户id
                user.uid = uid; //初始化userid
                Dictionary<string, string> token = new Dictionary<string, string>(); // 创建一个字典用来规范化token在响应信息中的格式
                TokenClass jwtHelper = new TokenClass(); //实例化token工具类
                token.Add("token", new TokenClass().getToken(user)); // 传入user对象并且生成一个带有user中个人信息的token,并且添加到token字典中
                responseMessage.code = 20000; // 定义状态码
                responseMessage.message = "success!"; // 定义响应消息
                responseMessage.data = JsonConvert.SerializeObject(token); // 将规范化的token放到响应信息的data键中,方便前端读取

            }
            return responseMessage;
        }
    }
}

在Tool/JWT目录(没有目录就需要新建)下新建Token工具类,TokenClass

using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Security.Claims;
using System.Text;
using 身份验证功能测试.Model;

namespace 身份验证功能测试.Tool.JWT
{
    /// <summary>
    /// 这是关于Token的操作类
    /// </summary>

    public class TokenClass
    {
       public string getToken(Users user)
        {
            var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory()) //获取当前目录
            .AddJsonFile("appsettings.json", optional: false); //获取appsetting.json文件
            var config = builder.Build();
            var jwtConfig = config.GetSection("Jwt");
            var expTime = jwtConfig.GetValue<int>("ExpTime");// 获取配置文件中Token的有效时长
            //秘钥,就是标头,这里用Hmacsha256算法,需要256bit的密钥
            var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.GetValue<string>("SecretKey")));
            var securityKey = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.GetValue<string>("SecretKey"))), SecurityAlgorithms.HmacSha256);
            //Claim,JwtRegisteredClaimNames中预定义了好多种默认的参数名,也可以像下面的Guid一样自己定义键名.
            //ClaimTypes也预定义了好多类型如role、email、name。Role用于赋予权限,不同的角色可以访问不同的接口
            //相当于有效载荷playload
            var claims = new Claim[] { //创建一个playload对象
            new Claim(JwtRegisteredClaimNames.Iss,jwtConfig.GetValue<string>("Issuer")),
            new Claim(JwtRegisteredClaimNames.Aud,jwtConfig.GetValue<string>("Audience")),
            new Claim("Guid",Guid.NewGuid().ToString("D")),// 全球唯一标识符
            new Claim("Uid",user.uid.ToString()), //添加用户id,自定义键名
            new Claim(ClaimTypes.Name,user.username.ToString()),// 添加用户名,使用自带的键名
            new Claim(ClaimTypes.Role,"system"), // 添加用户角色
            new Claim(ClaimTypes.Role,"admin"), // 添加用户角色
            };
            SecurityToken securityToken = new JwtSecurityToken( //填充playload信息
                notBefore:DateTime.Now,
                expires: DateTime.Now.AddSeconds(expTime),//过期时间
                claims: claims, // 填充上面的用户个人信息
                signingCredentials: securityKey //添加签名秘钥
            );
            //生成jwt令牌
            return new JwtSecurityTokenHandler().WriteToken(securityToken);
        }
    }
}

至此我们的Token生成的方法创建完成,接下来就是调用生成Token了,登录方法Login

//LoginController.cs 这是登录控制器,即登录的接口
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using 身份验证功能测试.Model;

namespace 身份验证功能测试.Controllers
{
    [ApiController] //定义为Api接口类型
    [Route("api/user/[controller]")] //定义路由,即可通过URL+端口号/api/user+方法名调用以下方法
    public class LoginController:ControllerBase
    {/// <summary>
     /// 登录接口
     /// </summary>
     /// <param name="loginRequestInfo">前端所发送的登录信息</param>
     /// <returns>返回Json格式的响应数据,其中包含了Token</returns>
        [AllowAnonymous] //允许匿名访问
        [HttpPost] //该接口只能通过POST方法连接
        public ActionResult Login(LoginRequestInfo loginRequestInfo)
        {
            //实例化并初始化一个Users的对象
            Users user = new Users();
            user.username = loginRequestInfo.username; //初始化username
            user.userpassword = loginRequestInfo.userpassword; //初始化userpassword
            return new JsonResult(user.userLogin(user)); //调用user中的userLogin方法生成Token
        }
    }
}

现在已经完成登录接口的创建,接下来是创建一个获取用户信息的接口进行测试权限控制

//GetUserInfoController.cs 获取用户信息控制器
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Security.Claims;
using 身份验证功能测试.Model;

namespace 身份验证功能测试.Controllers
{
    [ApiController]
    [Route("api/user/[controller]")] //定义路由
    public class GetUserInfoController: ControllerBase //继承ControllerBase基类,下面的this.User需要用
    {
        /// <summary>
        /// 获取用户信息接口
        /// </summary>
        /// <param name="token">前端返回的数据,现阶段token playload中包含用户名以及用户ID,即uid</param>
        /// <returns>返回状态码、身份权限、个人简介、头像、用户名、用户id</returns>
        [Authorize(Roles = "admin")] //只允许admin身份的用户访问
        [HttpGet]
        public ActionResult getuserinfo()
        {
            ResponseMessage responseMessage = new ResponseMessage(); // 实例化响应信息类
            Dictionary<string, string> userinfo = new Dictionary<string, string>();// 声明一个用户信息字典
            // 填充模拟的个人信息
                responseMessage.message = "获取个人信息成功!";
                userinfo.Add("roles", "admin");
                userinfo.Add("username", this.User.FindFirstValue(ClaimTypes.Name)); // 这个是在进行权限验证之后顺带对token进行解析后生成的playload对象,可根据在TokenClass中填充的格式进行读取用户信息
                                                                                     // 由于在TokenClass中是将username赋值给ClaimTypes.Name属性,因此可以利用该方法进行读取
                userinfo.Add("avatar", "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
                userinfo.Add("uid", this.User.FindFirst("Uid").Value); // 由于在TokenClass中是利用自定义的方式声明的Uid键,因此需要使用这样的方式进行读取uid
                responseMessage.code = 20000; // 定义状态码
                responseMessage.message = "success!"; // 定义响应消息
            responseMessage.data = JsonConvert.SerializeObject(userinfo); // 填充响应消息
            return new JsonResult(responseMessage);
        }
    }
}

到这我们的权限验证测试项目已经搭建好基本框架了,可以启动项目来利用项目自带的接口测试工具swagger来对接口进行测试

测试

首先启动项目,注意不用IIS进行发布,而是通过项目自身的程序进行发布,需要点击的是如下图所示的选项

image-20210526090538621

得到的界面如下

image-20210526092015055

  • 测试Login接口

单击接口之后可看到接口详情

image-20210526092344404

接口详情页如下

image-20210526092649241

点击Try it out修改参数对接口进行测试

image-20210526093035450

执行之后响应信息就在其下面展示

image-20210526093144732

其中token就在响应信息的data中,用Postman软件对获取用户信息接口进行测试

  • 测试GetUserInfo接口

打开Postman新建一个请求,并且按照如下图填写信息,发送请求查看获取到的信息

image-20210526094955813

  • 验证权限控制

将GetUserInfo接口的角色权限修改为除system和admin之外的角色,我这里设置为user,修改如下

image-20210526095925481

使用刚刚的admin身份用户登录生成的token访问GetUserInfo接口,测试身份验证功能是否生效,测试结果如下

image-20210526100304848

注意:这个可以通过修改失败响应事件来提供更加友善的提示信息,当然也可以保持原样,根据实际需要决定。身份验证失败我没有做相应的相应消息,但是token验证失败的响应消息在Startup中已经做了自定义,现在进行token有效时间的测试。

  • Token有效时长测试

注意:测试前记得将获取用户信息接口的身份权限设置为admin

我们在上面的appsetting.json中设置的有效时间是60,在添加ExpiresTime时是选择以秒为单位,因此Token有效时长为60秒(这个可以根据需求进行修改,现只是为了方便测试)

image-20210526100952923

获取token

image-20210526101918243

在时效内可以获取到信息

image-20210526102004859

过了时长(暂定为60秒,即一分钟)后显示没有权限进行访问

image-20210526102055145

通过以上测试表示token有效时长功能限制成功。

总结

本次测试主要包含以下功能的测试

  • token的生成
  • 利用token来获取特定用户的信息
  • 基于JWT的身份权限验证
  • JWT有效时长测试

这些功能都是JWT的基本功能,我们通过在项目中加入该模块,即可在每一个接口的入口处加一个权限验证属性即可进行身份的验证,这无疑方便了我们的鉴权,这个方案还有很多扩展,例如Token刷新等,这个后期我也会加入到我的项目中,如果有需要我也会出一篇文档。

网上还有其他前辈写的更好的方案,我也是在不断地学习,不断地优化中,这也很好地诠释了“学无止境”,如果文档中有错误欢迎指出,因为这个也是匆忙写出来的,同时如果有更好的建议也欢迎交流。

参考资料

NetCore WebApi使用Jwtbearer实现认证和授权 – 平民的麦田 – 博客园 (cnblogs.com)

.Net Core官方JWT授权验证的全过程实用技巧脚本之家 (jb51.net)