欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-07 10:02:12
...

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

 

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

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

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

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

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

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

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

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

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

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

 

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

 

 

 

相关标签: redis