ITPub博客

首页 > 数据库 > 数据库开发技术 > 互联网金融系统——交易防重设计实战

互联网金融系统——交易防重设计实战

原创 数据库开发技术 作者:MavenTalk 时间:2018-05-24 06:23:25 0 删除 编辑

为什么需要防范重复提交呢?举个最直接的栗子:你在商城里买了7888元的iphone x,付款后页面卡顿导致你重复点击了付款按钮,这时候如果后端不加重复交易验证的话,相当于付款15766元买了Iphone x手机,划算吧?

不单是互金系统交易时会生产此问题,凡涉及表单提交都会遇到,这里以某互金系统为例说明交易防重的过程设计。下图是交易防重设计的示图:

这个过程相信大家都不陌生,生活中随处可见。开封菜的甜品站,先付款,再给小票,拿着小票到取餐口拿甜品,交易完成后,小票撕毁。这就是一个典型的防止重复取餐的例子。

回到上图,来深入了解一下这个过程:

  • 1、在进入到需要防重交易的表单页面之前,请求后端生成token的服务,生成token并存储在后端,与该用户的请求绑定,便于后期在交易验证时与之比对,token返回到交易页面。

  • 2、携带token提交表单,在进入真正交易之前,做token验证(比如使用AOP),如果存在,则token正常,比对成功后销毁进入正常的交易功能。如果不存在,则证明token已经被销毁,为重复提交请求。

以上步骤可以看出token的关键性,若token获取失败,那么交易将无法完成,所以需要保证token服务的高可用性。

以上过程针对一个交易是完全没有问题的,但若涉及两个以上的关键交易提交时,就会出现后请求的交易获取的token替换首次交易获取的token,那么在首次交易提交时,会出现token找不到的情况,导致交易失败。由此引出另外两个关键的问题点:

token的数量以及token的销毁机制。 数量决定了能同时发起交易的数量,所以token的数量最好能够覆盖所有关键交易同时发起来的数量。token的销毁决定了使用token的正常顺序。

基于上面流程,我们再改造一下生成token的模块。

关键示例代码:

  1. //TOKEN对象

  2. @Data

  3. public class TokenVO implements Serializable {

  4.    /**

  5.     * serialVersionUID

  6.     *

  7.     * @since JDK 1.7

  8.     */

  9.    private static final long serialVersionUID = 1L;

  10.    /**

  11.     * FORM_TOKEN_ID:表单TokenId.

  12.     *

  13.     * @since JDK 1.7

  14.     */

  15.    public static final String FORM_TOKEN_ID = "_form_token_id";

  16.    private String tokenId;

  17.    private Date tokenCreateTime;

  18.    public TokenVO(String tokenId) {

  19.        this.tokenId = tokenId;

  20.        this.tokenCreateTime = new Date();

  21.    }

  22.    public String getTokenId() {

  23.        return tokenId;

  24.    }

  25.    public void setTokenId(String tokenId) {

  26.        this.tokenId = tokenId;

  27.    }

  28.    /**

  29.     * getTokenCreateTime:(获得token创建时间). <br/>

  30.     *

  31.     * @author st-gdq4556

  32.     * @return Date

  33.     * @since JDK 1.7

  34.     */

  35.    public Date getTokenCreateTime() {

  36.        if (tokenCreateTime == null) {

  37.            return null;

  38.        }

  39.        return (Date) tokenCreateTime.clone();

  40.    }

  41.    /**

  42.     * setTokenCreateTime:(设置token创建时间). <br/>

  43.     *

  44.     * @author st-gdq4556

  45.     * @param tokenCreateTime

  46.     *            token创建时间

  47.     * @since JDK 1.7

  48.     */

  49.    public void setTokenCreateTime(Date tokenCreateTime) {

  50.        if (tokenCreateTime == null) {

  51.            this.tokenCreateTime = null;

  52.        } else {

  53.            this.tokenCreateTime = (Date) tokenCreateTime.clone();

  54.        }

  55.    }

  56. }

生成token方法

  1.    /**

  2.     * newFormToken:生成一个新的token,如果目前token个数大于设定的最大token数则先删除最早的一个token. <br/>

  3.     * 新token用UUIDUtil.generate16UUID()生成Token.<br/>

  4.     *

  5.     * @author guooo

  6.     * @param request

  7.     *            请求对象

  8.     * @return TokenVO

  9.     * @since JDK 1.7

  10.     */

  11.    public TokenVO newFormToken(HttpServletRequest request) {

  12.        //每次都新生成一个token,放入队列里

  13.        TokenVO TokenVO = new TokenVO(UUIDUtil.generate16UUID());

  14.        Map<String, TokenVO> formTokens = getFormTokens(request);

  15.        synchronized (formTokens) {

  16.            // 如果目前token个数大于等于最大token数,那么删除最老的token,添加新token。

  17.            if (formTokens.size() > maxTokenNum) {

  18.                removeOldestToken(request);

  19.            }

  20.            formTokens.put(TokenVO.getTokenId(), TokenVO);

  21.        }

  22.        return TokenVO;

  23.    }

  24.    /**

  25.     * getTokens:获得目前session中的Token列表. <br/>

  26.     *

  27.     * @author guooo

  28.     * @param request

  29.     *            请求对象

  30.     * @return 返回的Map中以token的token为键,Form对象为值.

  31.     * @since JDK 1.7

  32.     */

  33.    @SuppressWarnings("unchecked")

  34.    protected Map<String, TokenVO> getFormTokens(HttpServletRequest request) {

  35.        Map<String, TokenVO> tokensInSession = null;

  36.        HttpSession session = request.getSession();

  37.        synchronized (session) {

  38.            tokensInSession = (Map<String, TokenVO>) session.getAttribute(SESSION_KEY_OF_TOKENS);

  39.            if (tokensInSession == null) {

  40.                tokensInSession = new HashMap<String, TokenVO>();

  41.                session.setAttribute(SESSION_KEY_OF_TOKENS, tokensInSession);

  42.            }

  43.        }

  44.        return tokensInSession;

  45.    }

  46.     /**

  47.     * removeOldestForm:删除最老的token. <br/>

  48.     *

  49.     * @author guooo

  50.     * @param request

  51.     *            请求对象

  52.     * @since JDK 1.7

  53.     */

  54.    protected void removeOldestToken(HttpServletRequest request) {

  55.        List<TokenVO> tokens = new ArrayList<TokenVO>(getFormTokens(request).values());

  56.        if (!tokens.isEmpty()) {

  57.            TokenVO oldestToken = tokens.get(0);

  58.            for (TokenVO TokenVO : tokens) {

  59.                if (TokenVO.getTokenCreateTime().before(oldestToken.getTokenCreateTime())) {

  60.                    oldestToken = TokenVO;

  61.                }

  62.            }

  63.            destroyFormToken(request, oldestToken.getTokenId());

  64.        }

  65.    }

交易校验,主要由拦截器完成。

  1. public class TradeTokenInterceptor extends HandlerInterceptorAdapter {

  2.    private static Logger logger = LoggerFactory.getLogger(TradeTokenInterceptor.class);

  3.    private TradeTokenService tokenMgr;

  4.    @Override

  5.    public boolean preHandle(HttpServletRequest request,

  6.            HttpServletResponse response, Object handler) throws Exception {

  7.        String tokenId = request.getParameter(TokenVO.FORM_TOKEN_ID);

  8.        if (tokenId != null) {

  9.            if (tokenMgr.hasFormToken(request, tokenId)) {

  10.                //token正常,比对后立即销毁

  11.                tokenMgr.destroyFormToken(request, tokenId);

  12.            } else {

  13.                //token多次提交,异常处理

  14.                RestAPIResult<Object> result = new RestAPIResult<Object>();

  15.                result.setRespCode(500);

  16.                result.setRespMsg("请求已受理,请勿重复提交");

  17.                response.getWriter().write(JSON.toJSONString(result));

  18.                return false;

  19.            }

  20.        }

  21.        return true;

  22.    }

  23.    @Autowired

  24.    public void setFormTokenManage(TradeTokenService formTokenManage) {

  25.        this.tokenMgr = formTokenManage;

  26.    }

  27. }

一般的解决方案是在前端由JS控制提交表单按钮,提交后置灰,禁止第二次提交。但此方法也只针对小白用户有效,防范机制也不是很彻底,比如直接调用请求而非通过页面表单进行,比如JS校验代码清除等,可以绕过JS的置灰功能进行二次提交。

采用前端JS置灰防止重复提交请求,再加上后端token验证,可以更有效的防止关键交易的重复提交。


扩展阅读:

歪脖贰点零
关注程序员个人成长

原文作者:MavenTalk;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2016-07-13

  • 博文量
    461
  • 访问量
    189225