`
hbxflihua
  • 浏览: 658502 次
  • 性别: 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);
    }

 

 

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics