bailangriver 2019-06-27
移动应用开发过程中请求服务端采用token(在计算机身份认证中是令牌(临时))方式请求方式进行,get请求方式下token直接暴露在请求路径很容易被别人利用进行篡改进行重复提交等,怎样保证移动端安全成为后台开发者所面临的问题,,因为涉及敏感行业数据APP接口开发过程中安全性成为要求,在网上看了很多资料最后选择采用token+time+nonce+sign方式在过滤器层进行校验,APP进行拼接加密,后端Filter进行解密校验,优点实现简单,能够很好保证请求过程中APP端到服务器端安全性,因此此种校验方式被很多互联网公司所采用。
token: token为用户登录时获取的临时令牌
time: APP获取到的当前时间,形式为long类型,time时间可进行匹配APP获取时间和服务器时间差如果大于某个时间则失效(如链接超过60秒失效,具体可参考源码),可防止别人截取数据后修改数据内容进行提交
nonce:APP接口通过UUID等形式获取的随机不重复内容,可以讲nonce存放在session或redis中并设置合适的超时时间,然后下次请求时通过nonce取session或redis中是否存在,如果存在则认为重复提交请求,防止被别人篡改后提交数据
sign: 通过token+time+nonce三个参数加密后的密文(sign加密可使用使用ase,md5等加密方式,需要注意md5加密不可逆,实现过程有所区别),sign主要校验token、time、nonce有没有被别人篡改数据
1在过滤层实现SecurityFilter类 public class SecurityFilter extends Filter { private static String secretKey = ApplicationConfig.secretKey; private static String ivParameter = ApplicationConfig.ivParameter ; // 本文采用ase加密 将加密secretKey及ivParameter 的保存在properties文件中,使用公共接口加载,用户可以选择其他加密方式 @Override protected void doDestroy() { // TODO Auto-generated method stub } @Override protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Throwable { String requestUri = request.getRequestURI(); //登录接口则直接不适用 if(requestUri.indexOf("applogin")>-1){ chain.doFilter(request, response); } else { MsgEntity msg = new MsgEntity(); // 获取request的URL,用于判断该请求是否超时 String time = request.getParameter("time"); // 获取请求nonce,用于判断是否用户重复提交 String nonce = request.getParameter("nonce"); // 获取token码,实际传入token值 String token = request.getParameter("token"); // 获取sign,通过time,nonce,token合并进行加密后密文,需要注意加密顺序,平台必须与APP一致 String sign = request.getParameter("sign"); // 判断数据是否存在 if (StringUtils.isEmpty(time) || StringUtils.isEmpty(nonce) || StringUtils.isEmpty(token) || StringUtils.isEmpty(sign)) { msg.setMsg("登录失败"); msg.setState(false); HttpRequestUtils.writeResponseJsonString(response, msg); return; } //在实际开发过程中传入数据发现存在%号无法进行解密,需要进行URLDecoder.decode进行解码 if (sign.indexOf("%") > -1) { sign = URLDecoder.decode(sign, "UTF-8"); } // 判断请求是否超过60秒,超过60秒则次请求链接失效,返回登录失败,可根据网络情况适当延长或者缩短时间 if (difference(time) > 60) { msg.setMsg("登录失败"); msg.setState(false); HttpRequestUtils.writeResponseJsonString(response, msg); return; } // 判断是否是重复请求,如果请求nonce存在则登录失败,不存在则将nonce存在redis中或session中并设置合适超时时间 if (JedisUtils.exists(nonce)) { msg.setState(false); msg.setMsg("登录失败"); HttpRequestUtils.writeResponseJsonString(response, msg); return; } else { JedisUtils.set(nonce, "0", 70); } // aes 解密,sign和token + time + nonce进行比较判断数据数据是否存在篡改 String mingwei = ""; try { mingwei = AesUtils.decrypt(sign); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } if (!mingwei.equals(token + time + nonce)) { msg.setState(false); msg.setMsg("登录失败"); HttpRequestUtils.writeResponseJsonString(response, msg); return; } //如果请求全部验证通过则放行 chain.doFilter(request, response); }
//主要用于系统时间-APP发送数据时生成日期得到当前差值(秒) public int difference(String past) { long newTimestamp = new Date().getTime(); return Math.abs((int) (newTimestamp - Long.parseLong(past)) / 1000); } } /** * aes 解密 * * @param sSrc * @return * @throws Exception */ public static String decrypt(String sSrc) throws Exception { try { byte[] raw = secretKey.getBytes("ASCII"); SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); Cipher cipher = Cipher.getInstance("AES / CBC / PKCS5Padding"); IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes()); cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv); byte[] encrypted1 = new BASE64Decoder().decodeBuffer(sSrc);// 先用base64解密 byte[] original = cipher.doFinal(encrypted1); String originalString = new String(original, "UTF-8"); return originalString; } catch (Exception ex) { return null; } }
以上为过滤实现token校验规则代码,在开发过程中可以参考此代码进行适当修改进行使用