API 接口的安全设计验证:ticket,签名,时间戳
一.背景
1.与前端对接的API接口,如果被第三方抓包并进行恶意篡改参数,可能会导致数据泄露,甚至会被篡改数据
2.与第三方公司的接口对接,第三方如果得到你的接口文档,但是接口确没安全校验,是十分不安全的
我主要围绕时间戳,token,签名三个部分来保证API接口的安全性
二.请求过程
1.用户成功登陆站点后,服务器会返回一个token,用户的任何操作都必须带了这个参数,可以将这个参数直接放到header里。
2.客户端用需要发送的参数和token生成一个签名sign,作为参数一起发送给服务端,服务端在用同样的方法生成sign进行检查是否被篡改。
3.但这依然存在问题,可能会被进行恶意无限制访问,这时我们需要引入一个时间戳参数,如果超时即是无效的。
4.服务端需要对token,签名,时间戳进行验证,只有token有效,时间戳未超时,签名有效才能被放行。
概念:
(1)开放接口
没有进行任何限制,简单粗暴的访问方式,这样的接口方式一般在开放的应用平台,查天气,查快递,只要你输入正确对应的参数调用,即可获取到自己需要的信息,我们可以任意修改参数值。
(2)Token认证获取
用户登录成功后,会获取一个ticket值,接下去任何接口的访问都需要这个参数。我们把它放置在redis内,有效期为10分钟,在ticket即将超时,无感知续命。延长使用时间,如果用户在一段时间内没进行任何操作,就需要重新登录系统。
(3)Sign签名
把所有的参数拼接一起,在加入系统秘钥,进行MD5计算生成一个sign签名,防止参数被人恶意篡改,后台按同样的方法生成秘钥,进行签名对比。
(4)重复访问
引入一个时间戳参数,保证接口仅在一分钟内有效,需要和客户端时间保持一致。
(5)拦截器
每次请求都带有这三个参数,我们都需要进行验证,只有在三个参数都满足我们的要求,才允许数据返回或被操作。
三.具体代码实现
1.编写获取tiket的接口
/** * 获取tiket * @param receiveRequest * @return */ @ResponseBody @RequestMapping(value = "/gettiket",method = RequestMethod.POST) public String gettiket(@RequestBody String data){ String result = ""; String msg = ""; try{ log.info("gettiket,入参为==="+data); JdbcTemplate jdbcTemplate = new JdbcTemplate(); String userTocken = UUID.randomUUID().toString(); //cache.put(userTocken, userMap);//数据库方式或者redis方式,这里用数据库方式 String insert_user_token_sql = "insert into user_token(pk_user_token,userid,user_token) VALUES (?,?,?)"; long pk_user_token = KeyUtils.nextId();//主键 jdbcTemplate.executeUpdate(insert_user_token_sql, new Object[]{ pk_user_token,"111",userTocken }); result = userTocken; msg = "{\"success\" : true,\"errorCode\" : \"200\", \"errorMsg\" : \"查询完成\", \"tiket\" :" +result + "}"; log.info("msg===="+msg); return msg; }catch(Exception e){ msg = "{\"success\" : true,\"errorCode\" : \"500\", \"errorMsg\" : \"查询完成\", \"data\" :" +e + "}"; return msg; } }
2.服务端验证
主程序入口
Map<String, String> paramMap = new HashMap<>(); String time = DateUtils.formatDate("yyyy-MM-dd HH:mm:ss.SSS"); paramMap.put("time", time); String ticket = "056a3d29-eed3-4ee9-80aa-c03321d5302f"; paramMap.put("ticket", ticket);//userTock为我第一次请求你的单点url时传给你的userTocken String serviceCode = "cs_demo";// 目标系统对应的密钥 String sign = null; try { sign = SignUtils.sing(paramMap, serviceCode, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } log.info("sign==="+sign); CheckPerService checkPerService= new CheckPerService(); Boolean istrue = checkPerService.TicketSignAndTime( ticket, sign, time, serviceCode); log.info("istrue==="+istrue);
工具类SignUtils
package tcc.test.utill; import org.apache.log4j.Logger; import org.springframework.util.DigestUtils; import java.io.UnsupportedEncodingException; import java.util.*; public class SignUtils { private static final Logger a = Logger.getRootLogger(); public SignUtils() { } public static String getContent(Map params) { List keys = new ArrayList(params.keySet()); Collections.sort(keys); String prestr = ""; boolean first = true; for(int i = 0; i < keys.size(); ++i) { String key = (String)keys.get(i); if (!"sign".equals(key) && !"_r".equals(key) && !"_result_type".equals(key) && !"_".equals(key)) { String value = String.valueOf(params.get(key)); if (value != null && value.trim().length() != 0) { if (first) { prestr = prestr + key + "=" + value; first = false; } else { prestr = prestr + "&" + key + "=" + value; } } } } a.info("加密字符串:" + prestr); return prestr; } public static String sing(Map Params, String key, String charset) throws UnsupportedEncodingException { String signStr = null; signStr = DigestUtils.md5DigestAsHex((getContent(Params) + key).getBytes(charset)); return signStr; } public static void main(String[] args) throws Exception { Map paramMap = new HashMap<String,String>(); paramMap.put("name","tcc"); paramMap.put("age","24"); String serviceCode = "siruinet"; String sing = SignUtils.sing(paramMap, serviceCode, "UTF-8"); System.out.println(sing); } }
权限校验工具类
package tcc.test.utill; import com.alibaba.druid.util.StringUtils; import com.util.FieldList; import jos.engine.core.jdbc.JdbcTemplate; import jos.engine.des.util.DesEncryptUtils; import org.apache.log4j.Logger; import java.util.HashMap; import java.util.Map; /** * Copyright (C) @2022 * * @author: tcc * @version: 1.0 * @date: 2022/1/31 * @time: 2:08 * @description: */ public class CheckPerService{ private static final Logger log = Logger.getRootLogger(); /* 接口权限校验方法1 ticket:票据 sign:签名 time:时间戳 serviceCode:服务编码*/ public static boolean TicketSignAndTime(String ticket, String sign, String time, String serviceCode){ time = time; ticket = ticket; sign = sign; Map<String, String> paramMap = new HashMap<>(); paramMap.put("time", time); paramMap.put("ticket", ticket);//ticket为第一次调用获取ticket接口的数据 serviceCode = serviceCode;// 目标系统对应的密钥 String qm = DesEncryptUtils.sing(paramMap, serviceCode, "UTF-8"); log.info("qm==="+qm); if (!StringUtils.equals(sign, qm)) { //密钥校验错误 log.info("签名不正确"); return false; } log.info("签名正确"); JdbcTemplate jdbcTemplate = new JdbcTemplate(); String qr_user_token_sql = "select count(1) as count from user_token where user_token = ?";//后期改成redis FieldList file_token = jdbcTemplate.queryField(qr_user_token_sql, new Object[]{ ticket }); int count = Integer.parseInt(file_token.get("count")); if(count<1){ return false; } return true; } /* 接口权限校验方法2 name:用户名 pwd:密码 */ public static boolean UnmAndPwd(String name,String pwd){ JdbcTemplate jdbcTemplate = new JdbcTemplate("mzdb"); String qr_user_token_sql = "select count(1) as count from bd_user where USERNAME = ? and USERPASS = ?";//后期改成redis FieldList file_token = jdbcTemplate.queryField(qr_user_token_sql, new Object[]{ name,pwd }); int count = Integer.parseInt(file_token.get("count")); if(count<1){ return false; } return true; } }