• 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().Count > 0
                    || actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes().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 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);
    }
  • 相关阅读:
    Find My护照|苹果Find My技术与护照结合,智能防丢,全球定位
    【系统设计系列】缓存
    hdoj 3549 Flow Problem(最大网络流)
    LVS负载均衡集群
    [附源码]计算机毕业设计JAVA疫情状态下病房管理平台
    webrtc gcc算法(1)
    postman的使用
    2022“杭电杯”中国大学生算法设计超级联赛(4)
    4.28|重量级嘉宾携卓翼飞思RflySim平台亮相国际盛会,内容抢先看!
    JNDI 、C3P0 、DBCP、JDBC、ODBC的区别
  • 原文地址:https://blog.csdn.net/qq_34309663/article/details/126489474