多步操作在日常生活和工作中很常见,比如孩子出生之前先要办理《准生证》,出生以后要办理《出生医学证明》,然后拿着《户口簿》和《出生医学证明》给孩子上户口。软件领域的多步操作事件驱动源于工作和生活,并将工作或生活场景搬到线上。线下操作通过人工核验来确保中间环节不被落下,而在软件领域,我们可以基于状态位、工作流或者工作令牌等防止绕过中间步骤。
我们先简单说说两个实际的软件应用场景:忘记密码和更换手机号码,两个场景中手机号码为登录账号。
忘记密码,忘记密码分为两步操作:
第一步,输入手机号获取短信验证码并对验证码做校验;
第二步,对该账号(手机号)设置新密码和确认密码;
在确认是本人操作后,第二步重置账号密码。逻辑上看似没问题吧?实际上,如果设计不严谨,很容易饶过第一步,直接进入第二步进行密码重置。
更换手机号,更换手机号也分为两步操作(前置条件:已登录):
第一步,获取老手机号短信验证码并校验;
第二步,获取新手机号短信验证码并校验;
两步操作貌似也比较严谨,但是如果第一步和第二步没有强制关联,仍然可以绕过第一步,直接进入第二步成功更换手机号。
试想一下,如果多步操作没有严谨的上下步操作逻辑校验,系统看上去是多麽的不堪一击。
多步操作在软件领域比比皆是,处理方法也多种多样。本文将通过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存在的问题。...
Redis开发基于redis实现高并发异步秒杀点评项目.zipRedis开发基于redis实现高并发异步秒杀点评项目.zipRedis开发基于redis实现高并发异步秒杀点评项目.zipRedis开发基于redis实现高并发异步秒杀点评项目.zipRedis...
在"java基于Redis Zset实现排行榜功能"的项目中,我们通常会包括以下几个关键组件: 1. **数据模型**:定义一个UserRank类,包含用户ID(uid)和得分(score)。这将是与Redis交互的基本单位。 2. **Redis连接**:...
【微服务SpringBoot整合Redis基于Redis的Stream消息队列实现异步秒杀下单】这篇文章主要讲解了如何在微服务架构中使用SpringBoot整合Redis来构建一个基于Redis Stream的消息队列,以此来实现实时、高效的异步秒杀...
使用redis构建简单的社交网站,基于redis构建社交平台技术分析+编程技术;使用redis构建简单的社交网站,基于redis构建社交平台技术分析+编程技术;使用redis构建简单的社交网站,基于redis构建社交平台技术分析+...
Mybatis-plus基于Redis实现二级缓存过程解析 Mybatis-plus是一款基于Java语言的持久层框架,旨在简化数据库交互操作。然而,在高并发、高性能的应用场景中,数据库的查询操作可能会成为性能瓶颈。为了解决这个问题...
Java基于Redis实现分布式锁代码实例 分布式锁的必要性 在多线程环境中,资源竞争是一个常见的问题。例如,在一个简单的用户操作中,一个线程修改用户状态,首先在内存中读取用户状态,然后在内存中进行修改,然后...
《基于Redis的高并发预约抢购系统实战解析》 Redis,作为一款高性能的键值存储系统,因其内存存储、快速响应、丰富的数据结构以及强大的持久化能力,在高并发场景下得到了广泛应用,尤其在预约抢购系统中发挥着关键...
秒杀是电商系统非常常见的...本教程采用:redis中list类型达到令牌机制完成秒杀。用户抢redis中的令牌,抢到 令牌的用户才能进行支付,支付成功之后可以生成订单,如果一定时间之内没有支 付那么就由定时任务来归还令牌
### 基于Redis方式实现分布式锁 #### 分布式锁概述 分布式锁是一种常见的分布式系统协调机制,用于控制分布式环境下的多个进程或线程之间的访问顺序,防止多个客户端同时修改共享资源,从而保证数据的一致性和完整...
**基于Redis的SSO接口文档和教程** 单点登录(Single Sign-On,简称SSO)是一种用户在多个应用系统中只需登录一次,就能在其他所有系统中自由切换并保持登录状态的技术。它简化了用户的登录流程,提高了用户体验。...
例如,为了实现一个简单的基于Redis的令牌桶限流器,你可能需要以下步骤: 1. 连接Redis服务器:创建`redis.Redis`实例并设置连接参数。 2. 初始化桶:设置初始令牌数量和填充速率。 3. 获取令牌:每次请求时,使用...
基于Redis和Mysql的存储系统的设计与实现,范东媛,钮心忻,本文基于Redis和Mysql数据库设计并且实现了在线学习平台的数据存储系统。利用Mysql的持久化存储和Redis的高速读写设计出具有存储数据庞�
结合Redis,一个开源的、基于网络的、键值存储系统,可以构建分布式环境下的令牌桶算法,确保算法的可扩展性和高可用性。 **描述分析:** "令牌桶算法对速率限制和网络拥塞控制非常有用" 速率限制是网络管理的关键...
当我们谈论“java-基于redis限流系统”时,我们实际上是指利用Redis这个内存数据存储作为工具来实现Java应用的流量控制。Redis因其高效、灵活的特性,常被用于构建限流系统。下面将详细阐述这一主题。 首先,限流的...
基于Redis的缓存框架是利用Redis的高性能和丰富的数据结构来实现应用程序的缓存功能。本篇文章将深入探讨如何构建一个基于Redis的缓存框架,并与Spring Cache进行对比。 首先,让我们了解一下Redis。Redis(Remote ...
基于redis单点登录解决方案。使用redis的key时效性代替session对多个相同进行统一管理。代码包括3个项目master、projectServlet、projectSpring,其中master是登录主项目,其他两个是次项目。只要在master登录就可以...
基于Redis的限流系统就是利用Redis的高效特性和数据结构,结合特定的限流算法,来限制系统的输入或输出流量。在这个“基于redis限流系统.zip”压缩包中,我们可以预见到包含了一个使用Redis和Lua脚本实现的限流解决...
**基于Redis的限流系统详解** 在高并发的互联网服务中,限流是一种常见的流量控制策略,用于保护系统免受过大的瞬时流量冲击,确保服务的稳定性和可用性。Redis,作为一款高效的键值存储数据库,常被用作限流系统的...