作用:每次请求不能重复提交,防止黑客通过URL请求窃取数据
原理:每次请求都携带 随机token(该token在后台产生), 携带的token与后台的token进行对比,若相同,表示请求合法,否则请求不合法。
- token算法:
public static String getRandomString(int length) { // length 字符串长度
StringBuffer buffer = new StringBuffer(“0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”);
StringBuffer sb = new StringBuffer();
SecureRandom r = new SecureRandom();
int range = buffer.length();
for (int i = 0; i < length; i++) {
sb.append(buffer.charAt(r.nextInt(range)));
}
return sb.toString();
}
2.token存在何处:
后端:每次生成新的token,存放在 session中(用于token对比), response头消息也存放个token(用于传给前端)
public static void setSessionToken(HttpServletRequest req,HttpServletResponse resp) {
String csrfToken = getRandomString(30);
req.getSession().setAttribute(AJAX_CSRF_TOKEN, csrfToken); //token放入session中
resp.setHeader(“Ajax-Param-Random”,csrfToken); //token放入response头消息中
}
前端: 获取的token存放在 request头消息(ajax请求)中 或者 Form表单中(form表单请求)
ajax形式:
var ajaxCsrfToken = “${sessionScope.token}”; // 页面初始化的token
$.ajaxSetup({
type: “post”,
global: false,
beforeSend: function (xhr) {
xhr.setRequestHeader(‘token’, ajaxCsrfToken); //提交请求携带的token
},
complete: function (xhr) {
if(xhr.getResponseHeader(“Ajax-Param-Random”) != null) {
ajaxCsrfToken = xhr.getResponseHeader(“Ajax-Param-Random”); //提交消息结束后,本窗口的token刷新
var curName = window;
while(curName.parent && curName.parent.name != curName.name) { //多个父窗口的token刷新
curName.parent.ajaxCsrfToken = xhr.getResponseHeader(“Ajax-Param-Random”);
curName = curName.parent;
}
}
}
});
form表单形式:
a. 跳转页面超链接改为post形式:
function postFancybox(url, params, options) {
options = !!options ? options : {};
var $a = $("
var $form = $("<form></form>").attr({"method": "post", "action": url, "target": iframeName});
params && $.each(params, function (key, value) {
var $input = $("<input type='hidden'/>");
$input.attr("name", key).val(value);
$form.append($input);
});
var $input = $("<input type='hidden'/>");
$input.attr("name", "csrfToken").val(ajaxCsrfToken);
$form.append($input);
$form.appendTo('body').submit().remove();
}
}, options)).trigger("click").remove();
refreshToken();
}
b. 导出的post形式
function exportExl(url, params, fileName) {
var inputs = “”;
for(var key in params) {
var param_value = params[key];
inputs += “”;
}
inputs += “”;
inputs += “”;
var form_str = “” + inputs + “”;
$(form_str).appendTo(‘body’).submit().remove();
refreshToken();
}
注:凡是表单提交,都要进行token刷新。 (向后台获取token需要带当前表单提交的token,保证token刷新也不能重复提交) 问题:表单提交的token为何还能用,后台token难道不更新吗?----采用缓存,见步骤 4
function refreshToken() {
$.ajax({
url : “/token_refresh.action”,
type: “post”,
beforeSend: function (xhr) {
xhr.setRequestHeader(‘token’,ajaxCsrfToken);
},
complete: function (xhr) {
if(xhr.getResponseHeader(“Ajax-Param-Random”) != null) {
ajaxCsrfToken = xhr.getResponseHeader(“Ajax-Param-Random”);
var curName = window;
while(curName.parent && curName.parent.name != curName.name) {
curName.parent.ajaxCsrfToken = xhr.getResponseHeader(“Ajax-Param-Random”);
curName = curName.parent;
}
}
}
});
}
- token如何比较
前端传的token在request头消息中或者form表单中。前端传的token与后端里的session的token进行比较。若相同则合法,否则非法
public static synchronized boolean sessionEqualsAjax(HttpServletRequest req,HttpServletResponse resp){
String sessionCsrfToken = String.valueOf(req.getSession().getAttribute(SessionCookie.AJAX_CSRF_TOKEN)); //获取session里的token
String formCsrfToken = req.getParameter(FORM_CSRF_TOKEN); //获取form表单的token
String headerToken = req.getHeader(SessionCookie.AJAX_CSRF_TOKEN); //获取request头消息里的token
String requestToken = StringUtils.isNotBlank(headerToken)?headerToken:formCsrfToken; //设置前端传的token
String tokenCacheValue;
try {
tokenCacheValue = ajaxTokenCache.get(requestToken, () -> { //前端传的token是否缓存中存在
return “0”;
});
} catch (ExecutionException e) {
tokenCacheValue = “0”;
}
if (“1”.equals(tokenCacheValue)) { //前端传的token是否缓存中存在,若存在,不用比较直接通过(该token可能不是最新的token,但缓存中存在)
resp.setHeader(“Ajax-Param-Random”,sessionCsrfToken); //将session里的token更新到response头消息中
ajaxTokenCache.put(sessionCsrfToken,“1”); //将session里的token更新到缓存中
return true;
} else if (sessionCsrfToken.equalsIgnoreCase(requestToken)) { //前端传的token在缓存中不存在,进行token比较
ajaxTokenCache.put(requestToken,“1”); //将当前请求的token更新到缓存中
setSessionToken(req,resp); //生成新的token
return true;
}
return false;
}
- 何时生成新的token。
登录成功后生成新的token
每次请求比较token成功后,再生成新的token(具体细节见步骤 3)
- 如何解决异步请求同时发消息(token值相同问题)
建个缓存,token不立即失效,在3秒之内提交的token可以相同
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
public static Cache<String, String> ajaxTokenCache = CacheBuilder.newBuilder()
.expireAfterAccess(tokenCacheTimeOut, TimeUnit.SECONDS)
.maximumSize(tokenMaximumSize)
.build();
(具体何时更新缓存,细节见 步骤 3)