以token处理登录的web系统,一般会有两个token:access-token和refresh-token。

node.js中,一般用jsonwebtoken这个模块。

access-token,是用户输入登录的账号密码,后台去db验证然后颁发的,它一般记录在浏览器的cookie中,并在浏览器关闭时自动删除,页面访问或ajax访问会自动通过cookie传回到后台,后台直接内存中校验,不用访问db,所以效率高;为了在access-token泄漏后及时止损,一般access-token会设置一个有效期,如1-8小时。

access-token设置了有效期后,过期了怎么办?为了及时止损,有效期不能设置太长,过期是一定会遇到的,比如工作狂,如果有效期设置的是8小时,他开着浏览器工作12小时,费力断断续续花了1个小时(电话多,喝咖啡尿多)打了张订单,提交时token过期了。再比如某些大屏幕展示的页面,可能连续几天几月的开着。遇到过期怎么办?

  1. 重定向到登录页面,用户输入账号密码登录后,再自动跳回订单页面,之前的资料都丢了,用户骂一句“靠”忍气吞声重新打。如果每天遇到一次,可能还可以忍。如果有效期太短,如1小时,每天遇到5,6次,那用户可能不干了,这时你可能要把未提交的订单资料暂时存到localstorage里面。
  2. 弹出登录框。登录框内容和代码如何做,预先就加载了,每个页面都有这部分,感觉很浪费,因为大部分时间用不上,动态从后台加载,可能不好实现。
  3. 登录后把账号密码记录在浏览器中,自动登录。但基于安全考虑,一般是用户特别勾选“记住我”,才会加密记录账号密码到localstorage中,用户下次打开浏览器时自动登录。如果token过期就自动登录,如何及时止损?后台修改密码或禁用账号,如何同步到前端的localstorage中。大部分app是这么干的。以上3种都是要再次去后台数据库验证,所以token过期时间不能太短,否则效率很差。
  4. 设置refresh-token机制,颁发access-token时同时颁发一个refresh-token,唯一区别是refresh-token有效期比较长,比如1个月。当access-token过期后,拿着refresh-token到后台换取新的access-token。通过在后台为refresh-token设置黑名单来及时止损,所以有黑名单的时候,可能效率也会一样的差。refresh-token也过期后,那就只有老老实实的让用户输入账号密码登录了,就是前面的1,2方法。因为refresh-token不常用,所以最好不要放在cookie中避免每次自动传到后台,放在localstorage较好。

刷新access-token过程,如何让用户没感觉?思路是:发现access-token,自动用refresh-token去刷新,然后再自动跳到原来页面或者自动调用一次原来的ajax。

  1. web页面访问时,nodejs后台中间件拦截到access-token过期,返回一个html,里面包含刷新token和跳转:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src='/js/jquery-1.11.1/jquery-1.11.1.min.js'></script>
</head>
<body>
<script>
var raw_url="<%-locals.raw_url%>";
var login_url="<%-locals.login_url%>";
var refresh_token=localStorage.getItem("<%-locals.cookie.IDs.refreshToken%>");
$.ajax({
    cache:false,
    method:"get",
    async:true,
    url:'/app/account/refreshToken',
    data:{       
        refresh_token:refresh_token
    },
    success:function(data, textStatus){
        location.href = raw_url;
    },
    error:function(XMLHttpRequest, textStatus, errorThrown){
        //access-token刷新失败(可能refresh-token过期或者账号密码改了),重定向到登录页面
        location.href = login_url;
    }
});
</script>
</body>
</html>
  1. ajax调用时,封装一下jquery的ajax:
yjClient.ajax=function(options){
    var data=options.data;
	var url=options.url;

	function callAjax(){
    	$.ajax({
    		cache:options.cache?options.cache:false,
    		method:options.method?options.method:(options.type?options.type:'get'),
    		async:(options.async==undefined)?true:options.async,
    		url:url,
        	data:data,
        	success:function(data, textStatus){
    			if (options.success){
    				options.success(data);
    			}				
    		},
    		error:function(XMLHttpRequest, textStatus, errorThrown){
    		    if (XMLHttpRequest.responseText){
    		        var msg=XMLHttpRequest.responseText;
    		        var err=JSON.parse(XMLHttpRequest.responseText);
    		    }
    		    else{
    		        var msg=yjClient.getAjaxErrorMsg(XMLHttpRequest,textStatus,errorThrown);
    		        var err=new Error(msg);
    		    }
    		    if (err.code=="tm.err.foil.accessTokenExpired"){
    		        console.log('refresh token...');
    		        var refresh_Token=refresh_token=localStorage.getItem(yjClient.cookieIDs.refreshToken);
    		        $.ajax({
    		            cache:false,
    		            method:"get",
    		            async:true,
    		            url:'/app/account/refreshToken',
    		            data:{
    		                refresh_token:refresh_token
    		            },
    		            success:function(data, textStatus){
    		                callAjax();
    		            },
    		            error:function(XMLHttpRequest, textStatus, errorThrown){
    		                //access-token刷新失败(可能refresh-token过期或者账号密码改了),重定向到登录页面
    		                yjClient.redirectToLoginPage();
    		            }
    		        });
    		        return;
				}
				if (err.code=="tm.err.foil.tokenNotProvided"){
					yjClient.redirectToLoginPage();
					return;
				}
    			if (options.error){
    				options.error(err);
    			}
    			else{
    				yjClient.showDialog(yjDD["Failed!"],msg);
    			}
    		}
    	});
    }
	callAjax();
}