Asp.Net Core 3.1 学习4、Web Api 中基于JWT的token验证及Swagger使用

2021-03-08 19:27

阅读:321

标签:file   接收   方案   serialize   依赖   fail   html   反射   过期   

1、初始JWT

1.1、JWT原理

       JWT(JSON Web Token)是目前最流行的跨域身份验证解决方案,他的优势就在于服务器不用存token便于分布式开发,给APP提供数据用于前后端分离的项目。登录产生的 token的项目完全可以独立与其他项目。当用户访问登录接口的时候会返回一个token,然后访问其他需要登录的接口都会带上这个token,后台进行验证如果token是有效的我们就认为用户是正常登录的,然后我们可以从token中取出来一些携带的信息进行操作。当然这些携带的信息都可以通过其他额外的字段进行传递,但是用token传递的话,不用其他额外加其他字段了。

    JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

1.2、JWT结构

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbklEIjoiYWRtaW4iLCJuYmYiOjE1ODc4OTE2OTMsImV4cCI6MTU4NzkyNzY5MywiaXNzIjoiV1lZIiwiYXVkIjoiRXZlcnlUZXN0T25lIn0.-snenNVHrrKq9obN8FzKe0t99ok6FUm5pHv-P_eYc30

第一部分我们称它为头部(header):声明类型,这里是jwt;声明加密的算法 通常直接使用 HMAC SHA256

{
  ‘typ‘: ‘JWT‘,
  ‘alg‘: ‘HS256‘
}

 

第二部分我们称其为载荷(payload, 类似于飞机上承载的物品):

iss:Token发布者

exp:过期时间 分钟

sub:主题

aud:Token接受者

nbf:在此之前不可用

iat:发布时间

jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,如下例:

{

"sub": "1234567890",

"name": "wyy",

"admin": true

}

 

第三部分是签证(signature):这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

2、生成Token

2.1、建立项目

在VS2019中新建一个Core Api程序 Core选3.1 然后在项目上添加一个Jwt文件夹帮助类,新建接口ITokenHelper,类:TokenHelper继承ITokenHelper,类JWTConfig,类TnToken

技术图片

JWTConfig:用来保存读取jwt相关配置

 

/// 
    /// 配置token生成信息
    /// 
    public class JWTConfig
    {
        /// 
        /// Token发布者
        /// 
        public string Issuer { get; set; }
        /// 
        /// oken接受者
        /// 
        public string Audience { get; set; }
        /// 
        /// 秘钥
        /// 
        public string IssuerSigningKey { get; set; }
        /// 
        /// 过期时间
        /// 
        public int AccessTokenExpiresMinutes { get; set; }
    }

TnToken:存放Token 跟过期时间的类 

/// 
    /// 存放Token 跟过期时间的类
    /// 
    public class TnToken
    {
        /// 
        /// token
        /// 
        public string TokenStr { get; set; }
        /// 
        /// 过期时间
        /// 
        public DateTime Expires { get; set; }
    }

 

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

/// 
    /// token工具类的接口,方便使用依赖注入,很简单提供两个常用的方法
    /// 
    public interface ITokenHelper
    {
        /// 
        /// 根据一个对象通过反射提供负载生成token
        /// 
        /// 
        /// 
        /// 
        TnToken CreateToken(T user) where T : class;
        /// 
        /// 根据键值对提供负载生成token
        /// 
        /// 
        /// 
        TnToken CreateToken(Dictionarystring, string> keyValuePairs);
    }

 

TokenHelper:实现类

/// 
    /// Token生成类
    /// 
    public class TokenHelper : ITokenHelper
    {
        private readonly IOptions _options;
        public TokenHelper(IOptions options)
        {
            _options = options;
        }

        /// 
        /// 根据一个对象通过反射提供负载生成token
        /// 
        /// 
        /// 
        /// 
        public TnToken CreateToken(T user) where T : class
        {
            //携带的负载部分,类似一个键值对
            List claims = new List();
            //这里我们用反射把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);
        }

        /// 
        /// 根据键值对提供负载生成token
        /// 
        /// 
        /// 
        public TnToken CreateToken(Dictionarystring, string> keyValuePairs)
        {
            //携带的负载部分,类似一个键值对
            List claims = new List();
            //这里我们通过键值对把数据提供给它
            foreach (var item in keyValuePairs)
            {
                claims.Add(new Claim(item.Key, item.Value));
            }
            //创建token
            return CreateTokenString(claims);
        }
        /// 
        /// 生成token
        /// 
        /// List的 Claim对象
        /// 
        private TnToken CreateTokenString(List 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 };
        }

    }

 2.2、在Startup中去配置jwt相关:

ConfigureServices中:

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

 

 JwtBearerDefaults.AuthenticationScheme与AddJwtBearer();下载两个依赖即可。或者NuGet安装

appsettings中简单配置一下jwt相关的信息:

 "JWTConfig": {
        "Issuer": "WYY", //Token发布者
        "Audience": "EveryTestOne", //Token接受者
        "IssuerSigningKey": "WYY&YL889455200Sily", //秘钥可以构建服务器认可的token;签名秘钥长度最少16
        "AccessTokenExpiresMinutes": "600" //过期时间 分钟
    },

 

Configure中去启用验证中间件:

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

 2.3、一个简单的登录获取token

在Controllers文件夹里面新建一个api 名字LoginTest

 [EnableCors("AllowCors")]
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class LoginTestController : ControllerBase
    {
        private readonly ITokenHelper tokenHelper = null;
        /// 
        /// 构造函数
        /// 
        /// 
        public LoginTestController(ITokenHelper _tokenHelper)
        {
            tokenHelper = _tokenHelper;
        }
        /// 
        /// 登录测试
        /// 
        /// 
        /// 
         [HttpPost]
        public ReturnModel Login([FromBody]UserDto user)
        {
            var ret = new ReturnModel();
            try
            {
                if (string.IsNullOrWhiteSpace(user.LoginID) || string.IsNullOrWhiteSpace(user.Password))
                {
                    ret.Code = 201;
                    ret.Msg = "用户名密码不能为空";
                    return ret;
                }
                //登录操作 我就没写了 || 假设登录成功
                if (1 == 1)
                {
                    Dictionarystring, string> keyValuePairs = new Dictionarystring, string>
                    {
                        { "loginID", user.LoginID }
                    };
                    ret.Code = 200;
                    ret.Msg = "登录成功";
                    ret.TnToken= tokenHelper.CreateToken(keyValuePairs);
                }
            }
            catch(Exception ex)
            {
                ret.Code = 500;
                ret.Msg = "登录失败:"+ex.Message;
            }
            return ret;
        }
    }

 

UserDto接收类

/// 
    /// 登录类Dto
    /// 
    public class UserDto
    {
        /// 
        /// 用户名
        /// 
        public string LoginID { get; set; }
        /// 
        /// 密码
        /// 
        public string Password { get; set; }
    }

 

 ReturnModel 只是我自己封装的一个统一的接口返回格式标准

/// 
    /// 返回类
    /// 
    public class ReturnModel
    {
        /// 
        /// 返回码
        /// 
        public int Code { get; set; }
        /// 
        /// 消息
        /// 
        public string Msg { get; set; }
        /// 
        /// 数据
        /// 
        public object Data { get; set; }
        /// 
        /// Token信息
        /// 
        public TnToken TnToken { get; set; }
    }

 

跨域上篇文章说了这里就不提了

2.4、前端获取token 

我是用传统的MVC的一个启动页面

input type="hidden" id="tokenValue" name="tokenValue" value="" />
br />br />br />
span>Token:span>div id="txtval">div>br />
span>有效期:span>div id="txtvalTime">div>br />

div>
    input type="button" value="获取Token" onclick="getToken()" />br />br />br />
div>
script src="~/Scripts/jquery-3.3.1.js">script>
script type="text/javascript">
    //获取token
    function getToken() {
        var data = JSON.stringify({ LoginID: "admin", Password: "admin888" });
        $.ajax({
            type: "post",
            url: "https://localhost:44331/api/LoginTest/Login",
            dataType: "json",
            async: true,
            data: data,
            contentType: application/json,
            success: function (data) {
                console.log(data);
                $("#txtval").html(data.tnToken.tokenStr);
                $("#txtvalTime").html(new Date(data.tnToken.expires).Format("yyyy-MM-dd hh:mm"));
                $("#tokenValue").val(data.tnToken.tokenStr);

            },
            error: function (data) {
                console.log("错误" + data);
            }
        });
    }
    Date.prototype.Format = function (fmt) { //author: zhengsh 2016-9-5
        var o = {
            "M+": this.getMonth() + 1, //月份
            "d+": this.getDate(), //
            "h+": this.getHours(), //小时
            "m+": this.getMinutes(), //
            "s+": this.getSeconds(), //
            "q+": Math.floor((this.getMonth() + 3) / 3), //季度
            "S": this.getMilliseconds() //毫秒
        };
        if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
        for (var k in o)
            if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
        return fmt;
    }
script>

 

 把Api启动起来 MVC也启动起来试试看

技术图片

 

在JWT管网解码

技术图片

3、验证前端传递的token

现在说说怎么来验证前台传递的jwt,其实很简单,最主要的就是验证token的有效性和是否过期。在接口ITokenHelper中添加验证的两个方法 。TokenHelper中实现

ITokenHelper中添加

/// 
        /// Token验证
        /// 
        /// token
        /// 自定义各类验证; 是否包含那种申明,或者申明的值
        /// 
        bool ValiToken(string encodeJwt, Funcstring, string>, bool> validatePayLoad = null);
        /// 
        /// 带返回状态的Token验证
        /// 
        /// token
        /// 自定义各类验证; 是否包含那种申明,或者申明的值
        /// 
        /// 
        TokenType ValiTokenState(string encodeJwt, Funcstring, string>, bool> validatePayLoad, Actionstring, string>> action);

 

 

 

TokenHelper中添加

/// 
        /// 验证身份 验证签名的有效性
        /// 
        /// 
        /// 自定义各类验证; 是否包含那种申明,或者申明的值, 
        public bool ValiToken(string encodeJwt, Funcstring, string>, bool> validatePayLoad = null)
        {
            var success = true;
            var jwtArr = encodeJwt.Split(.);
            if (jwtArr.Length 3)//数据格式都不对直接pass
            {
                return false;
            }
            var header = JsonConvert.DeserializeObjectstring, string>>(Base64UrlEncoder.Decode(jwtArr[0]));
            var payLoad = JsonConvert.DeserializeObjectstring, 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;
        }
        /// 
        /// 时间转换
        /// 
        /// 
        /// 
        private long ToUnixEpochDate(DateTime date)
        {
            return (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
        }
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        public TokenType ValiTokenState(string encodeJwt, Funcstring, string>, bool> validatePayLoad, Actionstring, string>> action)
        {
            var jwtArr = encodeJwt.Split(.);
            if (jwtArr.Length 3)//数据格式都不对直接pass
            {
                return TokenType.Fail;
            }
            var header = JsonConvert.DeserializeObjectstring, string>>(Base64UrlEncoder.Decode(jwtArr[0]));
            var payLoad = JsonConvert.DeserializeObjectstring, 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;
        }

 

其中TokenType是返回类型成功失败

public enum TokenType
    {
        Ok,
        Fail,
        Expired
    }

在api LoginTest中新增两个验证的方法

/// 
        /// 验证Token
        /// 
        /// token
        /// 
        [HttpGet]
        public ReturnModel ValiToken(string tokenStr)
        {
            var ret = new ReturnModel
            {
                TnToken = new TnToken()
            };
            bool isvilidate = tokenHelper.ValiToken(tokenStr);
            if(isvilidate)
            {
                ret.Code = 200;
                ret.Msg = "Token验证成功";
                ret.TnToken.TokenStr = tokenStr;
            }
            else
            {
                ret.Code = 500;
                ret.Msg = "Token验证失败";
                ret.TnToken.TokenStr = tokenStr;
            }
            return ret;
        }
        /// 
        /// 验证Token 带返回状态
        /// 
        /// 
        /// 
        [HttpGet]
        public ReturnModel ValiTokenState(string tokenStr)
        {
            var ret = new ReturnModel
            {
                TnToken = new TnToken()
            };
            string loginID = "";
            TokenType tokenType = tokenHelper.ValiTokenState(tokenStr, a => a["iss"] == "WYY" && a["aud"] == "EveryTestOne", action => { loginID = action["loginID"]; });
            if (tokenType == TokenType.Fail)
            {
                ret.Code = 202;
                ret.Msg = "token验证失败";
                return ret;
            }
            if (tokenType == TokenType.Expired)
            {
                ret.Code = 205;
                ret.Msg = "token已经过期";
                return ret;
            }

            //..............其他逻辑
            var data = new Liststring, string>>();
            var bb = new Dictionarystring, string>
            {
                { "Wyy", "123456" }
            };
            data.Add(bb);
            ret.Code = 200;
            ret.Msg = "访问成功!";
            ret.Data =data ;
            return ret;
        }

 

 上面一个简单的验证和支持自定义验证的就写好了。下面带有状态的是让我们清楚的知道是什么状态请求登录的时候 或者请求数据的时候,是token过期还是说token没有获取到等等。

ValiTokenState第三个参数我还更了一个系统委托,是这样想的,处理可以验证token,还可以顺便取一个想要的数据,当然其实这样把相关逻辑混到一起也增加代码的耦合性,当时可以提高一点效率不用在重新解析一次数据,当然这个数据也可以通前台传递过来,所以怎么用还是看实际情况,这里只是封装一下提供这样一个方法,用的时候也可以用。

其前端请求代码

 $.ajax({
            type: "post",
            url: "https://localhost:44331/api/LoginTest/ValiToken?tokenStr="+ $("#tokenValue").val(),
            dataType: "json",
            async: true,
            data: { token: $("#tokenValue").val() },
            contentType: ‘application/json‘,
            success: function (data) {
                console.log(data);             
            },
            error: function (data) {
                console.log("错误" + data);
            }
        });

4、Api中过滤器实现通用token验证

项目上新建一个文件夹Filter,在文件夹Filter里新建一个过滤器TokenFilter

namespace JWTToken.Filter
{
    public class TokenFilter : Attribute, IActionFilter
    {
        private ITokenHelper tokenHelper;
        public TokenFilter(ITokenHelper _tokenHelper) //通过依赖注入得到数据访问层实例
        {
            tokenHelper = _tokenHelper;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {

        }
        public void OnActionExecuting(ActionExecutingContext context)
        {
            ReturnModel ret = new ReturnModel();
            //获取token
            object tokenobj = context.ActionArguments["token"];

            if (tokenobj == null)
            {
                ret.Code = 201;
                ret.Msg = "token不能为空";
                context.Result = new JsonResult(ret);
                return;
            }

            string token = tokenobj.ToString();

            string userId = "";
            //验证jwt,同时取出来jwt里边的用户ID
            TokenType tokenType = tokenHelper.ValiTokenState(token, a => a["iss"] == "WYY" && a["aud"] == "EveryTestOne", action => { userId = action["userId"]; });
            if (tokenType == TokenType.Fail)
            {
                ret.Code = 202;
                ret.Msg = "token验证失败";
                context.Result = new JsonResult(ret);
                return;
            }
            if (tokenType == TokenType.Expired)
            {
                ret.Code = 205;
                ret.Msg = "token已经过期";
                context.Result = new JsonResult(ret);
            }
            if (!string.IsNullOrEmpty(userId))
            {
                //给控制器传递参数(需要什么参数其实可以做成可以配置的,在过滤器里边加字段即可)
                //context.ActionArguments.Add("userId", Convert.ToInt32(userId));
            }
        }
    }
}

context.ActionArguments。这是前段请求的时候地址栏带上的参数 token=xxx;这种类型的,不是请求的参数 不然会报错;

把过滤器在startup中注入一下:

 services.AddScoped();

需要验证token的地方,直接加上这个过滤器即可

技术图片

 

前台试试 请求上图的GetList

input type="hidden" id="tokenValue" name="tokenValue" value="" />
br />br />br />
span>Token:span>div id="txtval">div>br />
span>有效期:span>div id="txtvalTime">div>br />

div>
    input type="button" value="获取Token" onclick="getToken()" />br />br />br />
div>
input type="button" value="获取List" onclick="getList()" />br />
script src="~/Scripts/jquery-3.3.1.js">script>
script type="text/javascript">
    //获取token
    function getToken() {
        var data = JSON.stringify({ LoginID: "admin", Password: "admin888" });
        $.ajax({
            type: "post",
            url: "https://localhost:44331/api/LoginTest/Login",
            dataType: "json",
            async: true,
            data: data,
            contentType: application/json,
            success: function (data) {
                console.log(data);
                $("#txtval").html(data.tnToken.tokenStr);
                $("#txtvalTime").html(new Date(data.tnToken.expires).Format("yyyy-MM-dd hh:mm"));
                $("#tokenValue


评论


亲,登录后才可以留言!