数据安全系列4:密码技术的应用-接口调用的身份识别

 传送门

数据安全系列1:开篇

数据安全系列2:单向散列函数概念

数据安全系列3:密码技术概述

什么是认证?

一谈到认证,多数人的反应可能就是"用户认证" 。就是应用系统如何识别用户的身份,直接一点就是常说的"登录"功能,这可以说是一个系统中最基本的功能了:

认证(Authentication)、授权(Authorization)和凭证(Credentials)这三项可以说是一个系统中最基础的安全设计了,哪怕是再简陋的信息系统,大概也不可能忽略掉“用户登录”这个功能。

--------------------引自系统如何正确分辨操作用户的真实身份

而"登录"又是所有安全功能中的重中之重:没有经过用户认证的过程,所有的安全设计都这空中楼阁,这就意义着登录其实不是一件简单的事情:不仅仅是校验一下用户名、密码是否正确这么简单,而是一系列围绕认证展开的复杂问题:

  • 账户和权限信息作为一种必须最大限度保障安全和隐私
  • 同时又要兼顾各个系统模块、甚至是系统间共享访问的基础主数据

所以登录场景下的用户,除了一般意义上的真实的人,也可能不是一个真正的人:只要拥有用户名、密码并经过了系统的安全认证,就可以被系统所接受了。比如有些黑客程序,或者所谓的"攻击机器人",其实并不是真正的用户在操作。但是这里讨论的场景中,用户指的一般开发口中的各种应用系统,以及为了安全性而设计的应用身份识别!

应用身份认证

应用身份认证的场景,其实在开发中还是很常见的(可能对于非开发人员来说,倒不常见,因为一般用户操作的是时候以自己为主体的,所以不存在什么应用身份认证)。

API接口对于程序员来说(尤其是后端开发)几乎是每个人都接触过的,不论是开发API接口还是调用API接口都并不陌生。API接口一般是由应用系统开发出来供别的系统来调用,只要符合接口的规范或约定,一般都能调用成功。这里成功要说明一下:

  • 不考虑网络环境,默认是通的
  • 也不保证业务执行成功与否,只考虑是否满足参数、URL、请求方式等

调用API接口如果只满足基本要求就能调用,在安全性上其实是不够的。就好比一个系统如果没有"登录"这种基本的认证,任何人都能访问那不是一个道理吗?

在一般的内网环境里面,因为有防火墙的存在,其实对于应用之间的API接口调用的认证要求,倒并不是很严格。但是以下的一些情况却是不能忽视:

  • 涉及外网业务,提供了对外的API接口调用
  • 涉及敏感操作,比如转账汇款、删除资源的高危操作
  • 涉及集中管理,比如一些开放网关、公共应用平台系统
  • 其它一些暂时没有想到的......

有上面这些场景,系统就不能再"裸奔"了!对于具体怎么设计应用身份认证并没有统一的标准和既定的规范,放之四海皆准。不过还是有一些借鉴模式:

  • 使用Oauth2协议的密码模式
  • 使用消息认证码模式

具体使用Oauth2的密码模式还是消息论证码模式并没有明确的规定,主要看应用场景。如果是上面提到的开放网关、平台类系统,出于安全性及管理的需要,使用Oauht2的密码模式比较合适。如果是开发小型系统,也不用对接什么平台类的系统,要自主开发一套应用身份认证功能,可以采用消认证码模式,接下来可以具体讨论一下如何实现及对比之间的差异!

Oauth2密码模式

对于Oauth2协议前面讨论的足够多了,其中又专门介绍了Oauth2系列4:密码模式,所以不再赘述。

这里再简单画一个示意图来说明应用场景:

  • A系统开发API接口,并到平台系统注册
  • B系统调用API接口 ,也到平台系统注册
  • 平台系统负责管理注册的应用(包括对应的接口等资源),并负责在系统间接口调用时进行身份论证

那应用身份认证这个场景跟密码模式具体有什么关系呢,或者说为什么可以采用密码模式来做API接口调用的控制?这里觉得有必要做一个探讨与解释。我们知道Oauth协议其实是一个授权协议(可参考Oauth2系列1:初识Oauth2):

看一下网站应用微信登录开发指南

从上面的时序图可以看出标准场景Oauth2的流程有真实用户参与,所以为了应对没有没有真实用户参与的情况,比如应用身份认证(一般都是应用间接口调用,比如服务间通过HTTP接口调用),Oauth2制定了密码模式来应对:将应用模拟为"用户",并也向应用颁发"账号-clientID"、"密码-clientSecret",应用通过账号、密码直接获取token来完成身份认证!上面流程就变成了下面这样:

消息认证码

如果说Oauth2的密码模式适用于平台类系统,提供了一种通用、与业务无关的身份认证方式,那么消息认证码就是另外一种相对更底层与业务参数有关的认证方式。关于消息认证码的概念,可以参考数据安全系列3:密码技术概述,那么为什么消息论证码可以达到身份论证的目的呢?再回顾一下消息论证码的过程:

  • 在这样的交互过程中,交互的双方需要共享密钥,也即是前面的对称密钥
  • 要计算MAC值,必须持有共享密钥,没有就无法计算MAC值,消息认证码正是利用此特性来完成所谓的认证的。

除此以外,还需要说明的是这个过程里面还依赖于单向散列函数的不可逆性!

密钥管理

从Oauh2协议可以看出,可以单独做一个注册服务,负责client_id、client_secret的管理,对网关这种这种平台系统是必要的。如果是对接系统很少甚至就一个,只要双方约定好"密钥"就行:比如服务提供方生成一个16位"随机数",并颁发给调用方作为"密钥",这样会更简单:

UUID.randomUUID().toString()

至于密钥的具体生成、传输、存储、管理也是一个很大话题,一般可能会涉及到KMS之类系统,这里就不展开了。

接下来模拟一个接口,看下通过消息认证码如何实现身份认证!假设有一个用户注册接口:

    @PostMapping("register")public void register(@RequestParam("userName") String userName, @RequestParam("email") String email) {}

接受2个参数userName、email:规定只能拥有"密钥"的系统才能调用。

实现-版本1-基本功能

能最直接想到的办法是,检验参数内容是否符合要求:

  • 调用方:将userName、email拼接起来生成消息认证码,并传递给服务方
  • 服务方:接收userName、email,拼接起来生成消息认证码,并与调用方传递的认证码比较
  • 如果一致,表示认证成功,不一致则不允许调用

通过这个分析,接口就要多加一个参数接收消息认证码,比如叫signature或digest:

    @PostMapping("register")public void register(@RequestParam String userName, @RequestParam String email, @RequestParam String signature) {System.out.printf("userName:" + userName + ",email:" + email + ",signature:" + signature);}

这里还有一个问题就是如何生成消息认证码,这里提供一个Hmacsha256方法(可自行选择算法):

public static String genHmacSha256Sign(String message, String secret) {// 初始化密钥,这里使用一个示例密钥(在实际应用中,密钥应该保密)byte[] secretKeyBytes = secret.getBytes(StandardCharsets.UTF_8);SecretKeySpec secretKey = new SecretKeySpec(secretKeyBytes, "HmacSHA256");try {// 获取HMAC-SHA256的Mac实例Mac mac = Mac.getInstance("HmacSHA256");mac.init(secretKey);// 要签名的数据byte[] dataBytes = message.getBytes(StandardCharsets.UTF_8);mac.update(dataBytes);// 执行MAC计算byte[] resultBytes = mac.doFinal();// 编码为Base64字符串return Base64.getEncoder().encodeToString(resultBytes);} catch (Exception e) {throw new RuntimeException(e);}}

好,现在假定约定的密钥是:826270b4-542b-4e48-b48c-856bea6453db

注册的用户名、email分别是:张三、zhangsan@qq.com,客户端计算出来signature:

public static void main(String[] args) {String secret = "826270b4-542b-4e48-b48c-856bea6453db";String userName = "张三", email = "zhangsan@qq.com";String message = userName + email;System.out.println(genHmacSha256Sign(message, secret));}

输出摘要为:uTo95CYO1AchnvRK9uAJ1W+nc2bJo2p1IsOtLOdWpsk= 

服务端的验证逻辑调成为:

@PostMapping("register")public String register(@RequestParam String userName, @RequestParam String email, @RequestParam String signature) throws UnsupportedEncodingException {System.out.printf("userName:" + userName + ",email:" + email + ",signature:" + signature);String message = userName + email;String sha256Sign = URLDecoder.decode(SignUtil.genHmacSha256Sign(message, "826270b4-542b-4e48-b48c-856bea6453db"), StandardCharsets.UTF_8.name());if (signature.equals(sha256Sign)) {return "success";}return "error";// 省略注册业务逻辑}

现在启动一下服务端,通过postman来调用一下:

调用成功,一个最基本的认证功能实现完成了! 

实现-版本2-与业务解耦

上面的方式虽然实现了功能,不过还是会发现还是有一些问题:

  • signature放在业务接口里面
  • 要针对每个接口的参数单独约定好message的拼接规则(比如哪些参数参与认证、拼接顺序)

总之一句话,身份认证与业务接口没有强绑定了,所以最好把身份认证设计成一个通用的功能:

  • 提供一个过滤器,在里面进行身份认证的检验,并且指定拦截的URL
  • 为了统一message的拼接规则,统一规则接口的所有参数都参与拼接

所以约定:

  • 将signature从业务接口里面提出来,入到header中传递
  • 接口的入参统一用RequestBody的json形式接收,不再定义成RequestParam

改定代码,服务接口:

import com.tw.tsm.auth.dto.RegisterDtoReq;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;import java.io.UnsupportedEncodingException;@RestController
public class RegisterController {@PostMapping("register")public String register(@RequestBody RegisterDtoReq register) throws UnsupportedEncodingException {System.out.printf("userName:" + register.getUserName() + ",email:" + register.getEmail() + ",signature:" + signature);// 不再业务代码里面进行身份认证了//        String message = userName + email;
//        String sha256Sign = URLDecoder.decode(SignUtil.genHmacSha256Sign(message, "826270b4-542b-4e48-b48c-856bea6453db"), StandardCharsets.UTF_8.name());
//        if (signature.equals(sha256Sign)) {
//            return "success";
//        }
//        return "error";// 省略注册业务逻辑return null;}}@Data
@NoArgsConstructor
@AllArgsConstructor
public class RegisterDtoReq {private String userName;private String email;
}

过滤器:

import com.tw.tsm.base.util.RequestWrapper;
import com.tw.tsm.base.util.SignUtil;
import org.apache.commons.io.IOUtils;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;public class VerityFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {ServletRequest requestWrapper = null;if (request instanceof HttpServletRequest) {requestWrapper = new RequestWrapper((HttpServletRequest) request);}// 在chain.doFiler方法中传递新的request对象if (requestWrapper == null) {chain.doFilter(request, response);} else {verity((HttpServletRequest) requestWrapper);chain.doFilter(requestWrapper, response);}}private void verity(HttpServletRequest requestWrapper) throws IOException {//获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中。String requestBody = IOUtils.toString(requestWrapper.getInputStream(), StandardCharsets.UTF_8.name()).replaceAll("\r\n", "");System.out.printf(requestBody);String sha256Sign = SignUtil.genHmacSha256Sign(requestBody, "826270b4-542b-4e48-b48c-856bea6453db");String signature = requestWrapper.getHeader("signature");if (signature.equals(sha256Sign)) {return;}throw new IllegalArgumentException("参数异常!");}
}

包装的HttpServletRequest,用于读取Body:

import org.apache.commons.io.IOUtils;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;public class RequestWrapper extends HttpServletRequestWrapper {private byte[] requestBody;private HttpServletRequest request;public RequestWrapper(HttpServletRequest request) throws IOException {super(request);this.request = request;}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {if (requestBody == null) {ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();IOUtils.copy(request.getInputStream(), byteArrayOutputStream);this.requestBody = byteArrayOutputStream.toByteArray();}final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);return new ServletInputStream() {@Overridepublic int read() throws IOException {return bais.read();}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}};}}

注册Filter:

@Beanpublic FilterRegistrationBean httpServletRequestReplacedRegistration() {FilterRegistrationBean registration = new FilterRegistrationBean();registration.setFilter(new VerityFilter());registration.addUrlPatterns("/register");registration.addInitParameter("paramName", "paramValue");registration.setName("VerityFilter");registration.setOrder(1);return registration;}

客户端生成signature:

 public static void main(String[] args) {String secret = "826270b4-542b-4e48-b48c-856bea6453db";String userName = "张三", email = "zhangsan@qq.com";
//        String message = userName + email;JSONObject jsonObject = new JSONObject();jsonObject.put("userName", userName);jsonObject.put("email", email);String message = jsonObject.toJSONString();System.out.println(genHmacSha256Sign(message, secret));// System.out.println(genHmacSha256Sign(jsonObject.toString(), secret));}

 现在启动一下服务端,通过postman来调用一下:

header里面也要传参数:

实现-版本3-防重放

经过迭代过的版本,已经将身份认证与业务接口解耦开了,不过这里还有一个安全问题,就是防重放攻击,具体的应对方案也比较成熟:

  • 加时间戳-timestamp。该方法优点是不用额外保存其他信息。缺点是认证双方需要准确的时间同步,同步越好,受攻击的可能性就越小。但当系统很庞大,跨越的区域较广时,要做到精确的时间同步并不是很容易。所以一般会采用在指定时间范围,比如一分钟以内的请求才接受。并且单独使用时间戳,很难完全杜绝重放攻击
  • 加随机数-nonce。该方法优点是认证双方不需要时间同步,双方记住(客户端生成、传递给服务端)使用过的随机数,如发现报文中有以前使用过的随机数,就认为是重放攻击。缺点是需要额外保存使用过的随机数,若记录的时间段较长,则保存和查询的开销较大。所以一般会采用时间戳+随机数方式的:一分钟以内的+此时间段内不重复的随机数请求才接受(存储采用redis,利用reids的TTL机制自动清理数据)

在实际中,常将方法(1)和方法(2)组合使用,这样就只需保存某个很短时间段内的所有随机数,而且时间戳的同步也不需要太精确。时间戳一般都是客户端生成,而nonce可以由客户端生成、也可以由服务端生成:

  • 服务端生成的话,要额外增加一个接口级客户端单独获取nonce
  • 客户端生成则不需要,可以简化调用逻辑

生成timestamp、nonce,也放到header中做为公共参数,并参与message的拼接:message = 摘要算法(业务参数的json字符串+timestamp+nonce)。这里就不再实现了,代码也不难

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/75082.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STL之map和set

1. 关联式容器 vector、list、deque、 forward_list(C11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。 关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是结…

Vue3 其它API Teleport 传送门

Vue3 其它API Teleport 传送门 在定义一个模态框时,父组件的filter属性会影响子组件的position属性,导致模态框定位错误使用Teleport解决这个问题把模态框代码传送到body标签下

C++练习

1.将File练习题&#xff0c;内部的FILE*描述符&#xff0c;改成int描述符 2。写一个类Fifo管道类。提高难度&#xff0c;什么都不提示。只要求&#xff1a;使用自己编写的Fifo类对象&#xff0c;实现2个终端之间互相聊天 file.cpp #include <iostream> #include <c…

《Python Web网站部署应知应会》No4:基于Flask的调用AI大模型的高性能博客网站的设计思路和实战(上)

基于Flask的调用AI大模型的高性能博客网站的设计思路和实战&#xff08;上&#xff09; 摘要 本文详细探讨了一个基于Flask框架的高性能博客系统的设计与实现&#xff0c;该系统集成了本地AI大模型生成内容的功能。我们重点关注如何在高并发、高负载状态下保持系统的高性能和…

实现一个简易版的前端监控 SDK

【简易版的前端监控系统】 1、Promise的错误如何监控&#xff1f;–promise不是所有都是接口请求 2、接口的报错如何监控&#xff1f;–全局监控sdk&#xff0c;不改动公共的请求方法、不改动业务代码&#xff1b;一般接口使用axios请求 3、资源的报错如何监控&#xff1f; 4、…

【操作系统】软中断vs硬中断

在操作系统中&#xff0c;中断&#xff08;Interrupt&#xff09; 是 CPU 响应外部事件的重要机制&#xff0c;分为 硬中断&#xff08;Hardware Interrupt&#xff09; 和 软中断&#xff08;Software Interrupt&#xff09;。它们的核心区别在于 触发方式 和 处理机制。 1. 硬…

力扣刷题-热题100题-第27题(c++、python)

21. 合并两个有序链表 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/merge-two-sorted-lists/description/?envTypestudy-plan-v2&envIdtop-100-liked 常规法 创建一个新链表&#xff0c;遍历list1与list2&#xff0c;将新链表指向list1与list2…

Python包下载路径 Chrome用户数据 修改到非C盘

查看 site-packages 是否能通过命令行完成&#xff1f; 可以&#xff0c;使用以下命令&#xff08;不需写脚本&#xff09;&#xff1a; python -m site输出包含&#xff1a; sys.path site-packages 路径&#xff08;全局和用户级&#xff09; 如果只想看安装路径&#…

【鸿蒙5.0】鸿蒙登录界面 web嵌入(隐私页面加载)

在鸿蒙应用中嵌入 Web 页面并加载隐私页面&#xff0c;可借助 WebView 组件来实现。以下是一个完整示例&#xff0c;展示如何在鸿蒙 ArkTS 里嵌入 Web 页面并加载隐私政策页面。 在 HarmonyOS 应用开发中&#xff0c;如果你希望嵌入一个网页&#xff0c;并且特别关注隐私页面加…

AI加Python的文本数据情感分析流程效果展示与代码实现

本文所使用数据来自于梯田景区评价数据。 一、数据预处理 数据清洗 去除重复值、空值及无关字符(如表情符号、特殊符号等)。 提取中文文本,过滤非中文字符。 统一文本格式(如全角转半角、繁体转简体)。 中文分词与去停用词 使用 jieba 分词工具进行分词。 加载自定义词…

Microi吾码界面设计引擎之基础组件用法大全【内置组件篇·上】

&#x1f380;&#x1f380;&#x1f380; microi-pageengine 界面引擎系列 &#x1f380;&#x1f380;&#x1f380; 一、Microi吾码&#xff1a;一款高效、灵活的低代码开发开源框架【低代码框架】 二、Vue3项目快速集成界面引擎 三、Vue3 界面设计插件 microi-pageengine …

【多线程】单例模式和阻塞队列

目录 一.单例模式 1. 饿汉模式 2. 懒汉模式 二.阻塞队列 1. 阻塞队列的概念 2. BlockingQueue接口 3.生产者-消费者模型 4.模拟生产者-消费者模型 一.单例模式 单例模式&#xff08;Singleton Pattern&#xff09;是一种常用的软件设计模式&#xff0c;其核心思想是确保…

终值定理的推导与理解

终值定理的推导与理解 终值定理是控制理论和信号处理中的一个重要工具&#xff0c;它通过频域的拉普拉斯变换来分析时间域函数的最终稳态值。具体来说&#xff0c;终值定理提供了一个简便的方法&#xff0c;利用 F ( s ) F(s) F(s)&#xff08; f ( t ) f(t) f(t) 的拉普拉斯…

每日c/c++题 备战蓝桥杯(二分答案模版)

在算法学习中&#xff0c;二分答案算法是一种非常高效且常用的技巧。它的核心思想是通过不断缩小搜索范围&#xff0c;逐步逼近目标答案。相比传统的暴力搜索&#xff0c;二分答案算法的时间复杂度通常为 O(logn)&#xff0c;特别适合处理大规模数据的查找问题。 本文将详细介…

NLP高频面试题(二十六)——RAG的retriever模块作用,原理和目前存在的挑战

在自然语言处理领域&#xff0c;检索增强生成&#xff08;Retrieval-Augmented Generation&#xff0c;简称RAG&#xff09;是一种将信息检索与文本生成相结合的技术&#xff0c;旨在提升模型的回答准确性和信息丰富度。其中&#xff0c;Retriever在RAG架构中扮演着关键角色&am…

第30周Java分布式入门 分布式基础

分布式基础课程笔记 一、什么是分布式&#xff1f; 1. 权威定义 分布式系统定义为&#xff1a;“利用物理架构形成多个自治的处理元素&#xff0c;不共享主内存&#xff0c;通过发送消息合作”。 2. 核心解释 物理架构与处理元素 &#x1f31f; 多台独立服务器/电脑&#x…

Vuex状态管理

Vuex Vuex是一个专为Vue.js应用程序开发的状态管理模式。它采用集中式管理应用的所有组件状态&#xff0c;并以相应的规则保证状态以一种可预测的方式发生变化。&#xff08;类似于在前端的数据库&#xff0c;这里的数据存储在内存当中&#xff09; 一、安装并配置 在项目的…

从代码学习深度学习 - 使用块的网络(VGG)PyTorch版

文章目录 前言一、VGG网络简介1.1 VGG的核心特点1.2 VGG的典型结构1.3 优点与局限性1.4 本文的实现目标二、搭建VGG网络2.1 数据准备2.2 定义VGG块2.3 构建VGG网络2.4 辅助工具2.4.1 计时器和累加器2.4.2 准确率计算2.4.3 可视化工具2.5 训练模型2.6 运行实验总结前言 深度学习…

Baklib激活企业知识管理新动能

Baklib核心技术架构解析 Baklib的底层架构以模块化设计为核心&#xff0c;融合知识中台的核心理念&#xff0c;通过分布式存储引擎与智能语义分析系统构建三层技术体系。数据层采用多源异构数据接入协议&#xff0c;支持文档、音视频、代码片段等非结构化数据的实时解析与分类…

小智机器人中的部分关键函数,FreeRTOS中`xEventGroupWaitBits`函数的详细解析

以下是对FreeRTOS中xEventGroupWaitBits函数的详细解析&#xff1a; 函数功能 xEventGroupWaitBits用于在事件组中等待指定的位被设置。它可以配置为等待任意一个位或所有位&#xff0c;并支持超时机制。 注意&#xff1a;该函数不能在中断中调用。 函数原型 EventBits_t xEv…