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

标签: redis 中间 | 发表时间:2019-09-04 09:32 | 作者:hbxflihua
出处:https://www.iteye.com

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

 

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

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

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

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

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

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

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

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

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

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

 

        多步操作在软件领域比比皆是,处理方法也多种多样。本文将通过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 [email protected]
 * @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 [email protected]
 * @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 [email protected]
 * @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);
    }

 

 

 



已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [redis 中间] 推荐:

Redis分布式中间件TwemProxy

- - 企业架构 - ITeye博客
twemproxy,也叫nutcraker. 是一个twtter开源的一个redis和memcache代理服务器. redis作为一个高效的缓存服务器,非常具有应用价值. 但是当使用比较多的时候,就希望可以通过某种方式 统一进行管理. 避免每个应用每个客户端管理连接的松散性. 搜索了不少的开源代理项目,知乎实现的python分片客户端.

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

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

Redis 负载监控——redis-monitor

- - ITeye资讯频道
redis-monitor是一个Web可视化的 redis 监控程序. 使用 Flask 来开发的,代码结构非常简单,适合移植到公司内网使用. redis 服务器信息,包括 redis 版本、上线时间、 os 系统信息等等. 实时的消息处理信息,例如处理 command 数量、连接总数量等. 内存占用、 cpu 消耗实时动态图表.

Redis 起步

- - 博客园_首页
Rdis和JQuery一样是纯粹为应用而产生的,这里记录的是在CentOS 5.7上学习入门文章:. Redis是一个key-value存储系统. 和Memcached类似,但是解决了断电后数据完全丢失的情况,而且她支持更多无化的value类型,除了和string外,还支持lists(链表)、sets(集合)和zsets(有序集合)几种数据类型.

redis 配置

- - 谁主沉浮
# 当配置中需要配置内存大小时,可以使用 1k, 5GB, 4M 等类似的格式,其转换方式如下(不区分大小写). # 内存配置大小写是一样的.比如 1gb 1Gb 1GB 1gB. # daemonize no 默认情况下,redis不是在后台运行的,如果需要在后台运行,把该项的值更改为yes. # 当redis在后台运行的时候,Redis默认会把pid文件放在/var/run/redis.pid,你可以配置到其他地址.

Cassandra代替Redis?

- - Tim[后端技术]
最近用Cassandra的又逐渐多了,除了之前的360案例,在月初的QCon Shanghai 2013 篱笆网也介绍了其使用案例. 而这篇 百万用户时尚分享网站feed系统扩展实践文章则提到了Fashiolista和Instagram从Redis迁移到Cassandra的案例. 考虑到到目前仍然有不少网友在讨论Redis的用法问题,Redis是一个数据库、内存、还是Key value store?以及Redis和memcache在实际场景的抉择问题,因此简单谈下相关区别.

redis 部署

- - CSDN博客云计算推荐文章
一、单机部署 tar xvf redis-2.6.16.tar.gz cd redis-2.6.16 make make PREFIX=/usr/local/redis install  #指定安装目录为/usr/local/redis,默认安装安装到/usr/local/bin. # chkconfig: 2345 80 10       #添加redhat系列操作系统平台,开机启动需求项(运行级别,开机时服务启动顺序、关机时服务关闭顺序) # description:  Starts, stops redis server.

nagios 监控redis

- - C1G军火库
下载check_redis.pl. OK: REDIS 2.6.12 on 192.168.0.130:6379 has 1 databases (db0) with 49801 keys, up 3 days 14 hours - connected_clients is 1, blocked_clients is 0 | connected_clients=1 blocked_clients=0.

转 redis vs memcached

- - 数据库 - ITeye博客
传统MySQL+ Memcached架构遇到的问题.   实际MySQL是适合进行海量数据存储的,通过Memcached将热点数据加载到cache,加速访问,很多公司都曾经使用过这样的架构,但随着业务数据量的不断增加,和访问量的持续增长,我们遇到了很多问题:.   1.MySQL需要不断进行拆库拆表,Memcached也需不断跟着扩容,扩容和维护工作占据大量开发时间.

Redis优化

- - 数据库 - ITeye博客
键名:尽量精简,但是也不能单纯为了节约空间而使用不易理解的键名. 键值:对于键值的数量固定的话可以使用0和1这样的数字来表示,(例如:male/female、right/wrong). 当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能,不过一般都要持久化比较安全,而且是快照和aof同时使用比较安全.