iBook网站搭建之二 创建.net core 5 项目 配置JWT权限认证(token)

前面吐槽了一番 现在来记录下后端项目的搭建

当前 将写core项目的基本搭建,token鉴权的获取与使用

首先我们看下搭建的基本样子:

在这里插入图片描述

一、项目搭建

  1. 要搭建core 5 的项目需要将vs2019升级到最新。好像是升级到16版本就可以了
    在这里插入图片描述
  2. 启动vs2019 找到这个ASP.NET Core 空项目 这是空的,什么都需要自己建,也可以选择其他有默认文件的
    在这里插入图片描述
  3. 选择保存位置和设置项目名称就可以了
    在这里插入图片描述
  4. 上面完成后会到这个页面 默认是选择3.1版本的 可以选择修改掉
    在这里插入图片描述
    上面是否启动Docker 看自己的需要,,之后点击创建就可以了
    至此项目已经创建完成了
  5. 创建完成就是如下的样子了 一个空空如也的项目
    在这里插入图片描述
    appsettings.json:以键值对的形式保存的配置文件
    Program.cs:项目的启动入口 一般不做修改
    Startup.cs:core项目的配置入口,不管是AOP、过滤器还是管道的注册与使用 都是在这里面做配置,也是中间件的入口

二、配置JWT权限认证

  1. 添加一个类库 注意是core的类库 目标框架也要选择是net5
    然后创建一个JWT的文件夹,这个文件夹里面存放token的相关配置和验证

添加如下两个类库
在这里插入图片描述

使用引入如下的nugget包:

Microsoft.IdentityModel.Tokens
System.IdentityModel.Tokens.Jwt
Microsoft.Extensions.Options
Newtonsoft.Json
Microsoft.AspNetCore.Mvc.Abstractions
Microsoft.AspNetCore.Mvc.Formatters.Json

之后创建如下的文件:
TokenType.cs: 存放token

  public enum TokenType
    {
        Ok,
        Fail,
        Expired
    }
    /// <summary>
    /// 存放Token 跟过期时间的类
    /// </summary>
    public class TnToken
    {
        /// <summary>
        /// token
        /// </summary>
        public string TokenStr { get; set; }
        /// <summary>
        /// 过期时间
        /// </summary>
        public DateTime Expires { get; set; }
    }

BaseResultModel:统一的返回实体

public class BaseResultModel
    {
        public BaseResultModel(int? code = null, string message = null,
           object result = null, ReturnStatus returnStatus = ReturnStatus.Success)
        {
            this.Code = code;
            this.Data = result;
            this.Msg = message;
            this.ReturnStatus = returnStatus;
        }
        public int? Code { get; set; }

        public string Msg { get; set; }

        public object Data { get; set; }

        public ReturnStatus ReturnStatus { get; set; }
        /// <summary>
        /// token令牌
        /// </summary>
        public TnToken TnToken { get; set; }
    }
    public enum ReturnStatus
    {
        Success = 1,
        Fail = 0,
        ConfirmIsContinue = 2,
        Error = -1
    }

ErrCode:统一返回错误管理

/// <summary>
    /// 错误代码描述
    /// </summary>
    public static class ErrCode
    {

        /// <summary>
        /// 请求成功 文本
        /// </summary>
        public static string err0 = "请求成功";

        /// <summary>
        /// 请求成功代码 200
        /// </summary>
        public static int successCode = 200;

        /// <summary>
        /// 服务器错误代码 500
        /// </summary>
        public static int serverErrCode = 500;

        /// <summary>
        /// 请求失败 文本
        /// </summary>
        public static string err1 = "请求失败";

        /// <summary>
        /// 请求失败代码 201
        /// </summary>
        public static int failCode = 201;

        /// <summary>
        /// 获取access_token时AppID或AppSecret错误。请开发者认真比对appid和AppSecret的正确性,或查看是否正在为恰当的应用调用接口
        /// </summary>
        public static string err40001 = "获取access_token时AppID或AppSecret错误。请开发者认真比对appid和AppSecret的正确性,或查看是否正在为恰当的应用调用接口";

        /// <summary>
        /// 调用接口的服务器URL地址不正确,请联系供应商进行设置
        /// </summary>
        public static string err40002 = "调用接口的服务器URL地址不正确,请联系供应商进行授权";

        /// <summary>
        /// 没有token
        /// </summary>
        public static string err40003 = "未获取到用户令牌token。请开发者验证token是否包含在请求报文的头部中 并且key值为:\"token\" ";

        /// <summary>
        /// 不合法的凭证类型
        /// </summary>
        public static string err40004 = "不合法的凭证类型";

        /// <summary>
        /// 用户令牌accesstoken超时失效
        /// </summary>
        public static string err40005 = "用户令牌accesstoken超时失效";

        /// <summary>
        /// 您未被授权使用该功能,请重新登录试试或联系管理员进行处理
        /// </summary>
        public static string err40006 = "您未被授权使用该功能,请重新登录试试或联系系统管理员进行处理";

        /// <summary>
        /// 传递参数出现错误
        /// </summary>
        public static string err40007 = "传递参数出现错误";

        /// <summary>
        /// 用户未登录或超时
        /// </summary>
        public static string err40008 = "用户未登录或超时";
        /// <summary>
        /// token验证失败
        /// </summary>

        public static string err40009 = "token验证失败";


        /// <summary>
        /// 更新数据失败
        /// </summary>
        public static string err43001 = "新增数据失败";

        /// <summary>
        /// 更新数据失败
        /// </summary>
        public static string err43002 = "更新数据失败";

        /// <summary>
        /// 物理删除数据失败
        /// </summary>
        public static string err43003 = "删除数据失败";

        /// <summary>
        /// 程序异常提示
        /// </summary>
        public static string err500 = "程序异常";

        /// <summary>
        /// 该用户不存在
        /// </summary>
        public static string err50001 = "该用户不存在";

        /// <summary>
        /// 该用户已存在
        /// </summary>
        public static string err50002 = "用户已存在,请登录或重新注册!";

        /// <summary>
        /// 会员注册失败
        /// </summary>
        public static string err50003 = "会员注册失败";

        /// <summary>
        /// 查询数据不存在
        /// </summary>
        public static string err60001 = "查询数据不存在";
    }

JWTConfig.cs :配置token生成信息

public class JWTConfig
    {
        /// <summary>
        /// 用于识别用户信息
        /// </summary>
        public string UID { get; set; }
        /// <summary>
        /// Token发布者
        /// </summary>
        public string Issuer { get; set; }
        /// <summary>
        /// oken接受者
        /// </summary>
        public string Audience { get; set; }
        /// <summary>
        /// 秘钥
        /// </summary>
        public string IssuerSigningKey { get; set; }
        /// <summary>
        /// 过期时间
        /// </summary>
        public int AccessTokenExpiresMinutes { get; set; }
    }

ITokenHelper.cs: token工具类的接口,方便使用依赖注入,很简单提供两个常用的方法

public interface ITokenHelper
    {
        /// <summary>
        /// 根据一个对象通过反射提供负载生成token
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="user"></param>
        /// <returns></returns>
        TnToken CreateToken<T>(T user) where T : class;
        /// <summary>
        /// 根据键值对提供负载生成token
        /// </summary>
        /// <param name="keyValuePairs"></param>
        /// <returns></returns>
        TnToken CreateToken(Dictionary<string, string> keyValuePairs);
        /// <summary>
        /// Token验证
        /// </summary>
        /// <param name="encodeJwt">token</param>
        /// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值</param>
        /// <returns></returns>
        bool ValiToken(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad = null);
        /// <summary>
        /// 带返回状态的Token验证
        /// </summary>
        /// <param name="encodeJwt">token</param>
        /// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值</param>
        /// <param name="action"></param>
        /// <returns></returns>
        TokenType ValiTokenState(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad, Action<Dictionary<string, string>> action);
    }

然后实例上面的接口方法
TokenHelper.cs:

public class TokenHelper : ITokenHelper
    {
        private readonly IOptions<JWTConfig> _options;
        public TokenHelper(IOptions<JWTConfig> options)
        {
            _options = options;
        }

        /// <summary>
        /// 根据一个对象通过反射提供负载生成token
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="user"></param>
        /// <returns></returns>
        public TnToken CreateToken<T>(T user) where T : class
        {
            //携带的负载部分,类似一个键值对
            List<Claim> claims = new List<Claim>();
            //这里我们用反射把model数据提供给它
            foreach (var item in user.GetType().GetProperties())
            {
                object obj = item.GetValue(user);
                string value = "";
                if (obj != null)
                    value = obj.ToString();

                claims.Add(new Claim(item.Name, value));
            }
            //创建token
            return CreateToken(claims);
        }

        /// <summary>
        /// 根据键值对提供负载生成token
        /// </summary>
        /// <param name="keyValuePairs"></param>
        /// <returns></returns>
        public TnToken CreateToken(Dictionary<string, string> keyValuePairs)
        {
            //携带的负载部分,类似一个键值对
            List<Claim> claims = new();
            //这里我们通过键值对把数据提供给它
            foreach (var item in keyValuePairs)
            {
                claims.Add(new Claim(item.Key, item.Value));
            }
            //创建token
            return CreateTokenString(claims);
        }
        /// <summary>
        /// 生成token
        /// </summary>
        /// <param name="claims">List的 Claim对象</param>
        /// <returns></returns>
        private TnToken CreateTokenString(List<Claim> claims)
        {
            var now = DateTime.Now;
            var expires = now.Add(TimeSpan.FromMinutes(_options.Value.AccessTokenExpiresMinutes));
            var token = new JwtSecurityToken(
                issuer: _options.Value.Issuer,//Token发布者
                audience: _options.Value.Audience,//Token接受者
                claims: claims,//携带的负载
                notBefore: now,//当前时间token生成时间
                expires: expires,//过期时间
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256));
            return new TnToken { TokenStr = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires };
        }

        /// <summary>
        /// 验证身份 验证签名的有效性
        /// </summary>
        /// <param name="encodeJwt"></param>
        /// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值, </param>
        public bool ValiToken(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad = null)
        {
            var success = true;
            var jwtArr = encodeJwt.Split('.');
            if (jwtArr.Length < 3)//数据格式都不对直接pass
            {
                return false;
            }

            _ = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[0]));
            var payLoad = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[1]));
            //配置文件中取出来的签名秘钥
            var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(_options.Value.IssuerSigningKey));
            //验证签名是否正确(把用户传递的签名部分取出来和服务器生成的签名匹配即可)
            success = success && string.Equals(jwtArr[2], 
                Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));
            if (!success)
            {
                return success;//签名不正确直接返回
            }

            //其次验证是否在有效期内(也应该必须)
            var now = ToUnixEpochDate(DateTime.UtcNow);
            success = success && (now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString()));

            //不需要自定义验证不传或者传递null即可
            if (validatePayLoad == null)
                return true;

            //再其次 进行自定义的验证
            success = success && validatePayLoad(payLoad);

            return success;
        }
        /// <summary>
        /// 时间转换
        /// </summary>
        /// <param name="date"></param>
        /// <returns></returns>
        private long ToUnixEpochDate(DateTime date)
        {
            return (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="encodeJwt"></param>
        /// <param name="validatePayLoad"></param>
        /// <param name="action"></param>
        /// <returns></returns>
        public TokenType ValiTokenState(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad, Action<Dictionary<string, string>> action)
        {
            if (string.IsNullOrEmpty(encodeJwt))
            {
                return TokenType.Fail;
            }
            var jwtArr = encodeJwt.Split('.');
            if (jwtArr.Length < 3)//数据格式 不对直接pass
            {
                return TokenType.Fail;
            }

            _ = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[0]));
            var payLoad = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[1]));
            var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(_options.Value.IssuerSigningKey));
            //验证签名是否正确(把用户传递的签名部分取出来和服务器生成的签名匹配即可)
            if (!string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1]))))))
            {
                return TokenType.Fail;
            }
            //其次验证是否在有效期内(必须验证)
            var now = ToUnixEpochDate(DateTime.UtcNow);
            if (!(now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString())))
            {
                return TokenType.Expired;
            }

            //不需要自定义验证不传或者传递null即可
            if (validatePayLoad == null)
            {
                action(payLoad);
                return TokenType.Ok;
            }
            //再其次 进行自定义的验证
            if (!validatePayLoad(payLoad))
            {
                return TokenType.Fail;
            }
            //可能需要获取jwt摘要里边的数据,封装一下方便使用
            action(payLoad);
            return TokenType.Ok;
        }

    }

然后添加相关的引入就可以
TokenFilter.cs:token的过滤器 验证权限就在这里做

public class TokenFilter : Attribute, IActionFilter
    {
        private readonly ITokenHelper tokenHelper;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IOptions<JWTConfig> _options;

        //通过依赖注入得到数据访问层实例
        public TokenFilter(ITokenHelper _tokenHelper, IHttpContextAccessor httpContextAccessor, IOptions<JWTConfig> options) 
        {
            tokenHelper = _tokenHelper;
            //获取头部
            _httpContextAccessor = httpContextAccessor;
            _options = options;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {

        }
        public void OnActionExecuting(ActionExecutingContext context)
        {
            BaseResultModel ret = new();
            try
            {
                //获取token
                object tokenobj = null;
                //tokenobj = context.ActionArguments["token"];
                var headers = _httpContextAccessor.HttpContext.Request.Headers;
                tokenobj = headers["token"];

                if (tokenobj == null || string.IsNullOrWhiteSpace(tokenobj.ToString()))
                {
                    ret.Code = 40003;
                    ret.Msg = ErrCode.err40003;
                    context.Result = new JsonResult(ret);
                    return;
                }

                string token = tokenobj.ToString();

                string userId = "";
                //验证jwt,同时取出来jwt里边的用户ID
                TokenType tokenType = tokenHelper.ValiTokenState(token,
                    a => a["iss"] == _options.Value.Issuer && a["aud"] == _options.Value.Audience,
                    action => { userId = action[_options.Value.UID]; });
                if (tokenType == TokenType.Fail)
                {
                    ret.Code = 40009;
                    ret.Msg = ErrCode.err40009;
                    context.Result = new JsonResult(ret);
                    return;
                }
                if (tokenType == TokenType.Expired)
                {
                    ret.Code = 40008;
                    ret.Msg = ErrCode.err40008;
                    context.Result = new JsonResult(ret);
                }
                if (!string.IsNullOrEmpty(userId))
                {
                    //给控制器传递参数(需要什么参数其实可以做成可以配置的,在过滤器里边加字段即可)
                    //context.ActionArguments.Add("userId", Convert.ToInt32(userId));
                }
                //如果token值只剩半小时 则重新生成token并存入返回体

            }
            catch (Exception ex)
            {
                ret.Code = ErrCode.serverErrCode;
                ret.Msg = "token验证失败,异常:" + ex.Message;
                context.Result = new JsonResult(ret);
            }
        }
    }

在appsettings文件里面加入如下代码:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  //JWT 令牌相关配置
  "JWTConfig": {
    "UID": "UID", //存入的用户识别,,注意与JWTConfig中的配置一致
    "Issuer": "October", //Token发布者
    "Audience": "OCT", //Token接受者
    "IssuerSigningKey": "October&YL889455200IBook_API", //秘钥可以构建服务器认可的token;签名秘钥长度最少16
    "AccessTokenExpiresMinutes": "180" //过期时间 分钟
  }
}

相关的设置做完之后 我们进行token的注入并使用
回到Startup文件中
在iBook-core中添加引用:

Microsoft.AspNetCore.Authentication.JwtBearer
添加AOP项目的引入

Startup 中的 ConfigureServices 添加如下代码:

namespace iBook_core
{
    public class Startup
    {
        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            #region jwt配置

            services.AddTransient<ITokenHelper, TokenHelper>();
            //读取配置文件配置的jwt相关配置
            services.Configure<JWTConfig>(Configuration.GetSection("JWTConfig"));
            //启用JWT
            services.AddAuthentication(Options =>
            {
                Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).
            AddJwtBearer();

            //注入token过滤器
            services.AddScoped<TokenFilter>();

            #endregion
        }
     }
}

测试使用:

添加测试的控制器
在这里插入图片描述
添加两个接口:


namespace iBook_core.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly ITokenHelper _tokenHelper;
        private readonly BaseResultModel model;

        public ValuesController(ITokenHelper tokenHelper)
        {
            _tokenHelper = tokenHelper;
            model = new();
        }

        /// <summary>
        /// 获取token
        /// </summary>
        /// <param name="uid"></param>
        /// <returns></returns>
        [HttpGet]
        [Route("getToken")]
        public IActionResult GetToken(string uid)
        {
            Dictionary<string, string> keyValuePairs = new()
            {
                { "UID", uid }
            };
            var token = _tokenHelper.CreateToken(keyValuePairs);
            model.Code = 200;
            model.Msg = "获取token成功";
            model.Data = token;
            model.TnToken = token;

            return Ok(model);
        }

        [ServiceFilter(typeof(TokenFilter))]// 在需要token验证的地方加上这个注解 也可以直接加在控制器上 那么那个控制器下所有的接口都将需要提供token才可以
        [Route("getToken")]
        [HttpPost]
        public IActionResult verifyToken()
        {
            return null;
        }
    }
}

添加 Swagger ,这里可以不添加,用接口处测试工具进行测试也是可以的
引入

Swashbuckle.AspNetCore

将 Startup 修改如下:


namespace iBook_core
{
    public class Startup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddControllers();

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "iBook-core", Version = "v1" });
            });

            #region jwt配置

            services.AddTransient<ITokenHelper, TokenHelper>();
            //读取配置文件配置的jwt相关配置
            services.Configure<JWTConfig>(Configuration.GetSection("JWTConfig"));
            //启用JWT
            services.AddAuthentication(Options =>
            {
                Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).
            AddJwtBearer();

            //注入token过滤器
            services.AddScoped<TokenFilter>();

            //注入请求头部解析
            services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

            #endregion
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "iBook-core v1"));
            }
            app.UseHttpsRedirection();
            app.UseRouting();

            //启用认证中间件 要写在授权UseAuthorization()的前面
             app.UseAuthentication();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

启动项目
如果swagger没正常跳转 那就在地址加上https://localhost:44351/swagger/index.html
token获取:
在这里插入图片描述
token验证失败的情况:
在这里插入图片描述

[ServiceFilter(typeof(TokenFilter))]
// 在需要token验证的地方加上这个注解 也可以直接加在控制器上 那么那个控制器下所有的接口都将需要提供token才可以

这个jwt的使用是几年前在其他地方看到的,找不到原作者了 就没法加上转载链接。为了配合自己的项目 改了一些。
若有侵权 联系删除

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值