网站建设 技术规范书,海尔网站建设信息,找工作网站建设,wordpress文件名乱码问题引入
我们为开发者提供了接口#xff0c;却对调用者一无所知
假设我们的服务器只能允许 100 个人同时调用接口。如果有攻击者疯狂地请求这个接口#xff0c;那是很危险的。一方面这可能会损害安全性#xff0c;另一方面耗尽服务器性能#xff0c;影响正常用户的使用。…问题引入
我们为开发者提供了接口却对调用者一无所知
假设我们的服务器只能允许 100 个人同时调用接口。如果有攻击者疯狂地请求这个接口那是很危险的。一方面这可能会损害安全性另一方面耗尽服务器性能影响正常用户的使用。
因此我们必须为接口设置保护措施例如限制每个用户每秒只能调用十次接口即实施请求频次的限额控制。所以我们必须知道谁在调用接口并且不能让无权限的人随意调用。
在我们之前开发后端时我们会进行一些权限检查。例如当管理员执行删除操作时后端需要检查这个用户是否为管理员直接从后端的 session 中获取的。但问题来了比如我是前端直接发起请求没有登录操作没有输入用户名和密码我怎么去调用呢
API 签名认证
API 签名认证过程
签发签名 - 使用签名或校验签名
为什么需要API签名认证
为了保证安全性不能让任何人都能调用接口。
适用于无需保存登录态的场景。只认签名不关注用户登录态为了更通用。
如何在后端实现签名认证
需要两个东西即 accessKey 和 secretKey来标识用户
和用户名和密码类似不过每次调用接口都需要带上实现无状态的请求。 “无状态”指的是每个请求都是独立的服务器不会保存客户端的任何状态信息。每次请求都包含所有必要的信息来完成该请求。这种设计使得系统更易于扩展和管理。 签发 accessKey 和 secretKey
一般来说accessKey 和 secretKey 需要尽可能复杂无规律防止黑客尝试破解特别是密码。
签名认证实现
通过 http request header 头传递参数。
参数 1accessKey调用的标识 userA, userB复杂、无序、无规律参数 2secretKey密钥复杂、无序、无规律该参数不能放到请求头中参数 3用户请求参数参数 4签名
加密方式
对称加密、非对称加密、md5 签名不可解密
用户参数 密钥 签名生成算法MD5、HMac、Sha1 不可解密的值
怎么知道这个签名对不对
服务端用一模一样的参数和算法去生成签名只要和用户传的的一致就表示一致。
怎么防重放
参数 5加 nonce 随机数只能用一次存在问题服务端要保存使用过的随机数
所以配合
参数 6加 timestamp 时间戳校验时间戳是否过期。
API 签名认证是一个很灵活的设计具体要有哪些参数、参数名如何一定要根据场景来。 为什么需要两个 key 如果仅凭一个 key 就可以调用接口那么任何拿到这个 key 的人都可以无限制地调用这个接口。这就好比为什么你在登录网站时需要输入密码而不是只输入用户名就可以了其实这两者的原理是一样的。如果像 token 一样一个 key 不行吗token 本质上也是不安全的有可能会通过重放等等方式来攻破的。 TODO关于 accessKey、secretKey 的生成方法自行编写代码实现
实践
在客户端 拿到 accessKey、secretKey 需要获取用户传递的 accessKey 和 secretKey。
对于这种数据建议不要直接在 URL 中传递而是选择在请求头中传递会更为妥当。
因为 GET 请求的 URL 存在最大长度限制如果你传递的其他参数过多可能会导致关键数据被挤出。
PostMapping(/user)
public String getUserNameByPost(RequestBody User user, HttpServletRequest request) {// 从请求头中获取名为 accessKey 的值String accessKey request.getHeader(accessKey);// 从请求头中获取名为 secretKey 的值String secretKey request.getHeader(secretKey);// 如果 accessKey 不等于 sujie 或者 secretKey 不等于 abcdefghif (!accessKey.equals(sujie) || !secretKey.equals(abcdefgh)){// 抛出一个运行时异常表示权限不足throw new RuntimeException(无权限);}// 如果权限校验通过返回 POST 用户名字是 用户名return POST 用户名字是 user.getUsername();
}改造一下 SuApiClient.java发请求可以带上 header用这个就可以去添加很多的请求头
// 使用POST方法向服务器发送User对象并获取服务器返回的结果public String getUserNameByPost(RequestBody User user) {// 将User对象转换为JSON字符串String json JSONUtil.toJsonStr(user);// 使用HttpRequest工具发起POST请求并获取服务器的响应HttpResponse httpResponse HttpRequest.post(http://localhost:8123/api/name/user)// 添加前面构造的请求头.addHeaders(getHeaderMap()).body(json) // 将JSON字符串设置为请求体.execute(); // 执行请求// 打印服务器返回的状态码System.out.println(httpResponse.getStatus());// 获取服务器返回的结果String result httpResponse.body();// 打印服务器返回的结果System.out.println(result);// 返回服务器返回的结果return result;}// 创建一个私有方法用于构造请求头private MapString, String getHeaderMap() {// 创建一个新的 HashMap 对象MapString, String hashMap new HashMap();// 将 accessKey 和其对应的值放入 map 中hashMap.put(accessKey, accessKey);// 将 secretKey 和其对应的值放入 map 中hashMap.put(secretKey, secretKey);// 返回构造的请求头 mapreturn hashMap;}
安全传递
存在的问题
我们的请求有可能被人拦截我们将密码放在请求头中如果有中间人拦截到了你的请求他们就可以直接从请求头中获取你的密码然后使用你的密码发送请求。
密码绝对不能传递。也就是说在向对方发送请求时密码绝对不能以明文的方式传递必须通过特殊的方式进行传递。
我们需要对该密码进行加密这里通常称之为签名。
可以将用户传递的参数与该密钥拼接在一起然后使用单向签名算法进行加密。
如何防止重放请求有两种方式可以考虑
第一种方式是通过加入一个随机数实现标准的签名认证。每次请求时发送一个随机数给后端。后端只接受并认可该随机数一次一旦随机数被使用过后端将不再接受相同的随机数。这种方式解决了请求重放的问题因为即使对方使用之前的时间和随机数进行请求后端会认识到该请求已经被处理过不会再次处理。然而这种方法需要后端额外开发来保存已使用的随机数。并且如果接口的并发量很大每次请求都需要一个随机数那么可能会面临处理百万、千万甚至亿级别请求的情况。因此除了使用随机数之外我们还需要其他机制来定期清理已使用的随机数。
第二种方式是加入一个时间戳timestamp。每个请求在发送时携带一个时间戳并且后端会验证该时间戳是否在指定的时间范围内例如不超过10分钟或5分钟。这可以防止对方使用昨天的请求在今天进行重放。通过这种方式我们可以一定程度上控制随机数的过期时间。因为后端需要同时验证这两个参数只要时间戳过期或随机数被使用过后端会拒绝该请求。因此时间戳可以在一定程度上减轻后端保存随机数的负担。通常情况下这两种方法可以相互配合使用。
因此在标准的签名认证算法中建议至少添加以下五个参数accessKey、secretKey、sign、nonce随机数、timestamp时间戳。此外建议将用户请求的其他参数例如接口中的 name 参数也添加到签名中以增加安全性。
安全传递实现
新建一个 utils 包在 utils 包下新建 SignUtils.java(签名工具)
这个 hashmap 还需要进行拼接我们传递的是用户的这些参数但其实没有必要传递那么多参数直接将 body 作为参数传递进来(在这里我们也可以传递 hashmap只要有一些共同的参数能让客户端和服务端之间保持一致即可)。
/*** 签名工具*/
public class SignUtils {/*** 生成签名* param hashMap 包含需要签名的参数的哈希映射* param secretKey 密钥* return 生成的签名字符串*/public static String genSign(MapString, String hashMap, String secretKey) {// 使用SHA256算法的DigesterDigester md5 new Digester(DigestAlgorithm.SHA256);// 构建签名内容将哈希映射转换为字符串并拼接密钥String content hashMap.toString() . secretKey;// 计算签名的摘要并返回摘要的十六进制表示形式return md5.digestHex(content);}
}刚刚客户端只有这两个参数 accessKey、secretKey
现在再加几个参数
/*** 获取请求头的哈希映射* param body 请求体内容* return 包含请求头参数的哈希映射*/
private MapString, String getHeaderMap(String body) {MapString, String hashMap new HashMap();hashMap.put(accessKey, accessKey);// 注意不能直接发送密钥// hashMap.put(secretKey, secretKey);// 生成随机数(生成一个包含100个随机数字的字符串)hashMap.put(nonce, RandomUtil.randomNumbers(4));// 请求体内容hashMap.put(body, body);// 当前时间戳// System.currentTimeMillis()返回当前时间的毫秒数。通过除以1000可以将毫秒数转换为秒数以得到当前时间戳的秒级表示// String.valueOf()方法用于将数值转换为字符串。在这里将计算得到的时间戳以秒为单位转换为字符串hashMap.put(timestamp, String.valueOf(System.currentTimeMillis() / 1000));// 生成签名hashMap.put(sign, genSign(body, secretKey));return hashMap;
}/*** 通过POST请求获取用户名* param user 用户对象* return 从服务器获取的用户名*/
public String getUserNameByPost(RequestBody User user) {// 将用户对象转换为JSON字符串String json JSONUtil.toJsonStr(user);HttpResponse httpResponse HttpRequest.post(http://localhost:8123/api/name/user)// 添加请求头.addHeaders(getHeaderMap(json))// 设置请求体.body(json)// 发送POST请求.execute();// 打印响应状态码System.out.println(httpResponse.getStatus());// 打印响应体内容String result httpResponse.body();System.out.println(result);return result;
}接下来服务端 PostMapping(/user)public String getUserNameByPost(RequestBody User user, HttpServletRequest request) {// 1.拿到这五个我们可以一步一步去做校验,比如 accessKey 我们先去数据库中查一下// 从请求头中获取参数String accessKey request.getHeader(accessKey);String nonce request.getHeader(nonce);String timestamp request.getHeader(timestamp);String sign request.getHeader(sign);String body request.getHeader(body);// 不能直接获取秘钥// String secretKey request.getHeader(secretKey);// TODO 2.校验权限,这里模拟一下,直接判断 accessKey 是否为yupi,实际应该查询数据库验证权限if (!accessKey.equals(sujie)){throw new RuntimeException(无权限);}// TODO 3.校验一下随机数,因为时间有限,就不带大家再到后端去存储了,后端存储用hashmap或redis都可以// 校验随机数,模拟一下,直接判断nonce是否大于10000if (Long.parseLong(nonce) 10000) {throw new RuntimeException(无权限);}// TODO 4.校验时间戳与当前时间的差距,交给大家自己实现//// if (timestamp) {}// TODO 5. 从实际数据库中取得用户secretKeyString serverSign SignUtils.genSign(body, abcdefgh);if (!serverSign.equals(sign)) {throw new RuntimeException(无权限);}return POST 用户名字是 user.getUsername();}整个签名认证算法的流程就是这样
需要强调的是API签名认证是一种非常灵活的设计具体需要哪些参数以及参数名的选择都应根据具体场景来确定。尽量避免在前端进行签名认证而是由服务端来处理
例如某些公司或项目的签名认证可能会包含 userId 字段以区分用户。还可能包含 appId 和 version 字段来表示应用程序的版本号。有时还会添加一些固定的盐值等等。