文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

如何保证API接口安全?

2024-12-03 04:15

关注

当你的公司体量上来了时候,这个时候可能有一些公司开始找你进行技术对接了,转变成由你来提供api接口,那这个时候,我们应该如何设计并保证API接口安全呢?

二、方案介绍

最常用的方案,主要有两种:

1. token方案

其中 token 方案,是一种在web端使用最广的接口鉴权方案,我记得在之前写过一篇《手把手教你,使用JWT实现单点登录》的文章,里面介绍的比较详细,有兴趣的朋友可以看一下,没了解的也没关系,我们在此简单的介绍一下 token 方案。

从上图,我们可以很清晰的看到,token 方案的实现主要有以下几个步骤:

在实际使用过程中,当用户登录成功之后,生成的token存放在redis中时是有时效的,一般设置为2个小时,过了2个小时之后会自动失效,这个时候我们就需要重新登录,然后再次获取有效token。

token方案,是目前业务类型的项目当中使用最广的方案,而且实用性非常高,可以很有效的防止黑客们进行抓包、爬取数据。

但是 token 方案也有一些缺点!最明显的就是与第三方公司进行接口对接的时候,当你的接口请求量非常大,这个时候 token 突然失效了,会有大量的接口请求失败。

这个我深有体会,我记得在很早的时候,跟一家中、大型互联网公司进行联调的时候,他们提供给我的接口对接方案就是token方案,当时我司的流量高峰期时候,请求他们的接口大量报错,原因就是因为token失效了,当token失效时,我们会调用他们刷新token接口,刷新完成之后,在token失效与重新刷新token这个时间间隔期间,就会出现大量的请求失败的日志,因此在实际API对接过程中,我不推荐大家采用 token方案。

2. 接口签名

接口签名,顾名思义,就是通过一些签名规则对参数进行签名,然后把签名的信息放入请求头部,服务端收到客户端请求之后,同样的只需要按照已定的规则生产对应的签名串与客户端的签名信息进行对比,如果一致,就进入业务处理流程;如果不通过,就提示签名验证失败。

在接口签名方案中,主要有四个核心参数:

其中签名的生成规则,分两个步骤:

对请求参数进行一次md5加密签名

  1. //步骤一 
  2. String 参数1 = 请求方式 + 请求URL相对地址 + 请求Body字符串; 
  3. String 参数1加密结果= md5(参数1) 

对第一步签名结果,再进行一次md5加密签名

  1. //步骤二 
  2. String 参数2 = appsecret + timestamp + nonce + 参数1加密结果; 
  3. String 参数2加密结果= md5(参数2) 

参数2加密结果,就是我们要的最终签名串。

接口签名方案,尤其是在接口请求量很大的情况下,依然很稳定。

换句话说,你可以将接口签名看作成对token方案的一种补充。

但是如果想把接口签名方案,推广到前后端对接,答案是:不适合。

因为签名计算非常复杂,其次,就是容易泄漏appsecret!

说了这么多,下面我们就一起来用程序实践一下吧!

二、程序实践

1. token方案

就像上文所说,token方案重点在于,当用户登录成功之后,我们只需要生成好对应的token,然后将其返回给前端,在下次请求业务接口的时候,需要把token带上。

具体的实践,也可以分两种:

下面,我们介绍的是第二种实现方式。

首先,编写一个jwt 工具。

  1. public class JwtTokenUtil { 
  2.     //定义token返回头部 
  3.     public static final String AUTH_HEADER_KEY = "Authorization"
  4.     //token前缀 
  5.     public static final String TOKEN_PREFIX = "Bearer "
  6.     //签名密钥 
  7.     public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x"
  8.     //有效期默认为 2hour 
  9.     public static final Long EXPIRATION_TIME = 1000L*60*60*2; 
  10.      
  11.     public static String createToken(String content){ 
  12.         return TOKEN_PREFIX + JWT.create() 
  13.                 .withSubject(content) 
  14.                 .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) 
  15.                 .sign(Algorithm.HMAC512(KEY)); 
  16.     } 
  17.      
  18.     public static String verifyToken(String token) throws Exception { 
  19.         try { 
  20.             return JWT.require(Algorithm.HMAC512(KEY)) 
  21.                     .build() 
  22.                     .verify(token.replace(TOKEN_PREFIX, "")) 
  23.                     .getSubject(); 
  24.         } catch (TokenExpiredException e){ 
  25.             throw new Exception("token已失效,请重新登录",e); 
  26.         } catch (JWTVerificationException e) { 
  27.             throw new Exception("token验证失败!",e); 
  28.         } 
  29.     } 

接着,我们在登录的时候,生成一个token,然后返回给客户端。

  1. @RequestMapping(value = "/login"method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) 
  2. public UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){ 
  3.     //...参数合法性验证 
  4.     //从数据库获取用户信息 
  5.     User dbUser = userService.selectByUserNo(userDto.getUserNo); 
  6.     //....用户、密码验证 
  7.     //创建token,并将token放在响应头 
  8.     UserToken userToken = new UserToken(); 
  9.     BeanUtils.copyProperties(dbUser,userToken); 
  10.     String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken)); 
  11.     response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token); 
  12.     //定义返回结果 
  13.     UserVo result = new UserVo(); 
  14.     BeanUtils.copyProperties(dbUser,result); 
  15.     return result; 

最后,编写一个统一拦截器,用于验证客户端传入的token是否有效。

  1. @Slf4j 
  2. public class AuthenticationInterceptor implements HandlerInterceptor { 
  3.     @Override 
  4.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 
  5.         // 从http请求头中取出token 
  6.         final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY); 
  7.         //如果不是映射到方法,直接通过 
  8.         if(!(handler instanceof HandlerMethod)){ 
  9.             return true; 
  10.         } 
  11.         //如果是方法探测,直接通过 
  12.         if (HttpMethod.OPTIONS.equals(request.getMethod())) { 
  13.             response.setStatus(HttpServletResponse.SC_OK); 
  14.             return true; 
  15.         } 
  16.         //如果方法有JwtIgnore注解,直接通过 
  17.         HandlerMethod handlerMethod = (HandlerMethod) handler; 
  18.         Method method=handlerMethod.getMethod(); 
  19.         if (method.isAnnotationPresent(JwtIgnore.class)) { 
  20.             JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class); 
  21.             if(jwtIgnore.value()){ 
  22.                 return true; 
  23.             } 
  24.         } 
  25.         LocalAssert.isStringEmpty(token, "token为空,鉴权失败!"); 
  26.         //验证,并获取token内部信息 
  27.         String userToken = JwtTokenUtil.verifyToken(token); 
  28.         //将token放入本地缓存 
  29.         WebContextUtil.setUserToken(userToken); 
  30.         return true; 
  31.     } 
  32.     @Override 
  33.     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 
  34.         //方法结束后,移除缓存的token 
  35.         WebContextUtil.removeUserToken(); 
  36.     } 

在生成token的时候,我们可以将一些基本的用户信息,例如用户ID、用户姓名,存入token中,这样当token鉴权通过之后,我们只需要通过解析里面的信息,即可获取对应的用户ID,可以省下去数据库查询一些基本信息的操作。

同时,使用的过程中,尽量不要存放敏感信息,因为很容易被黑客解析!

2. 接口签名

同样的思路,站在服务端验证的角度,我们可以先编写一个签名拦截器,验证客户端传入的参数是否合法,只要有一项不合法,就提示错误。

具体代码实践如下:

  1. public class SignInterceptor implements HandlerInterceptor { 
  2.  
  3.     @Autowired 
  4.     private AppSecretService appSecretService; 
  5.  
  6.     @Autowired 
  7.     private RedisUtil redisUtil; 
  8.  
  9.     @Override 
  10.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
  11.             throws Exception { 
  12.         //appId验证 
  13.         final String appId = request.getHeader("appid"); 
  14.         if(StringUtils.isEmpty(appId)){ 
  15.             throw new CommonException("appid不能为空"); 
  16.         } 
  17.         String appSecret = appSecretService.getAppSecretByAppId(appId); 
  18.         if(StringUtils.isEmpty(appSecret)){ 
  19.             throw new CommonException("appid不合法"); 
  20.         } 
  21.         //时间戳验证 
  22.         final String timestamp = request.getHeader("timestamp"); 
  23.         if(StringUtils.isEmpty(timestamp)){ 
  24.             throw new CommonException("timestamp不能为空"); 
  25.         } 
  26.         //大于5分钟,非法请求 
  27.         long diff = System.currentTimeMillis() - Long.parseLong(timestamp); 
  28.         if(Math.abs(diff) > 1000 * 60 * 5){ 
  29.             throw new CommonException("timestamp已过期"); 
  30.         } 
  31.         //临时流水号,防止重复提交 
  32.         final String nonce = request.getHeader("nonce"); 
  33.         if(StringUtils.isEmpty(nonce)){ 
  34.             throw new CommonException("nonce不能为空"); 
  35.         } 
  36.         //验证签名 
  37.         final String signature = request.getHeader("signature"); 
  38.         if(StringUtils.isEmpty(nonce)){ 
  39.             throw new CommonException("signature不能为空"); 
  40.         } 
  41.         final String method = request.getMethod(); 
  42.         final String url = request.getRequestURI(); 
  43.         final String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8")); 
  44.         String signResult = SignUtil.getSignature(method, url, body, timestamp, nonce, appSecret); 
  45.         if(!signature.equals(signResult)){ 
  46.             throw new CommonException("签名验证失败"); 
  47.         } 
  48.         //检查是否重复请求 
  49.         String key = appId + "_" + timestamp + "_" + nonce; 
  50.         if(redisUtil.exist(key)){ 
  51.             throw new CommonException("当前请求正在处理,请不要重复提交"); 
  52.         } 
  53.         //设置5分钟 
  54.         redisUtil.save(key, signResult, 5*60); 
  55.         request.setAttribute("reidsKey",key); 
  56.     } 
  57.  
  58.     @Override 
  59.     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 
  60.             throws Exception { 
  61.         //请求处理完毕之后,移除缓存 
  62.         String value = request.getAttribute("reidsKey"); 
  63.         if(!StringUtils.isEmpty(value)){ 
  64.             redisUtil.remove(value); 
  65.         } 
  66.     } 
  67.  

签名工具类SignUtil:

  1. public class SignUtil { 
  2.  
  3.      
  4.     public static String getSignature(String method, String url, String body, String timestamp, String nonce, String appSecret){ 
  5.         //第一层签名 
  6.         String requestStr1 = method + url + body + appSecret; 
  7.         String signResult1 = DigestUtils.md5Hex(requestStr1); 
  8.         //第二层签名 
  9.         String requestStr2 = appSecret + timestamp + nonce + signResult1; 
  10.         String signResult2 = DigestUtils.md5Hex(requestStr2); 
  11.         return signResult2; 
  12.     } 

签名计算,可以换成hamc方式进行计算,思路大致一样。

三、小结

上面介绍的token和接口签名方案,对外都可以对提供的接口起到保护作用,防止别人篡改请求,或者模拟请求。

但是缺少对数据自身的安全保护,即请求的参数和返回的数据都是有可能被别人拦截获取的,而这些数据又是明文的,所以只要被拦截,就能获得相应的业务数据。

对于这种情况,推荐大家对请求参数和返回参数进行加密处理,例如RSA、AES等加密工具。

同时,在生产环境,采用https方式进行传输,可以起到很好的安全保护作用!

 

来源:Java极客技术内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯