可能我比较偏业务,所以离我们的大佬@dr0op 的行业和涉及面其实不是特别大。但是八月中旬的时候经历了一场有惊无险的事故之后,我意识到安全是保证业务流程能够work的基本条件。现在细细道来事情的经过吧:
公司的项目大都是SpringMVC,有部分的是SpringBoot和SpringCloud(服务不只一个,领导有很强的想法转Spring boot),而作为后台我们服务的多为APP和H5,所以数据格式和工作模式多为前后分离,而用户发起会话的安全保障并不能使用Session的方案,而是Token。我们在拦截器配置来源,将一些来源不是服务器内部的请求,都要统一经过一个Passport的鉴权服务。
服务器在接受到请求的时候,会根据请求传来的唯一一个类用户ID的东西,经过我们架构设计的鉴权服务里的算法,生成一个Token,然后这个东西再Base64编码一下返回给客户端。那么下次请求的时候就得带着这个Token过来。只要Token不为空,服务端会暂时存起来这个Token,在本次的会话生命周期内,保持有效和活跃。
而这里所谓的暂存并不是真正意义上的储存,而是向线程变量中的一个类设置两个属性:AccessKey和Token。而着两个属性伴随着整个访问:
public void process(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) { String accessKey = getAccessKey(request); String userToken = getUserToken(request); AccountContext.setAccessKey(accessKey); AccountContext.setUserToken(userToken); logger.info("setting accessKey -->{} userToken -->{} for request -->{}", accessKey, userToken, request.getRequestURI()); boolean appVersionCheck = !ServletUtil.hasAnno(handler, NoAppVersionCheck.class); ApiAuthService authService = AuthServiceHolder.getAuthService(); Assert.hasText(accessKey, "accessKey"); DataResult result = authService.verifyAppAccessKey(accessKey, appVersionCheck); ResultUtil.assertSucResult(result); }
这样的方式在线程中存好之后而在使用的时候我们只需要在服务端起一个基础服务来提供信息:
@Component public class TradeAccountContext { public String getUserId() { return AccountContext.getUserId(); } /** * @return the loginName */ public String getLoginName() { return AccountContext.getLoginName(); } /** * @return the userToken */ public String getUserToken() { return AccountContext.getUserToken(); } /** * @return the accessKey */ public String getAccessKey() { return AccountContext.getAccessKey(); } public boolean isReleaseMode(){ return "release".equals(env); } }
这样我们只需要请求Url的时候这样做就可以防止非正常用户的操作:
@Autowired private TradeAccountContext tradeAccountContext; @RequestMapping(value = "****", method = RequestMethod.GET, produces = "application/json; charset=utf-8") public ReturnResult xx(String xx, HttpServletRequest request) { String userId= tradeAccountContext.getUserId(); if(userId == null){ throw new Exception(...); } ***** }
可是如果伪造呢?拿一个真正用户去生成Token,然后拿着这个生成的Token去请求其他的URL:
@RequestMapping(value = "****", method = RequestMethod.GET, produces = "application/json; charset=utf-8") public ReturnResult CancelOrder(String userId, String orderNo, String priceType, HttpServletRequest request) { ParameterValidator.validateParamString(orderNo, "orderNo"); OnTheWayFundDetail detail = guideFundService.canCelOrder(orderNo, userId); ReturnResult returnResult = new ReturnResult(); returnResult.setData(detail); return returnResult; }
那就完蛋了。因为Userid是URL传的,也就是说不从线程里去获取上面的UserId的话,他可以操作其他人的相关东西。可不就是这样扯淡的情况嘛..公司的取消订单的接口就是上面的情况。
所以,就有了下面这一幕:
嗯,所以就恶心了呀。