实用-API接口设计( 二 )


PingSweep:使用ICMP Echo轮询多个主机 。
三、sign 简介nonce:随机值,是客户端随机生成的值,作为参数传递过来,随机值的目的是增加sign签名的多变性 。随机值一般是数字和字母的组合,6位长度,随机值的组成和长度没有固定规则 。
sign: 一般用于参数签名,防止参数被非法篡改,最常见的是修改金额等重要敏感参数,sign的值一般是将所有非空参数按照升续排序然后+token+key+timestamp+nonce(随机数)拼接在一起,然后使用某种加密算法进行加密,作为接口中的一个参数sign来传递,也可以将sign放到请求头中 。
接口在网络传输过程中如果被黑客挟持,并修改其中的参数值,然后再继续调用接口,虽然参数的值被修改了,但是因为黑客不知道sign是如何计算出来的,不知道sign都有哪些值构成,不知道以怎样的顺序拼接在一起的,最重要的是不知道签名字符串中的key是什么,所以黑客可以篡改参数的值,但没法修改sign的值,当服务器调用接口前会按照sign的规则重新计算出sign的值然后和接口传递的sign参数的值做比较,如果相等表示参数值没有被篡改,如果不等,表示参数被非法篡改了,就不执行接口了 。
四、防止重复提交对于一些重要的操作需要防止客户端重复提交的(如非幂等性重要操作),具体办法是当请求第一次提交时将sign作为key保存到redis,并设置超时时间,超时时间和Timestamp中设置的差值相同 。
当同一个请求第二次访问时会先检测redis是否存在该sign,如果存在则证明重复提交了,接口就不再继续调用了 。如果sign在缓存服务器中因过期时间到了,而被删除了,此时当这个url再次请求服务器时,因token的过期时间和sign的过期时间一直,sign过期也意味着token过期,那样同样的url再访问服务器会因token错误会被拦截掉,这就是为什么sign和token的过期时间要保持一致的原因 。拒绝重复调用机制确保URL被别人截获了也无法使用(如抓取数据) 。
对于哪些接口需要防止重复提交可以自定义个注解来标记 。
注意:所有的安全措施都用上的话有时候难免太过复杂,在实际项目中需要根据自身情况作出裁剪,比如可以只使用签名机制就可以保证信息不会被篡改,或者定向提供服务的时候只用Token机制就可以了 。如何裁剪,全看项目实际情况和对接口安全性的要求 。
五、使用流程1.接口调用方(客户端)向接口提供方(服务器)申请接口调用账号,申请成功后,接口提供方会给接口调用方一个appId和一个key参数
2.客户端携带参数appId、timestamp、sign去调用服务器端的API token,其中sign=加密(appId + timestamp + key)
3.客户端拿着api_token 去访问不需要登录就能访问的接口
4.当访问用户需要登录的接口时,客户端跳转到登录页面,通过用户名和密码调用登录接口,登录接口会返回一个usertoken, 客户端拿着usertoken 去访问需要登录才能访问的接口
sign的作用是防止参数被篡改,客户端调用服务端时需要传递sign参数,服务器响应客户端时也可以返回一个sign用于客户度校验返回的值是否被非法篡改了 。客户端传的sign和服务器端响应的sign算法可能会不同 。
六、示例代码

  1. dependency
org.springframework.boot spring-boot-starter-data-redis redis.clients jedis 2.9.0 org.springframework.boot spring-boot-starter-web
  1. RedisConfiguration
@Configuration public class RedisConfiguration { @Bean public JedisConnectionFactory jedisConnectionFactory(){ return new JedisConnectionFactory(); }
/** * 支持存储对象 * @return */@Beanpublic RedisTemplate<String, String> redisTemplate(){RedisTemplate<String, String> redisTemplate = new StringRedisTemplate();redisTemplate.setConnectionFactory(jedisConnectionFactory());Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}复制代码}
  1. TokenController
@Slf4j @RestController @RequestMapping("/api/token") public class TokenController {
@Autowiredprivate RedisTemplate redisTemplate;/** * API Token * * @param sign * @return */@PostMapping("/api_token")public ApiResponse<AccessToken> apiToken(String appId, @RequestHeader("timestamp") String timestamp, @RequestHeader("sign") String sign) {Assert.isTrue(!StringUtils.isEmpty(appId) && !StringUtils.isEmpty(timestamp) && !StringUtils.isEmpty(sign), "参数错误");long reqeustInterval = System.currentTimeMillis() - Long.valueOf(timestamp);Assert.isTrue(reqeustInterval < 5 * 60 * 1000, "请求过期,请重新请求");// 1. 根据appId查询数据库获取appSecretAppInfo appInfo = new AppInfo("1", "12345678954556");// 2. 校验签名String signString = timestamp + appId + appInfo.getKey();String signature = MD5Util.encode(signString);log.info(signature);Assert.isTrue(signature.equals(sign), "签名错误");// 3. 如果正确生成一个token保存到redis中,如果错误返回错误信息AccessToken accessToken = this.saveToken(0, appInfo, null);return ApiResponse.success(accessToken);}@NotRepeatSubmit(5000)@PostMapping("user_token")public ApiResponse<UserInfo> userToken(String username, String password) {// 根据用户名查询密码, 并比较密码(密码可以RSA加密一下)UserInfo userInfo = new UserInfo(username, "81255cb0dca1a5f304328a70ac85dcbd", "111111");String pwd = password + userInfo.getSalt();String passwordMD5 = MD5Util.encode(pwd);Assert.isTrue(passwordMD5.equals(userInfo.getPassword()), "密码错误");// 2. 保存TokenAppInfo appInfo = new AppInfo("1", "12345678954556");AccessToken accessToken = this.saveToken(1, appInfo, userInfo);userInfo.setAccessToken(accessToken);return ApiResponse.success(userInfo);}private AccessToken saveToken(int tokenType, AppInfo appInfo,UserInfo userInfo) {String token = UUID.randomUUID().toString();// token有效期为2小时Calendar calendar = Calendar.getInstance();calendar.setTime(new Date());calendar.add(Calendar.SECOND, 7200);Date expireTime = calendar.getTime();// 4. 保存tokenValueOperations<String, TokenInfo> operations = redisTemplate.opsForValue();TokenInfo tokenInfo = new TokenInfo();tokenInfo.setTokenType(tokenType);tokenInfo.setAppInfo(appInfo);if (tokenType == 1) {tokenInfo.setUserInfo(userInfo);}operations.set(token, tokenInfo, 7200, TimeUnit.SECONDS);AccessToken accessToken = new AccessToken(token, expireTime);return accessToken;}public static void main(String[] args) {long timestamp = System.currentTimeMillis();System.out.println(timestamp);String signString = timestamp + "1" + "12345678954556";String sign = MD5Util.encode(signString);System.out.println(sign);System.out.println("-------------------");signString = "password=123456&username=1&12345678954556" + "ff03e64b-427b-45a7-b78b-47d9e8597d3b1529815393153sdfsdfsfs" + timestamp + "A1scr6";sign = MD5Util.encode(signString);System.out.println(sign);}复制代码


推荐阅读