多步操作在日常生活和工作中很常见,比如孩子出生之前先要办理《准生证》,出生以后要办理《出生医学证明》,然后拿着《户口簿》和《出生医学证明》给孩子上户口。软件领域的多步操作事件驱动源于工作和生活,并将工作或生活场景搬到线上。线下操作通过人工核验来确保中间环节不被落下,而在软件领域,我们可以基于状态位、工作流或者工作令牌等防止绕过中间步骤。
我们先简单说说两个实际的软件应用场景:忘记密码和更换手机号码,两个场景中手机号码为登录账号。
忘记密码,忘记密码分为两步操作:
第一步,输入手机号获取短信验证码并对验证码做校验;
第二步,对该账号(手机号)设置新密码和确认密码;
在确认是本人操作后,第二步重置账号密码。逻辑上看似没问题吧?实际上,如果设计不严谨,很容易饶过第一步,直接进入第二步进行密码重置。
更换手机号,更换手机号也分为两步操作(前置条件:已登录):
第一步,获取老手机号短信验证码并校验;
第二步,获取新手机号短信验证码并校验;
两步操作貌似也比较严谨,但是如果第一步和第二步没有强制关联,仍然可以绕过第一步,直接进入第二步成功更换手机号。
试想一下,如果多步操作没有严谨的上下步操作逻辑校验,系统看上去是多麽的不堪一击。
多步操作在软件领域比比皆是,处理方法也多种多样。本文将通过Redis的多步令牌颁发和验证来防绕过中间步骤。
1、添加多步操作token注解
package com.huatech.common.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 多步操作token * @author lh@erongdu.com * @since 2019年9月3日 * @version 1.0 * */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface StepToken { /** * 如果是Step.HEAD 等同设置publishKey * 如果是Step.TAIL 等同设置validateKey * @return */ String value() default ""; /** * 当前环节 * @return */ Step step() default Step.HEAD; /** * 发布 token key,除最后一步外其他环节必传 * @return */ String publishKey() default ""; /** * 校验token key,除第一步外其他环节必传 * @return */ String validateKey() default ""; }
package com.huatech.common.annotation; /** * 多步操作环节 * @author lh@erongdu.com * @since 2019年9月3日 * @version 1.0 * */ public enum Step { /** * 第一步 */ HEAD, /** * 中间步骤 */ MIDDLE, /** * 最后一步 */ TAIL }
2、添加多步操作颁发和验证token拦截器
package com.huatech.common.interceptor; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import com.alibaba.fastjson.JSONObject; import com.huatech.common.annotation.Step; import com.huatech.common.annotation.StepToken; import com.huatech.common.constant.Constants; /** * 多步操作拦截验证 * @author lh@erongdu.com * @since 2019年9月3日 * @version 1.0 * */ public class StepTokenInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(StepTokenInterceptor.class); @Autowired StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { request.setAttribute("start", System.currentTimeMillis()); if (handler instanceof HandlerMethod) { HandlerMethod method = (HandlerMethod) handler; StepToken stepToken = method.getMethodAnnotation(StepToken.class); if (stepToken == null || Step.HEAD.equals(stepToken.step())) {//不需要校验token return true; } // 校验token Long userId = null;//UserUtil.getSessionUserId(request); String tokenKey = String.format(Constants.KEY_STEP_TOKEN, userId == null ? request.getSession().getId() : userId, StringUtils.isBlank(stepToken.validateKey()) ? stepToken.value() : stepToken.validateKey()); logger.info("validate token, tokenKey:{}", tokenKey); if(!redisTemplate.hasKey(tokenKey)){ Map<String, Object> result = new HashMap<>(); result.put("code", "500"); result.put("msg", "请求超时或重复提交!"); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(JSONObject.toJSON(result)); return false; } redisTemplate.delete(tokenKey); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { long start = Long.valueOf(request.getAttribute("start").toString()); String url = request.getRequestURI(); if (handler instanceof HandlerMethod) { HandlerMethod method = (HandlerMethod) handler; StepToken stepToken = method.getMethodAnnotation(StepToken.class); if (stepToken == null || Step.TAIL.equals(stepToken.step())) {//不需要添加token return; } // 成功返回 添加token,可以替换成response.getStatus()等做验证 String code = response.getHeader(Constants.HEAD_DATA_CODE); if(StringUtils.isBlank(code) || !"200".equals(code)){// 未成功返回,不添加token return; } // 添加token Long userId = null; //UserUtil.getSessionUserId(request); String tokenKey = String.format(Constants.KEY_STEP_TOKEN, userId == null ? request.getSession().getId() : userId, StringUtils.isBlank(stepToken.publishKey()) ? stepToken.value() : stepToken.publishKey()); logger.info("publish token, tokenKey:{}", tokenKey); redisTemplate.boundValueOps(tokenKey).set("1", 60); } logger.info("当前请求接口:{}, 响应时间:{}ms" , url, (System.currentTimeMillis() - start)); } }
3、spring-mvc配置文件中配置拦截器
<mvc:interceptors> <!-- 多步操作验证,防止跳过中间步骤 --> <mvc:interceptor> <mvc:mapping path="/**"/> <bean class="com.huatech.common.interceptor.StepTokenInterceptor"/> </mvc:interceptor> </mvc:interceptors>
4、在Controller多步操作方法中添加@StepToken
/** * 忘记密码第一步,验证账号和验证码 */ @RequestMapping(value = "/api/userInfo/forgetPwdOne.htm", method = RequestMethod.POST) @StepToken(step = Step.HEAD, value = "forgetPwdOne") public void preForgetPwd(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "loginName") String loginName, @RequestParam(value = "vCode") String vCode) { Map<String, Object> result = apiUserService.forgetPwdOne(loginName, vCode); ServletUtils.writeToResponse(response, result); } /** * 忘记密码第二步,设置新密码 */ @RequestMapping(value = "/api/userInfo/forgetPwdTwo.htm", method = RequestMethod.POST) @StepToken(step = Step.TAIL, value = "forgetPwdOne") public void forgetPwd(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "loginName") String loginName, @RequestParam(value = "newPwd") String newPwd, @RequestParam(value = "confirmPwd") String confirmPwd) { Map<String, Object> result = apiUserService.forgetPwdTwo(loginName, newPwd, confirmPwd); ServletUtils.writeToResponse(response, result); }
相关推荐
系统在运行过程中,如遇上某些活动,访问的人数会在一瞬间内爆增,导致服务器瞬间压力飙升,使系统超...本文介绍php基于redis,使用令牌桶算法,实现访问流量的控制,提供完整算法说明及演示实例,方便大家学习使用。
分布式缓存-基于Redis集群解决单机Redis存在的问题。分布式缓存-基于Redis集群解决单机Redis存在的问题。分布式缓存-基于Redis集群解决单机Redis存在的问题。分布式缓存-基于Redis集群解决单机Redis存在的问题。...
100讲带你实战基于Redis的高并发,学习笔记
保证能跑通-基于Redis Zset实现排行榜功能的源码,包含页面,接口,下载运行即可访问,步骤请阅读README.MD文件
100讲带你实战基于Redis的高并发预约抢购系统
基于Redis方式实现分布式锁
秒杀是电商系统非常常见的...本教程采用:redis中list类型达到令牌机制完成秒杀。用户抢redis中的令牌,抢到 令牌的用户才能进行支付,支付成功之后可以生成订单,如果一定时间之内没有支 付那么就由定时任务来归还令牌
基于Redis和Mysql的存储系统的设计与实现,范东媛,钮心忻,本文基于Redis和Mysql数据库设计并且实现了在线学习平台的数据存储系统。利用Mysql的持久化存储和Redis的高速读写设计出具有存储数据庞�
主要介绍了Java基于redis实现分布式锁代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
基于redis实现的分布式session控制,多站点 多服务器均可兼容,使用方法:本地启动redis并配置到webconfig中
基于redis单点登录解决方案。使用redis的key时效性代替session对多个相同进行统一管理。代码包括3个项目master、projectServlet、projectSpring,其中master是登录主项目,其他两个是次项目。只要在master登录就可以...
基于redis实现的单点登录这套方案比SSO CAS来说比较简单,容易上手,主要是依赖每个应用的拦截器和redis实现单点登录。
主要介绍了Mybatis-plus基于redis实现二级缓存过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
分布式Redis原子操作示例,近期项目中遇到分布式项目中多节点大并发操作redis同一个key。此案例利用java调用LUA脚本实现redis操作的原子性。分享出来大家参考。
在应用系统当中,需要确保数据访问所具备的时效性, 将关键性的业务数据全都存储在内存当中。不过如果出现业务 范围持续拓展的情况,只是利用一台机器已然无法实现对全部 关键性业务数据的上传,所以,应该针对...
scrapy-redis, 基于Redis的组件组件 scrapy 基于redis的组件组件。自由软件:MIT许可证文档:https://scrapy-redis.readthedocs.org 。python 版本:2.7,3.4 特性分
SpringBoot基于redis的分布式锁,有word使用文档,根据文档配置即可使用
autocomplete-redis 是基于redis的自动补全,他会自动索引你要自动补全的句子,然后根据你的输入返回包含这个输入的句子。这儿有一个完整的演示实例: http://ohbooklist.com/redis/ ,我们索引了3.7万本书的名字。 ...
php基于Redis实现自增计数,主要使用redis的incr方法,并发执行时保证计数自增唯一。
redismanager redis操作工具