基于redis的多步令牌操作防绕过中间步骤 -凯发k8国际

`
hbxflihua
  • 浏览: 649507 次
  • 性别:
  • 来自: 杭州
博主相关
  • 博客
  • 微博
  • 相册
  • 收藏
  • 社区版块
    • ( 0)
    • ( 0)
    • ( 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 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配置文件中配置拦截器

    	
    		
    		
    		
    			
    			
    		
    		
    	

     

     

     

    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 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 result = apiuserservice.forgetpwdtwo(loginname, newpwd, confirmpwd);
            servletutils.writetoresponse(response, result);
        }

     

     

     

    分享到:
    评论

    相关推荐

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

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

      100讲带你实战基于redis的高并发,学习笔记

      保证能跑通-基于redis zset实现排行榜功能的源码,包含页面,接口,下载运行即可访问,步骤请阅读readme.md文件

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

      基于redis方式实现分布式锁

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

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

      基于redis实现的分布式session控制,多站点 多服务器均可兼容,使用方法:本地启动redis并配置到webconfig中

      基于redis单点登录凯发k8国际娱乐官网入口的解决方案。使用redis的key时效性代替session对多个相同进行统一管理。代码包括3个项目master、projectservlet、projectspring,其中master是登录主项目,其他两个是次项目。只要在master登录就可以...

      基于redis实现的单点登录这套方案比sso cas来说比较简单,容易上手,主要是依赖每个应用的拦截器和redis实现单点登录。

      主要介绍了mybatis-plus基于redis实现二级缓存过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

      主要介绍了java基于redis实现分布式锁代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

      分布式redis原子操作示例,近期项目中遇到分布式项目中多节点大并发操作redis同一个key。此案例利用java调用lua脚本实现redis操作的原子性。分享出来大家参考。

      在应用系统当中,需要确保数据访问所具备的时效性, 将关键性的业务数据全都存储在内存当中。不过如果出现业务 范围持续拓展的情况,只是利用一台机器已然无法实现对全部 关键性业务数据的上传,所以,应该针对...

      scrapy-redis, 基于redis的组件组件 scrapy 基于redis的组件组件。自由软件:mit许可证文档:https://scrapy-redis.readthedocs.org 。python 版本:2.7,3.4 特性分

      springboot基于redis的分布式锁,有word使用文档,根据文档配置即可使用

      autocomplete-redis 是基于redis的自动补全,他会自动索引你要自动补全的句子,然后根据你的输入返回包含这个输入的句子。这儿有一个完整的演示实例: http://ohbooklist.com/redis/ ,我们索引了3.7万本书的名字。 ...

      php基于redis实现自增计数,主要使用redis的incr方法,并发执行时保证计数自增唯一。

      redismanager redis操作工具

    global site tag (gtag.js) - google analytics
    网站地图