本项目前端采用原始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); }