`
hbxflihua
  • 浏览: 683614 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

基于Redis的多步令牌操作防绕过中间步骤

阅读更多

        多步操作在日常生活和工作中很常见,比如孩子出生之前先要办理《准生证》,出生以后要办理《出生医学证明》,然后拿着《户口簿》和《出生医学证明》给孩子上户口。软件领域的多步操作事件驱动源于工作和生活,并将工作或生活场景搬到线上。线下操作通过人工核验来确保中间环节不被落下,而在软件领域,我们可以基于状态位、工作流或者工作令牌等防止绕过中间步骤。

 

        我们先简单说说两个实际的软件应用场景:忘记密码和更换手机号码,两个场景中手机号码为登录账号。       

        忘记密码,忘记密码分为两步操作:

                第一步,输入手机号获取短信验证码并对验证码做校验;

                第二步,对该账号(手机号)设置新密码和确认密码;

        在确认是本人操作后,第二步重置账号密码。逻辑上看似没问题吧?实际上,如果设计不严谨,很容易饶过第一步,直接进入第二步进行密码重置。

        更换手机号,更换手机号也分为两步操作(前置条件:已登录):

                第一步,获取老手机号短信验证码并校验;

                第二步,获取新手机号短信验证码并校验;

        两步操作貌似也比较严谨,但是如果第一步和第二步没有强制关联,仍然可以绕过第一步,直接进入第二步成功更换手机号。

        试想一下,如果多步操作没有严谨的上下步操作逻辑校验,系统看上去是多麽的不堪一击。

 

        多步操作在软件领域比比皆是,处理方法也多种多样。本文将通过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使用令牌桶算法实现流量控制

    系统在运行过程中,如遇上某些活动,访问的人数会在一瞬间内爆增,导致服务器瞬间压力飙升,使系统超...本文介绍php基于redis,使用令牌桶算法,实现访问流量的控制,提供完整算法说明及演示实例,方便大家学习使用。

    Redis思维导图分布式缓存-基于Redis集群解决单机Redis存在的问题

    分布式缓存-基于Redis集群解决单机Redis存在的问题。分布式缓存-基于Redis集群解决单机Redis存在的问题。分布式缓存-基于Redis集群解决单机Redis存在的问题。分布式缓存-基于Redis集群解决单机Redis存在的问题。...

    Redis开发基于redis实现高并发异步秒杀点评项目.zip

    Redis开发基于redis实现高并发异步秒杀点评项目.zipRedis开发基于redis实现高并发异步秒杀点评项目.zipRedis开发基于redis实现高并发异步秒杀点评项目.zipRedis开发基于redis实现高并发异步秒杀点评项目.zipRedis...

    java基于Redis实现排行榜功能源码

    在"java基于Redis Zset实现排行榜功能"的项目中,我们通常会包括以下几个关键组件: 1. **数据模型**:定义一个UserRank类,包含用户ID(uid)和得分(score)。这将是与Redis交互的基本单位。 2. **Redis连接**:...

    微服务SpringBoot整合Redis基于Redis的Stream消息队列实现异步秒杀下单

    【微服务SpringBoot整合Redis基于Redis的Stream消息队列实现异步秒杀下单】这篇文章主要讲解了如何在微服务架构中使用SpringBoot整合Redis来构建一个基于Redis Stream的消息队列,以此来实现实时、高效的异步秒杀...

    基于redis构建社交平台技术分析+编程技术

    使用redis构建简单的社交网站,基于redis构建社交平台技术分析+编程技术;使用redis构建简单的社交网站,基于redis构建社交平台技术分析+编程技术;使用redis构建简单的社交网站,基于redis构建社交平台技术分析+...

    Mybatis-plus基于redis实现二级缓存过程解析

    Mybatis-plus基于Redis实现二级缓存过程解析 Mybatis-plus是一款基于Java语言的持久层框架,旨在简化数据库交互操作。然而,在高并发、高性能的应用场景中,数据库的查询操作可能会成为性能瓶颈。为了解决这个问题...

    Java基于redis实现分布式锁代码实例

    Java基于Redis实现分布式锁代码实例 分布式锁的必要性 在多线程环境中,资源竞争是一个常见的问题。例如,在一个简单的用户操作中,一个线程修改用户状态,首先在内存中读取用户状态,然后在内存中进行修改,然后...

    100讲带你实战基于Redis的高并发预约抢购系统.zip

    《基于Redis的高并发预约抢购系统实战解析》 Redis,作为一款高性能的键值存储系统,因其内存存储、快速响应、丰富的数据结构以及强大的持久化能力,在高并发场景下得到了广泛应用,尤其在预约抢购系统中发挥着关键...

    redis令牌机制实现秒杀系统

    秒杀是电商系统非常常见的...本教程采用:redis中list类型达到令牌机制完成秒杀。用户抢redis中的令牌,抢到 令牌的用户才能进行支付,支付成功之后可以生成订单,如果一定时间之内没有支 付那么就由定时任务来归还令牌

    基于Redis方式实现分布式锁

    ### 基于Redis方式实现分布式锁 #### 分布式锁概述 分布式锁是一种常见的分布式系统协调机制,用于控制分布式环境下的多个进程或线程之间的访问顺序,防止多个客户端同时修改共享资源,从而保证数据的一致性和完整...

    基于redis的sso接口文档和教程

    **基于Redis的SSO接口文档和教程** 单点登录(Single Sign-On,简称SSO)是一种用户在多个应用系统中只需登录一次,就能在其他所有系统中自由切换并保持登录状态的技术。它简化了用户的登录流程,提高了用户体验。...

    python基于redis的限流器.zip

    例如,为了实现一个简单的基于Redis的令牌桶限流器,你可能需要以下步骤: 1. 连接Redis服务器:创建`redis.Redis`实例并设置连接参数。 2. 初始化桶:设置初始令牌数量和填充速率。 3. 获取令牌:每次请求时,使用...

    论文研究-基于Redis和Mysql的存储系统的设计与实现 .pdf

    基于Redis和Mysql的存储系统的设计与实现,范东媛,钮心忻,本文基于Redis和Mysql数据库设计并且实现了在线学习平台的数据存储系统。利用Mysql的持久化存储和Redis的高速读写设计出具有存储数据庞�

    Go-利用Redis和Golang实现分布式令牌桶算法

    结合Redis,一个开源的、基于网络的、键值存储系统,可以构建分布式环境下的令牌桶算法,确保算法的可扩展性和高可用性。 **描述分析:** "令牌桶算法对速率限制和网络拥塞控制非常有用" 速率限制是网络管理的关键...

    java-基于redis限流系统

    当我们谈论“java-基于redis限流系统”时,我们实际上是指利用Redis这个内存数据存储作为工具来实现Java应用的流量控制。Redis因其高效、灵活的特性,常被用于构建限流系统。下面将详细阐述这一主题。 首先,限流的...

    基于redis的缓存框架

    基于Redis的缓存框架是利用Redis的高性能和丰富的数据结构来实现应用程序的缓存功能。本篇文章将深入探讨如何构建一个基于Redis的缓存框架,并与Spring Cache进行对比。 首先,让我们了解一下Redis。Redis(Remote ...

    基于redis单点登录实例

    基于redis单点登录解决方案。使用redis的key时效性代替session对多个相同进行统一管理。代码包括3个项目master、projectServlet、projectSpring,其中master是登录主项目,其他两个是次项目。只要在master登录就可以...

    基于redis限流系统.zip

    基于Redis的限流系统就是利用Redis的高效特性和数据结构,结合特定的限流算法,来限制系统的输入或输出流量。在这个“基于redis限流系统.zip”压缩包中,我们可以预见到包含了一个使用Redis和Lua脚本实现的限流解决...

    基于redis限流系统

    **基于Redis的限流系统详解** 在高并发的互联网服务中,限流是一种常见的流量控制策略,用于保护系统免受过大的瞬时流量冲击,确保服务的稳定性和可用性。Redis,作为一款高效的键值存储数据库,常被用作限流系统的...

Global site tag (gtag.js) - Google Analytics