前言(项目背景):
这个API签名认证是API开放平台得一个重要环节,我们知道,这个API开发平台,用处就是给客户去调用现成得接口来完成某些事情得。
在讲API签名认证之前,我们先模拟一个场景并且介绍一个java工具类
模拟场景:
我们新建一个模块,新建一个controller层来接受请求,再创建一个客户端来发送请求
所以我们得场景就是服务端和客户端的请求。
服务端controller层代码:
@RestController
@RequestMapping("/name")
public class nameController {@Resourceprivate UserService userService;@GetMapping("/get")public String GetNameByGet(String name){return "Get 你的名字是"+name;}@PostMapping("/")public String PostNameByPost(@RequestParam String name){return "Post 你的名字是"+name;}
}
非常简单就是两个请求接口,方式分别是Get和Post
客户端代码:
public class HttpHutool {public String GetNameByGet(String name){//可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中HashMap<String, Object> paramMap = new HashMap<>();paramMap.put("name",name);String result= HttpUtil.get("http://localhost:8123/api/name/", paramMap);System.out.println(result);return result;}public String PostNameByPost(@RequestParam String name){HashMap<String, Object> paramMap = new HashMap<>();paramMap.put("name", "ljh");String result= HttpUtil.post("http://localhost:8123/api/name/", paramMap);System.out.println(result);return result;}}
这里介绍一个工具:Hutool
简介 | Hutool
Hutool是项目中“util”包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的bug
这是Hutool得官方解释
把这个Hutool看成一个工具类即可
Hutool得依赖:
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version>
</dependency>
1798519188993286146_0.41094494896964641798519188993286146_0.2456341272020588
在客户端代码中,用Hutool来向Controller发送请求
测试类代码:
public class Main {public static void main(String[] args) {HttpHutool httpHutool = new HttpHutool();final String result1 = httpHutool.GetNameByGet("ljh1");final String result2 = httpHutool.PostNameByPost("ljh2");User user = new User();user.setName("ljh3");final String result3 = httpHutool.PostNameByPostRestful(user);System.out.println(result1);System.out.println(result2);System.out.println(result3);}
}
这样就可以简单模拟出结果了。
下面就要引出这篇文章得主角了:
API签名认证:
我们现在要思考一个重要问题:如果我们为开发者提供了一个接口,却对调用者一无所知。假设我们的服务器只能允许100个人同时调用接口。如果有攻击者疯狂地请求这个接口,那将极其危险。一方面这可能会损害我们的安全性,另一方面也可能耗尽服务器性能,影响正常用户的使用。
因此,我们必须为接口设置保护措施,例如限制每个用户每秒只能调用十次接口,即实施请求频次的限额控制。如果在后期,你的业务扩大,可能还需要收费。因此,我们必须知道谁在调用接口,并且不能让无权限的人随意调用。
现在,我们需要设计一个方法,来确定谁在调用接口。在我们之前开发后端时,我们会进行一些权限检查。例如,当管理员执行删除操作时,后端需要检查这个用户是否为管理员。那么,我们如何获取用户信息呢?是否直接从后端的 session 中获取?但问题来了,当我们调用接口时,我们有 session 吗?比如说,我是前端直接发起请求,我没有登录操作,我没有输入用户名和密码,我怎么去调用呢?因此,一般情况下,我们会采用一个叫API签名认证的机制。这是一个重要的概念。
那么,什么是API签名认证?简单地说,如果你想来我家做客,我不可能随便让任何陌生人进来。所以我会提前给你发一个类似于请帖的东西,作为授权或许可证。当你来访问我的时候,你需要带上这个许可证。我可能并不认识你,但我认识你的请帖。只要你有这个请帖,我就允许你进来。
所以,API签名认证主要包括两个过程。第一个是签发签名,第二个是使用签名或校验签名。这就像一些短信接口的key一样。为什么我们需要API签名认证呢?简单地说,第一,为了保证安全性,不能让任何人都能调用接口。那么,我们如何
在后端实现签名认证呢?我们需要两个东西,即 accessKey 和 secretKey。这和用户名和密码类似,不过每次调用接口都需要带上,实现无状态的请求(这里解释一下什么叫无状态请求,像我们平常上网,我们登录过之后,下次访问可能就会有记录,下一次就不需要再重新登录了,不过这个不一样,你每次来都要登录)这样,即使你之前没来过,只要这次的状态正确,你就可以调用接口。所以我们需要这两个东西来标识用户。
下面将为大家演示如何签发 accessKey 和 secretKey,以及如何使用和验证它们。在签发过程中,你可以自己编写一个生成 accessKey 和 secretKey 的工具。一般来说,accessKey 和 secretKey 需要尽可能复杂,以防止黑客尝试破解,特别是密码,需要尽可能复杂,无规律。
知道了上面得流程之后,我们来先写一个简单得签名认证流程:
1:在数据库得user表中加入两个字段accessKey和secretKey
2:在用户端带上这两个凭证:
并且在测试类中修改一下
3:服务端接口校验:
我们需要获取用户传递的 accessKey 和 secretKey。对于这种数据,建议不要直接在URL 中传递,而是选择在请求头中传递会更为妥当。因为GET请求的URL 存在最大长度限制,如果你传递的其他参数过多,可能会导致关键数据被挤出。因此,建议从请求头中获取这些数据。
这里就用了一下别人得笔记,我自己写得时候得方法已经被添加了很多东西
这样我们简单得签名认证逻辑就写好了
在发送请求得时候,控制台就会输出信息,如果你得签名是对的,就通过。
问题:
不过我们看到这个简单的流程中有一个巨大的问题
就是:secretKey在服务器中发送。
问题在于我们的请求有可能被人拦截,我们将密码放在请求头中,如果有中间人拦截到了你的请求,他们就可以直接从请求头中获取你的密码,然后使用你的密码发送请求。
比如你向后端发送请求,secretKey被拦截下来了,然后你这个时候肯定收不到服务端的回应,你可能就是觉得网络卡了,没当回事,其实是你的secretKey被拦截了,别人等过一段时间之后重新带着你的secretKey向后端发送请求,就可以获取到需要的资源了。
所以密码绝对不能传递。也就是说,在向对方发送请求时,密码绝对不能以明文的方式传递,必须通过特殊的方式进行传递。因此,我们目前的做法是行不通的。绝对不能直接在服务端或服务器之间传递密钥,这样是不安全的。那么,我们应该如何使其安全呢?在标准的API签名认证中,我们需要传递一个签名。通常我们不是直接将其传递给后台,而是根据该密钥生成一个签名。因为密码不能直接在服务器中传递,有可能会被拦截。
所以我们需要对该密码进行加密,这里通常称之为签名。那么这个签名是如何生成的呢?让我们思考一下,我们可以将用户传递的参数(例如ABC参数)与该密钥拼接在一起,然后使用签名算法进行加密。但这里实际上并不是真正的加密,也可以使用加密算法。
加密算法:
我们的加密算法可以分为:单向加密(md5签名)、对称加密、非对称加密。对称加密是什么?它是分配一组密钥,你 可以加密和解密。还有非对称加密,你可以使用公钥加密,私钥解密,有些情况下也可以使用私钥加密,公钥解密。此外,还有一种单向加密,即加密后无法解密。这种是安全性最高的
md5 本质上是一种签名算法。例如,百度网盘上传文件时,每个文件都有一个唯一的值。就像md5,一般情况下是 不可逆的,即无法解密。理论上,md5 这种方式最安全。所以我们的思路不是给用户分配的密钥来进行加密。而是我们将其与用户的参数进行拼接。例如,我们的密钥是 ABCDEFGH,经过签名算法后,最后得到的值可能是 fgthtrgerge。这个值是无法解密的。然后我们只需将此值发送给服务器,服务器只需验证签名是否正确即可。这样,我们根本就不传递密码,就是根据密码生成一个值传给服务器
然而问题来了,既然这个值无法解密,作为我的API接口,我如何知道你传递的签名是否正确呢?如何判断?这个
很简单,我可以再次使用相同的参数进行生成,并与你传递的参数进行对比,看它们是否一致。
就比如,你的accessKey是a,secretKey是abc,传递的参数body是:c,然后可能还有时间戳或者随机数一起经过加密算法算出来的值是kjlkj,
我后端有用户的accessKey,secretKey,随机数,然后我将这些变量全都拼起来然后用同样的加密算法进行计算,算出来的值相同才算通过,并且上面有说过重放问题,我这样可以进行校验这个时间戳离我当前的时间,如果超过了一定的时间,我就也不算通过。
说了这么多,我们来梳理一下,我们前后端需要交接的参数
- accessKey
- 随机数(这个可以存储在用户的列表中,不过得定期删除)
- 时间戳
- 用户的请求参数
- 由加密算法算出来的sign参数
服务端controller代码:
@PostMapping("/restful")public String PostNameByPostRestful(@RequestBody User name, HttpServletRequest request){String accessKey = request.getHeader("accessKey");String body = request.getHeader("body");String random = request.getHeader("random");String timestamp = request.getHeader("timestamp");String sign = request.getHeader("sign");//查找数据库中被分配accessKey的用户QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("accessKey",accessKey);User user = userService.getOne(queryWrapper);System.out.println(user.getUserAccount());System.out.println("=================");if(user==null){//user==null,说明这个accessKey根本没有被分配给用户throw new RuntimeException("accessKey不存在");}if(Long.parseLong(random) > 10000){throw new RuntimeException("无权限");}//通过时间戳判断是否过期long currentTimeMillis = System.currentTimeMillis()/1000;long differenceInSeconds = (long) (currentTimeMillis/1000 - Long.parseLong(timestamp));if(differenceInSeconds > 300){throw new RuntimeException("时间戳超时");}//从数据库中查出secretKey//将secretKey和请求参数body再次进行计算算出flag和从请求头中得sign进行比较String secretKey = user.getSecretKey();final String flag = SignUtils.getSign(body, secretKey);if(!flag.equals(sign)){throw new RuntimeException("无权限");}return "PostRestful 你的名字是"+name;}
这里整体的controller代码的逻辑是:
首先先从请求头中取出对呀的参数
再根据accessKey从数据库中查找或者根据request写一个获取当前用户的方法进行校验
接着再判断时间戳是否过期
然后取出用户的secretKey,进行加密计算算出flag与用户在请求头中传过来的sign进行对比
客户端代码:
public HashMap<String,String> addHeader(String body){HashMap<String, String> headermap = new HashMap<>();headermap.put("accessKey",accessKey);
// headermap.put("secretKey",secretKey);headermap.put("body",body);//生成一个长度为4的随机数字headermap.put("random", RandomUtil.randomNumbers(4));headermap.put("timestamp",String.valueOf(System.currentTimeMillis()/1000));headermap.put("sign", SignUtils.getSign(body,secretKey));return headermap;}public String PostNameByPostRestful(@RequestBody Body name){String json = JSONUtil.toJsonStr(name);HttpResponse result = HttpRequest.post("http://localhost:8123/api/name/restful").addHeaders(addHeader(json)).body(json).execute();System.out.println(result.getStatus());System.out.println(result);return result.body();}
这里客户端代码分为两部分,一个是发送请求PostNameByPostRestful,另一个是拼接参数addHeader。