web api前后分离开发时,jwt token无感刷新的实现

本项目前端采用原始html ,jquery,css,layui完成,不采用基于razor的asp.net技术,后端采用C#开发的web api实现,而不是.net core实现,整体前后分离

之前本人已经实现在.net core 5.0上实现token的无感刷新,但是发现公司的服务器竟然是windows server2008操作系统的,而.net core5.0最低都需要server2012,无奈只能改成最原始的web api方式重新实现一遍。

思路:

所谓无感刷新,我的逻辑是如果token超时1个小时,则直接提示说token过期,重新登录,如果是1个小时以内,则实现刷新token操作,并将刷新好的token返回到前端,前端保存到localStorage中,下次发送时则发送刷新后的token。

具体实现:

后端部分

引用以下两个,截图如下:

引入过滤器,需要引入两个过滤器,本来只打算引用权限认证的,但是发现无法将刷新后的token存入到header中,所以才多用了一个方法级别的过滤器:

以下过滤器负责拦截未登录就过来的请求,token过期则返回。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using Utils;
using System.Text;

namespace Filters
{
    public class RequestAuthorizeAttribute : AuthorizeAttribute
    {
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            //首先检查 Action 或 Controller 是否允许匿名访问
            if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0
                || actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0)
            {
                base.OnAuthorization(actionContext);
            }
            else 
            {
                //不允许匿名访问
                var content = actionContext.Request.Properties["MS_HttpContext"] as HttpContextBase;
                var token = content.Request.Headers["auth"];
                if (!string.IsNullOrEmpty(token))
                {
                    //删除第一个字符
                    string xxx = token.Substring(1);
                    //删除最后一个字符
                    xxx = xxx.Substring(0, xxx.Length - 1);
                    //解密token
                    string result = TokenUtil.JWTJieM(xxx);
                    //过期了
                    if (result == "expired") 
                    {
                        //继续判断是否已经过期超过1个小时
                        //解析payload,拿到exp
                        var exp = TokenUtil.GetExp(xxx);
                        //解析exp具体时间
                        DateTime expTime = DateTimeUtil.TimeStampToDateTime(long.Parse(exp));
                        //跟当前时间比较,有没有超过1个小时,不超过则刷新,超过则返回token过期,需要重新登录
                        bool isExpire = DateTimeUtil.DiffMin(expTime);
                        if (isExpire)
                        {
                            actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(
                                    HttpStatusCode.BadRequest, "token已过期,请重新登录");
                        }
                        else 
                        {
                            //刷新token
                            xxx = TokenUtil.refreshToken(xxx);

//这一步负责将刷新后的token存入上下文,方便后面调用
                            actionContext.Request.Properties.Add("tokenStr666", xxx);
                        }
                    }
                    else if (result == "invalid")
                    {
                        actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(
                                HttpStatusCode.Unauthorized, string.Format("非法的token"));
                    }
                    else if (result == "error")
                    {
                        actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(
                                HttpStatusCode.Unauthorized, string.Format("token解析出错"));
                    }
                }
                else 
                {
                    HandleUnauthorizedRequest(actionContext);
                }
            }
        }


        protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
        {
            StringBuilder sbMsg = new StringBuilder();
            actionContext.Response = new HttpResponseMessage
            {
                Content = new StringContent("abc", Encoding.UTF8, "application/json"),
                StatusCode = HttpStatusCode.Unauthorized
            };
        }
    }
}

以下方法负责将刷新后的token存入到头部返回

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace Filters
{
    public class IosApproveFilterAttribute :ActionFilterAttribute
    {
        
        //执行方法前
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            base.OnActionExecuting(actionContext);
        }

        //执行方法后
        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            //能到这里,说明可能存在token过期,但没有过期1个小时的情况
            //尝试从请求体中获取到更新的token
            actionExecutedContext.Request.Properties.TryGetValue("tokenStr666", out object myObj);

//这一步必须有,否则前端获取不到token
            actionExecutedContext.Response.Headers.Add("Access-Control-Expose-Headers", "tokenStr666");

//存入到头部,方便前端获取到token
            actionExecutedContext.ActionContext.Response.Headers.Add("tokenStr666", myObj.ToString());
            base.OnActionExecuted(actionExecutedContext);
        }
    }
}

TokenUtil的实现

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;
using System.Threading.Tasks;
using JWT;
using JWT.Algorithms;
using JWT.Serializers;
using log4net;
using Model;
using JWT.Exceptions;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;

namespace Utils
{
    public class TokenUtil
    {
        private static readonly string SecretKey = "ShdusFndk_JshF15451515*@&_YGH$%";
        private static ILog log = LogManager.GetLogger("TokenUtil");


        //生成新的token
        public static string getToken(int userId, string username) //传入用户登录,获取id和name
        {
            //创建token
            //header
            var signingAlgorithm = SecurityAlgorithms.HmacSha256;
            //payload
            var claims = new[]
            {
                   //sub
                   new Claim(ClaimTypes.Sid,userId.ToString()),
                   new Claim(ClaimTypes.Name,username)
            };
            //signiture
            var secretByte = Encoding.UTF8.GetBytes(SecretKey);
            var signingKey = new SymmetricSecurityKey(secretByte);
            var credentials = new SigningCredentials(signingKey, signingAlgorithm);
            var token = new JwtSecurityToken(
                    "Issuer",
                    "Audience",
                    claims,
                    notBefore: DateTime.UtcNow,
                    //expires: DateTime.UtcNow.AddMinutes(10),//过期时间10分钟
                    expires: DateTime.UtcNow.AddSeconds(5),//过期时间5秒
                    signingCredentials: credentials
                 );
            var tokenStr = new JwtSecurityTokenHandler().WriteToken(token);
            return tokenStr;
        }


        public static string JWTJieM(string token)//该方法引用的是jwt.net库,其余的使用的net core支持的jwt库
        {
            try
            {
                IJsonSerializer serializer = new JsonNetSerializer();
                IDateTimeProvider provider = new UtcDateTimeProvider();
                IJwtValidator validator = new JwtValidator(serializer, provider);
                IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
                IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); // symmetric
                IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
                var json = decoder.Decode(token, SecretKey, true);
                //校验通过,返回解密后的字符串
                return json;
            }
            catch (TokenExpiredException)
            {
                //表示过期
                //在这里解析过期时间,和expired一起返回

                return "expired";
            }
            catch (SignatureVerificationException)
            {
                //表示验证不通过
                return "invalid";
            }
            catch (Exception)
            {
                return "error";
            }
        }


        //获取过期时间
        public static string GetExp(string token)
        {
            //继续判断是否已经过期超过1个小时
            //解析payload,拿到exp
            var handler = new JwtSecurityTokenHandler();
            var payload = handler.ReadJwtToken(token).Payload;
            var claims = payload.Claims;
            return claims.First(claim => claim.Type == "exp").Value;
        }


        //刷新过期的token
        public static string refreshToken(string accessToken)
        {
            if (string.IsNullOrWhiteSpace(accessToken)) return "404";
            var userClaims = GetClaimsPrincipalFromAccessToken(accessToken);
            if (userClaims == null) return "404";
            //声明
            var claims = new[]
            {
              //用户ID
              new Claim(ClaimTypes.Sid,userClaims.FindFirst(u=>u.Type.Equals(ClaimTypes.Sid)).Value),
              //用户名
              new Claim(ClaimTypes.Name,userClaims.FindFirst(u=>u.Type.Equals(ClaimTypes.Name)).Value)
            };
            //设置秘钥
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));
            //设置凭证
            var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            //生成token
            var jwtToken = new JwtSecurityToken(
                "Issuer", 
                "Audience", 
                claims,
                expires: DateTime.UtcNow.AddMinutes(10),//刷新后更新10分钟有效期
                signingCredentials: credentials
                );
            string newToken = new JwtSecurityTokenHandler().WriteToken(jwtToken);
            return newToken;
        }

        //根据token,获取身份声明的代码
        private static ClaimsPrincipal GetClaimsPrincipalFromAccessToken(string token)
        {
            var jwtSecurityToken = new JwtSecurityTokenHandler();
            var claimsPrincipal = jwtSecurityToken.ValidateToken(token, new TokenValidationParameters
            {
                ValidateAudience = false,
                ValidateIssuer = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)),
                ValidateLifetime = false
            }, out _);
            return claimsPrincipal;
        }

    }
}

具体应用,

不需要在WebApiConfig.cs中注册过滤器,考虑到前端页面存在多个ajax时,过滤器会被多次

调用,可以在需要验证的方法上添加标记即可,如下所示:

public class DataController : ApiController
    {

        private ILog log =LogManager.GetLogger("DataController");

        //页面初始化时,查询信息
        [HttpGet]
        [RequestAuthorize]
        [IosApproveFilter]
        public ResponesData getTable()
        {
            ResponesData resp = new ResponesData();
            //List<DeviceInfo> devList = new DevDAL().getDevice();
             resp.data = devList;
            //resp.count = new DevDAL().getDeviceNum().ToString();
            resp.code = 200;
            return resp;
        }

}

 前端实现

考虑每次发送都需要发送token,则可以设置一个全局ajax来处理,如下图所示:

//$.ajaxSetup方法为ajax请求设置全局默认行为,即项目中只要用到了ajax请求,就会触发这个方法
//为所有 AJAX 请求设置默认 URL 和 success 函数:
//每次发送ajax请求时,将token从localStorage中取出后存入到http请求的头部
layui.use(['form', 'table'], function () {
    let $ = layui.jquery;
    //let api="http://localhost:3000/api/";
    $.ajaxSetup({
        cache : false,
        //默认是true,即为异步方式,ajax执行后,会继续执行ajax后面的脚本,直到服务器端返回数据后,触发$.ajax里的success方法,这时候执行的是两个线程。若要将其设置为false,则所有的请求均为同步请求,在没有返回值之前,同步请求将锁住浏览器,用户其它操作必须等待请求完成才可以执行。
        async:false,
        /*beforeSend:function(xhr){
            console.log('ajax开始发送前的准备,往头部放入token.........................................................');
            let tokenStr=localStorage.getItem("token");
            console.log('tokenStr:',tokenStr);
            if(tokenStr!=null){
                xhr.setRequestHeader('auth',tokenStr);
            }
        },*/
        headers: { "auth":localStorage.getItem("token") },//通过请求头来发送token,放弃了通过cookie的发送方式
        success(result,status,xhr){
            console.log('来自jq.js的success的result值21:',result);
            console.log('来自jq.js的success的status值21:',status);
            console.log('来自jq.js的success的xhr值21:',xhr);
        },
        //这里可以理解为token没有或者已经过期失效
        error : function(xhr) {
            console.log('xhr22',xhr);
        },
        complete:function (event,xhr,options) {
            let tokenStr=event.getResponseHeader('tokenStr666');
            if(tokenStr!==null){
                console.log('更新token');
                localStorage.setItem('token',tokenStr);
            }
        }
    });

});

一般业务ajax请求

function getTableData(){
    //console.log('开始执行初始化工作....');
    let loadIndex = layer.load(2);
    $.ajax({
        //请求类型
        type:"get",
        url:api+"Data/getTable",
        async:false,
        dataType:"text",
        success:function (result,status,xhr) {
            console.log('来自table的success的result值11:',result);
            console.log('来自table的success的status值11:',status);
            console.log('来自table的success的xhr值11:',xhr);
            /*
            console.log('xhr.getResponseHeader(tokenstr666)11',xhr.getResponseHeader('tokenstr666'));
            console.log('xhr.getAllResponseHeaders()11:',xhr.getAllResponseHeaders());*/
            if(status==='success'){
               // console.log('运行到这里了.......................');
                let res = JSON.parse(result);
                //console.log('2初始化。。。。');
              // console.log('resTable2',res);
              //  console.log('res.data',res.data);
                //console.log('res.msg',res.msg);
                table.reload('currentTableId',{data:res.data});
            }
        },
        error:function (xhr) {
            console.log('出错啦!getTable',xhr);
            ///layer.msg(xhr.responseText);
        }
    });
    layer.close(loadIndex);
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值