作者:许瑜钊
实现思路
在springmvc配置文件中加入拦截器的配置,当转到页面的请求到来时,生成token的名字和token值,一份放到redis缓存中,一份放传给页面表单的隐藏域。
当表单请求提交时,拦截器得到参数中的tokenName和token,然后到缓存中去取token值,如果能匹配上,请求就通过,不能匹配上就不通过。这里的tokenName生成时也是随机的,每次请求都不一样。而从缓存中取token值时,会立即将其删除(删与读是原子的,无线程安全问题)。
实现方式
1.新建注解:
import
java.lang.annotation.ElementType;
import
java.lang.annotation.Retention;
import
java.lang.annotation.RetentionPolicy;
import
java.lang.annotation.Target;
/**
* <p>
* 防止重复提交注解,用于方法上<br/>
* 在新建页面方法上,设置needSaveToken()为true,此时拦截器会在Session中保存一个token,
* 同时需要在新建的页面中添加
* <input type="hidden" id="ffzx.tokens" name="ffzx.tokens" value="${(token) !}">
* <br/>
* 保存方法需要验证重复提交的,设置needRemoveToken为true
* 此时会在拦截器中验证是否重复提交
* </p>
* @author: 张明
* @date: 2016-7-18上午11:14:02
*
*/
@Target
(ElementType.METHOD)
@Retention
(RetentionPolicy.RUNTIME)
public
@interface
PreventDuplicateSubmission {
boolean
needSaveToken()
default
false
;
boolean
needRemoveToken()
default
false
;
}
|
2.Token工具类
import
javax.servlet.http.HttpServletRequest;
import
org.apache.log4j.Logger;
import
java.util.Map;
/**
* @author zhangming
* @date 2016/7/18
*/
public
class
TokenUtils {
private
static
final
Logger LOG = Logger.getLogger(TokenUtils.
class
);
private
static
RedisUtil redisUtil;
public
static
RedisUtil getRedisCacheClient() {
return
redisUtil;
}
public
static
void
setRedisCacheClient(RedisUtil redisUtil) {
TokenUtils.redisUtil = redisUtil;
}
/**
* 保存token值的默认命名空间
*/
public
static
final
String TOKEN_NAMESPACE =
"ffzx.tokens"
;
/**
* 持有token名称的字段名
*/
public
static
final
String TOKEN_NAME_FIELD =
"token"
;
/**
* 使用UUID字串作为token名字保存token
*
* @param request
* @return token
*/
public
static
String setToken(HttpServletRequest request) {
return
setToken(request, generateGUID());
}
/**
* 使用给定的字串作为token名字保存token
*
* @param request
* @param tokenName
* @return token
*/
private
static
String setToken(HttpServletRequest request, String tokenName) {
String token = tokenName;
setCacheToken(request, tokenName, token);
return
token;
}
/**
* 保存一个给定名字和值的token
*
* @param request
* @param tokenName
* @param token
*/
private
static
void
setCacheToken(HttpServletRequest request, String tokenName, String token) {
try
{
String tokenName0 = buildTokenCacheAttributeName(tokenName);
redisUtil.set(tokenName0, token);
request.setAttribute(TOKEN_NAME_FIELD, tokenName);
request.setAttribute(tokenName, token);
}
catch
(IllegalStateException e) {
String msg =
"Error creating HttpSession due response is commited to client. You can use the CreateSessionInterceptor or create the HttpSession from your action before the result is rendered to the client: "
+ e.getMessage();
LOG.error(msg, e);
throw
new
IllegalArgumentException(msg);
}
}
/**
* 构建一个基于token名字的带有命名空间为前缀的token名字
*
* @param tokenName
* @return the name space prefixed session token name
*/
public
static
String buildTokenCacheAttributeName(String tokenName) {
return
TOKEN_NAMESPACE +
":"
+ tokenName;
}
/**
* 从请求域中获取给定token名字的token值
*
* @param tokenName
* @return the token String or null, if the token could not be found
*/
public
static
String getToken(HttpServletRequest request, String tokenName) {
if
(tokenName ==
null
) {
return
null
;
}
Map<?, ?> params = request.getParameterMap();
String[] tokens = (String[]) params.get(tokenName);
String token;
if
((tokens ==
null
) || (tokens.length <
1
)) {
LOG.warn(
"Could not find token mapped to token name "
+ tokenName);
return
null
;
}
token = tokens[
0
];
return
token;
}
/**
* 从请求参数中获取token名字
*
* @return the token name found in the params, or null if it could not be
* found
*/
public
static
String getTokenName(HttpServletRequest request) {
Map<?, ?> params = request.getParameterMap();
if
(!params.containsKey(TOKEN_NAMESPACE)) {
LOG.warn(
"Could not find token name in params."
);
return
null
;
}
String[] tokenNames = (String[]) params.get(TOKEN_NAMESPACE);
String tokenName;
if
((tokenNames ==
null
) || (tokenNames.length <
1
)) {
LOG.warn(
"Got a null or empty token name."
);
return
null
;
}
tokenName = tokenNames[
0
];
return
tokenName;
}
/**
* 验证当前请求参数中的token是否合法,如果合法的token出现就会删除它,它不会再次成功合法的token
*
* @return 验证结果
*/
public
static
boolean
validToken(HttpServletRequest request) {
String tokenName = getTokenName(request);
if
(tokenName ==
null
) {
LOG.warn(
"no token name found -> Invalid token "
);
return
false
;
}
String tokenCacheName = buildTokenCacheAttributeName(tokenName);
String cacheToken = (String) redisUtil.get(tokenCacheName);
if
(!tokenName.equals(cacheToken)) {
LOG.warn(
"invalid.token Form token "
+ tokenName +
" does not match the session token "
+ cacheToken +
"."
);
return
false
;
}
redisUtil.remove(tokenCacheName);
return
true
;
}
public
static
String generateGUID() {
return
UUIDGenerator.getUUID();
}
}
|
3. 新建拦截器:
import
java.lang.reflect.Method;
import
javax.annotation.Resource;
import
javax.servlet.http.HttpServletRequest;
import
javax.servlet.http.HttpServletResponse;
import
org.apache.log4j.Logger;
import
org.springframework.web.method.HandlerMethod;
import
org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import
com.ffzx.commerce.framework.annotation.PreventDuplicateSubmission;
import
com.ffzx.commerce.framework.utils.RedisUtil;
import
com.ffzx.commerce.framework.utils.TokenUtils;
/**
* @author zhangming
* @date 2016/7/18
*/
public
class
PreventDuplicateSubmissionInterceptor
extends
HandlerInterceptorAdapter {
private
static
final
Logger LOG = Logger.getLogger(PreventDuplicateSubmissionInterceptor.
class
);
@Resource
private
RedisUtil redisUtil;
@Override
public
boolean
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws
Exception {
if
(TokenUtils.getRedisCacheClient() ==
null
) {
TokenUtils.setRedisCacheClient(redisUtil);
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
PreventDuplicateSubmission annotation = method.getAnnotation(PreventDuplicateSubmission.
class
);
if
(annotation !=
null
) {
boolean
needSaveSession = annotation.needSaveToken();
if
(needSaveSession) {
String tokenString = TokenUtils.setToken(request);
LOG.debug(
"本次请求的token已保存==>>"
+ tokenString);
return
true
;
}
boolean
needRemoveSession = annotation.needRemoveToken();
if
(needRemoveSession) {
if
(!TokenUtils.validToken(request)) {
return
false
;
}
}
}
return
true
;
}
}
|
4. 在Spring中配置:
<
mvc:interceptors
>
<!-- 这里是拦截所有的请求, 用于解决重复提交的问题 -->
<
mvc:interceptor
>
<
mvc:mapping
path
=
"/**"
/>
<
bean
class
=
"com.ffzx.commerce.framework.interceptor.PreventDuplicateSubmissionInterceptor"
></
bean
>
</
mvc:interceptor
>
</
mvc:interceptors
>
|
5. 在相关方法中加入注解:
/**
* 关于这个方法的用法是:在需要生成token的controller上增加@PreventDuplicateSubmission(needSaveToken=true),
* 而在需要检查重复提交的controller上添加@PreventDuplicateSubmission(needRemoveToken =true)就可以了
*/
@RequestMapping
(value =
"/form"
)
@PreventDuplicateSubmission
(needSaveToken =
true
)
public
String form(String id, ModelMap map){
@RequestMapping
(
"/save"
)
@ResponseBody
@PreventDuplicateSubmission
(needRemoveToken =
true
)
public
ResultVo save(
@Valid
CustomerResource customerResource, BindingResult result) {
|
6.在新建页面中加入:
<
input
type
=
"hidden"
id
=
"ffzx.tokens"
name
=
"ffzx.tokens"
value
=
"${(token) !}"
>
|