目录
一、前言
二、登录认证安全问题
3.1 认证方式选择
三、常用的加密方案
3.1 MD5加密算法
3.1.1 md5特点
3.1.2 md5原理
3.1.3 md5使用场景
3.2 AES加密算法
3.2.1 AES简介
3.2.2 AES加解原理
3.2.3 AES算法优缺点
3.2.4 AES算法使用场景
3.3 RSA加密算法
3.3.1 RSA加密算法介绍
3.3.2 RSA加密算法原理
3.3.3 RSA算法优缺点
3.3.4 RSA算法应用场景
3.4 JWT算法
3.4.1 JWT是什么
3.4.2 JWT算法特点
3.4.3 JWT工作原理
3.4.4 JWT优点
3.5 OAuth 2.0
3.5.1 OAuth2.0 介绍
3.5.2 OAuth2.0 流程
3.5.3 OAuth2.0 授权类型
3.5.4 OAuth2.0 核心角色
3.5.5 OAuth2.0 优点
3.6 数字签名
3.6.1 场景分析一
3.6.2 场景分析二
3.6.3 数字签名流程
四、常用加密算法使用
4.1 MD5加密算法使用
4.2 AES加密算法使用
4.3 RSA加密算法使用
4.4 JWT加密算法使用
4.4.1 JWT构成
4.4.2 操作演示
4.5 数字签名使用
五、写在文末
一、前言
登录认证功能可以说是所有系统基本功能。比如在springboot构建的微服务工程中,大家熟悉的jwt技术,以及基于oauth 2.0协议的安全认证等,就是用于构建系统安全的基础技术框架。
认证的目的是为了保护系统资源不被恶意使用,通常来说,最基础的认证方式即大家熟悉的用户名和密码。随着移动端应用的广泛使用,用户对登录认证的操作便捷性也提出了越来越高的要求,像微信扫扫码登录,手机号登录等方式都是适应时代发展的产物。
二、登录认证安全问题
在所有需要登录认证的系统中,首要考虑的问题包括下面两点:
-
采用什么认证方式?是用户名+密码?微信扫码登录?还是手机短信验证码登录?
-
如果是用户名+密码登录,密码采用什么方式进行加密?
3.1 认证方式选择
选择哪种方式作为系统的认证方案呢?这个没有统一的答案。目前主流的互联网产品从大类上可以分为C端和B端两类产品。继续细分下去,C端产品又可以分成多种类型。比如面向社交的产品,电商交易产品,音视频等。而B端产品一般面向企业,政府,银行,券商等业务,主要是企业级内部业务使用。
相对来说,面向C端的产品通常为了考虑用户的使用体验,以及快速占领市场为目标,会尽可能让登录更简单,减少用户的操作难度,这才有今天的扫码登录,短信验证码登录的模式。
而B端产品,更多的是业务型驱动,系统的使用人员通常是为了处理某些具体场景下的业务而保持对系统的长期使用,所以在登录认证方面,为了系统的安全性,适当牺牲一些性能和体验也能被接受。这种情况下,就可以考虑安全性更好的登录认证方案。
三、常用的加密方案
互联网产品发展到今天,为了拥抱用户端的需求和变化,逐渐出现很多种成熟的密码加密方案,下面就常用的密码加密方式展开说明。
3.1 MD5加密算法
MD5,全称:Message Digest Algorithm 5,是一种常用的哈希算法,可用于将任意长度的数据转换为固定长度的哈希值(通常为128位,16个字节)。在早些年,MD5被广泛使用过。但现在它已经不再被视为安全的加密算法,因为它存在一些安全漏洞,如碰撞攻击和彩虹表攻击。
3.1.1 md5特点
md5加密具有如下特点:
-
单向加密,即明文经过加密之后形成密文;
-
产生的是固定长度的散列值,无论输入了多长的数据,经过 MD5 处理后都只会产生一个16字节的散列值;
-
不可逆,经过 MD5 处理后得到的散列结果,无法计算出原始数据,正是因为 MD5 无法从密文还原成明文,它不能用于解密了;
-
运算速度快,MD5 采用的是位运算,速度很快,几乎不占用 CPU 资源;
-
安全性差,1996年后该算法被证实存在弱点,可被加以破解。2004年,证实 MD5 算法无法防止碰撞,因此不适用于安全性认证,比如 SSL 公开密钥认证、数字签名等用途。2011年,RFC 6151 禁止 MD5 用作密钥散列消息认证码;
3.1.2 md5原理
MD5 本质上也是一种哈希算法,会输出一个16字节(128位)固定长度的散列值,该算法的大致流程如上图,其 算法原理,可简单描述为:MD5 码以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。
3.1.3 md5使用场景
尽管MD5的安全性方面相比其他主流加密算法稍显不足,但是经过多年的发展和沉淀,也有很多适用场景,下面列举了一些经典的使用场景。
密码存储
在许多系统中,用户的密码需要存储。而密码直接存储在数据库中是非常危险的,因此MD5算法可以将密码转为不可逆的摘要值再存储,即使数据库被攻击,被泄露的密码也无法被还原。
数字签名
数字签名是利用密钥将数据进行签名,以确保数据完整性和可靠性。MD5算法可以生成一个用于数字签名的摘要值,如果数据在传输过程中被篡改,摘要值也会发生改变,从而保证数据完整性。
文件校验
MD5算法可以对文件进行校验,确保文件没有被篡改或损坏。对于需保密的文件,可以用MD5算法对文件进行加密处理,只有经过相应计算才能打开文件。
软件验证
MD5算法可用于软件验证,比如网站的下载链接可以加密,下载后进行MD5值比较,可以确保文件的完整性和安全性。
3.2 AES加密算法
3.2.1 AES简介
AES加密算法,全称Advanced Encryption Standard,是一种对称加密算法,也被称为Rijndael算法,用于加密和解密数据。它在数据传输、文件加密和网络安全等领域有广泛的应用。
3.2.2 AES加解原理
AES算法采用对称加密算法进行加密和解密,使用相同的密钥进行加密和解密。对称加密算法是一种加密和解密使用相同密钥的加密算法。AES算法使用相同的密钥进行加密和解密,因此它是一种对称加密算法。
加密过程:
-
首先,需要选择一个密钥,这个密钥用于加密和解密数据;
-
将需要加密的数据分成若干个块,每个块的大小为128位;
-
对每个块进行加密,加密过程采用AES算法进行;
-
将加密后的块按顺序拼接起来,形成加密后的数据;
解密过程:
-
首先,需要选择一个密钥,这个密钥用于加密和解密数据;
-
将加密后的数据分成若干个块,每个块的大小为128位;
-
对每个块进行解密,解密过程采用AES算法进行;
-
将解密后的块按顺序拼接起来,形成解密后的数据;
AES算法采用不同的密钥长度,包括128位、192位和256位。这些密钥长度的选择可以根据需要和安全性要求进行调整。
3.2.3 AES算法优缺点
AES加密算法具有如下优点:
-
安全性高:AES算法是一种可靠的加密算法,在数据传输、文件加密和网络安全等领域有广泛的应用;
-
效率高:AES算法采用对称加密算法进行加密和解密,使用相同的密钥进行加密和解密。对称加密算法比非对称加密算法更加高效,因此AES算法具有更高的效率;
-
应用广泛:AES算法在数据传输、文件加密和网络安全等领域有着广泛的应用。在数据传输过程中,AES算法可以对数据进行加密,保护数据的安全性。在文件加密过程中,AES算法可以对文件进行加密,保护文件的安全性。在网络安全领域,AES算法可以对网络数据进行加密,保护网络的安全性;
AES算法缺点:
-
加密和解密的速度较慢,需要消耗大量的计算资源;
-
对于大文件的加密和解密操作,可能会导致内存不足的问题;
-
对于一些特定的攻击方式,如侧信道攻击等,可能会导致AES算法的安全性受到影响;
-
密钥管理较为困难,密钥的生成和分发需要耗费大量的时间和资源;
-
对于数据的完整性和认证等方面的保护能力较弱,需要结合其他的算法来实现更全面的保护;
3.2.4 AES算法使用场景
AES具有广泛的使用场景,如下列举了常用的使用场景:
-
网络通信:AES可用于保护网络通信中的敏感数据,如HTTPS协议中使用TLS/SSL加密传输数据。
-
数据库存储:AES常用于对数据库中敏感数据进行加密,以保护数据在存储和备份过程中的安全性。
-
文件和文件夹加密:AES可以用于对文件和文件夹进行加密,确保敏感数据在存储介质上的安全性。
-
电子邮件加密:AES可以用于对电子邮件内容和附件进行加密,防止邮件被未授权的人读取。
-
移动设备和应用程序:AES被广泛用于保护移动设备上的数据,如手机、平板电脑等,以及移动应用程序中的敏感数据。
-
虚拟私人网络(VPN):AES常用于VPN连接的数据加密,确保远程访问和数据传输的安全性。
-
加密货币和区块链:AES算法在加密货币和区块链技术中起到了重要的作用,用于保护数字资产的安全性和隐私性。
-
云计算和数据中心:AES可用于对云计算和数据中心中的敏感数据进行加密,确保数据在传输和存储过程中的安全性。
3.3 RSA加密算法
3.3.1 RSA加密算法介绍
RSA(Rivest-Shamir-Adleman)是一种非对称加密算法,由Ron Rivest、Adi Shamir和Leonard Adleman在1977年共同提出。RSA算法的安全性基于大数分解的困难性,即将一个大的合数分解为其质数因子的乘积。
所谓非对称加密算法,即加密和解密使用不同的密钥。RSA算法的加密和解密密钥是一对,一个是公钥,另一个是私钥。公钥可被任何人使用加密消息,但只有私钥的持有者才能解密消息。
3.3.2 RSA加密算法原理
RSA算法主要包括以下步骤:
-
密钥生成:选择两个大素数p和q,计算它们的乘积n=pq,以及一个指示位数的正整数e。然后计算d,使得(ed) mod ((p-1)*(q-1)) = 1。最终生成公钥为(n, e),私钥为(n, d);
-
加密:将明文分组成较小的块,并使用公钥(n, e)中的公钥指数e对每个块进行加密。加密操作是对明文m执行加密操作得到密文c,其中c = m^e mod n;
-
解密:接收到密文c的接收者使用私钥(n, d)中的私钥指数d对密文进行解密。解密操作是对密文c执行解密操作得到原始明文m,其中m = c^d mod n;
结合上图,可以得到使用非对称加密算法进行加解密的完整流程如下。
加密流程:
-
将明文m转换为整数M;
-
使用公钥(n,e)进行加密,计算密文C = M^e(mod n);
-
将密文C发送给接收方;
解密流程:
-
接收方使用私钥(n,d)进行解密,计算明文M = C^d(mod n);
-
将明文M转换为字符串,即为原始明文m;
在加密过程中,明文m首先被转换为整数M,然后使用公钥进行加密,得到密文C。在解密过程中,接收方使用私钥进行解密,得到明文M,然后将明文M转换为字符串,即为原始明文m。
RSA算法中,加密和解密使用的密钥是不同的。公钥可以公开,任何人都可以使用来加密消息。私钥必须保密,只有私钥的持有者才能解密消息。这种非对称加密的方式可以保证通信的安全性。
3.3.3 RSA算法优缺点
RSA算法优点:
-
安全性高:RSA算法基于大数分解的困难性,使得攻击者很难破解密文。因为大数分解是一种计算量极大的数学问题,即使使用最先进的计算机和算法,也需要花费很长的时间才能破解密文。
-
非对称加密:RSA算法采用非对称加密方式,可以保证通信的安全性。非对称加密是一种使用不同的密钥进行加密和解密的加密方式,公钥可以公开,任何人都可以使用它来加密消息,但只有私钥的持有者才能解密消息。这种非对称加密的方式可以保证通信的安全性。
-
数字签名:RSA算法可以用于数字签名,可以保证消息的完整性和真实性。数字签名是一种用于验证消息来源和完整性的技术,RSA算法可以用于生成数字签名,保证签名的真实性和不可伪造性。
-
算法公开:RSA算法是一种公开的加密算法,任何人都可以使用它进行加密和解密。算法的公开性可以促进技术的发展和应用,也可以避免算法被滥用和误用。
RSA算法缺点:
-
计算量大:RSA算法的加密和解密计算量很大,特别是在处理大数时,会消耗大量的时间和计算资源。因为RSA算法的安全性依赖于密钥长度,密钥长度越长,加密和解密的计算量也就越大。
-
密钥管理:RSA算法需要管理公钥和私钥,保证私钥的安全性和公钥的正确性是很重要的。如果私钥泄露,就会导致通信的安全性受到威胁,如果公钥被篡改,就会导致消息的真实性和完整性受到威胁。
-
明文长度限制:RSA算法对明文的长度有限制,一般不能超过密钥长度减去一定的安全边界。这是因为RSA算法采用的是模运算,明文长度超过一定的范围,就会导致模运算的结果不准确,从而影响加密和解密的正确性。
-
选择合适的密钥长度:RSA算法的安全性依赖于密钥长度,密钥长度越长,安全性越高,但计算量也越大,需要在安全性和效率之间做出权衡。选择合适的密钥长度是一项关键的工作,需要根据具体的应用场景和安全需求来确定。
3.3.4 RSA算法应用场景
RSA加密算法在许多领域和场景中都有着广泛应用,其中包括下面这些场景:
-
数据传输加密:RSA可用于保护敏感数据在传输过程中的安全性,特别是在网络通信、电子邮件和即时通讯等场景中。
-
数字签名:RSA可用于生成和验证数字签名,确保数据的完整性、身份验证和防止篡改。
-
身份认证:RSA可用于用户身份认证,如登录过程中的密码加密和解密。
-
虚拟私人网络(VPN):RSA可以用于VPN连接中的密钥交换和身份验证,确保远程访问和数据传输的安全性。
-
数字证书和SSL/TLS:RSA可以用于生成和管理数字证书,用于建立安全的SSL/TLS连接,例如用于安全的网站访问。
-
数据库加密:RSA可用于保护数据库中存储的敏感数据,确保数据在存储和备份过程中的安全性。
-
数字货币和区块链:RSA算法在加密货币和区块链技术中起到了重要的作用,用于生成和验证数字签名以及保护数字资产的安全性。
-
移动设备应用程序:RSA可用于移动设备应用程序中的数据加密和数字签名,确保敏感数据在移动设备上的安全性。
总之,RSA加密算法适用于许多需要数据保护和身份认证的场景,特别是在网络通信、数据存储和移动设备等领域中得到广泛应用。
3.4 JWT算法
3.4.1 JWT是什么
JWT (JSON Web Tokens): JWT是一种基于JSON格式的令牌,用于在客户端和服务器之间传递安全信息。它使用基于密钥的签名算法(如HMAC-SHA256)或公钥/私钥对来验证和保护数据的完整性。
通俗地说,JWT的本质就是一个不规则字符串,它可以将用户相关信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改。
3.4.2 JWT算法特点
去中心,无状态成为很多系统在认证设计时的一个重要考虑因素,而选择jwt作为认证方案正是考虑了这个特性。具体来说,如下:
-
可扩展性好,JWT的载荷(Payload)可以自定义,可以根据需要添加额外的信息。这使得JWT非常灵活,并且可以适应各种场景和需求。
-
无状态性,由于JWT中包含了所有必要的信息,服务器不需要在后端存储任何会话数据。每次请求都可以独立验证JWT的完整性和有效性,因此实现了无状态的身份认证。
-
安全性,JWT使用密钥对令牌进行签名,以确保令牌的完整性和真实性。只有拥有正确密钥的服务器才能生成和验证JWT,防止令牌被篡改和伪造。
-
自包含性,JWT中包含了所有必要的信息,因此可以避免频繁地查询数据库或缓存来获取用户信息。载荷中的声明(Claims)可以存储用户身份、权限和其他相关信息,减少服务器的负载。
-
跨平台支持,JWT是基于JSON规范的,因此可以在不同的编程语言和平台之间轻松传输和解析。这使得JWT成为一种通用的身份认证和授权机制。
-
分布式系统支持,由于JWT的无状态性和自包含性,它很适合于构建分布式系统和微服务架构。每个服务都可以独立验证JWT,并使用其中的信息进行身份认证和授权。
-
适用于单点登录,JWT可以用于实现单点登录(SSO),用户只需要在一次身份验证后,就可以使用生成的令牌访问多个应用程序和服务。
需要注意的是,JWT并不适合存储敏感数据,因为它的内容可以被解码。因此,在使用JWT时,需要避免将敏感信息存储在JWT的载荷中,或者对敏感信息进行额外的加密处理。另外,密钥管理也是使用JWT时需要特别关注的方面,保证密钥的安全性和合理的密钥轮换机制。
3.4.3 JWT工作原理
JWT的工作流程通常如下:
-
客户端使用身份验证信息向服务器发送请求;
-
服务器验证身份验证信息,并生成一个JWT,并用密钥对JWT进行签名;
-
服务器将JWT作为响应的一部分发送给客户端;
-
客户端将JWT保存,以便在后续的请求中发送给服务器进行身份验证;
-
服务器接收到带有JWT的请求,验证JWT的签名,并读取其中的信息来确认用户身份和权限;
如下是jwt在实际开发使用中完整的工作流程,以token机制为例进行说明:
-
首先,客户端(一般是前端)通过Web表单发起请求,调用登录认证API,传入用户名何密码;
-
后端校验用户名和密码通过后,将用户其他信息作为 JWT-Payload(负载),将其与头部分别进行Base64编码拼接后签名形成一个JWT,形成的JWT就是一个形同111.zzz.xxx的字符串;
-
后端将JWT字符串作为登录成功的返回结果返回给前端,前端将返回结果进行存储,退出登录时前端删除保存的JWT;
-
登录成功后,前端每次请求时,都会将JWT信息通过HTTP Header中的Authorization传递给后端;
-
后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确,检查Token是否过期,检查Token的接收方是否是自己;
-
验证通过后,后端使用JWT中包含的用户信息进行其他逻辑操作,返回结果;
3.4.4 JWT优点
JWT具备如下优点:
-
无状态:由于JWT包含了所有必要的信息,服务器不需要保存任何会话数据,可以实现无状态的身份验证。
-
可扩展性:JWT的声明(Claims)可以自定义,可以根据需要添加额外的信息。
-
安全性:JWT使用密钥对令牌进行签名,可以防止被篡改和伪造。
然而,需要注意的是,在使用JWT时,必须确保密钥的安全性。泄露密钥可能导致被恶意使用者篡改令牌或伪造令牌。因此,密钥的生成、存储和管理至关重要。
3.5 OAuth 2.0
3.5.1 OAuth2.0 介绍
严格来说,OAuth 2.0并不是一种具体的算法,而是一种用于授权的开放标准。它允许用户向第三方应用程序提供对其资源的有限访问权限,而无需共享其登录凭据。像我们平时使用12306购票付款时,弹出一个微信或支付宝付款的界面,这个操作以及后面的动作就涉及到OAuth2.0的相关内容。
OAuth 2.0是一个授权框架(Authorization Framework),用于允许用户通过第三方应用程序(客户端)访问受保护的资源,而无需直接公开他们的用户名和密码。OAuth 2.0的设计目标是简化和统一授权流程,同时提供更好的安全性和可扩展性。
3.5.2 OAuth2.0 流程
OAuth 2.0的工作流程通常如下:
-
客户端向授权服务器发起请求,请求访问某个资源。
-
授权服务器验证资源所有者的身份,如果验证成功,则颁发访问令牌给客户端。
-
客户端使用获得的访问令牌来请求访问资源服务器。
-
资源服务器验证访问令牌的有效性,如果有效,则返回受保护的资源给客户端。
以SpringSecurity为例,该框架遵循OAuth2.0协议,提供了多种安全认证方式,可以用来在企业级项目中集成使用,在下面是一张在实际项目中利用SpringSecurity+JWT 身份验证及动态权限解决方案的完整流程;
3.5.3 OAuth2.0 授权类型
OAuth 2.0中有四种不同的授权流程(Grant Types),如下:
-
授权码模式(Authorization Code Grant):适用于Web应用程序,并且可以在安全的服务器端进行授权流程。
-
密码模式(Password Grant):适用于受信任的客户端,并且资源所有者能够直接提供其凭据。
-
简化模式(Implicit Grant):适用于无法在客户端保持机密信息的情况下,比如JavaScript应用程序或移动应用程序。
-
客户端凭证模式(Client Credentials Grant):适用于客户端自身需要访问受保护资源,而不代表特定的用户。
3.5.4 OAuth2.0 核心角色
OAuth 2.0的核心概念中主要包括以下角色:
-
资源所有者(Resource Owner):资源所有者是指授权访问自己资源的用户,通常是终端用户。
-
客户端(Client):客户端是第三方应用程序,代表资源所有者请求访问受保护的资源。客户端可以是Web、移动应用程序或后端服务。
-
授权服务器(Authorization Server):授权服务器是负责验证资源所有者并颁发访问令牌(Access Token)的服务器。它可以是独立的服务或与身份提供者(如Google、Facebook等)结合在一起。
-
资源服务器(Resource Server):资源服务器是存储和提供受保护资源的服务器,它通过验证访问令牌来控制资源的访问权限。
3.5.5 OAuth2.0 优点
OAuth 2.0具备如下优点:
-
简化用户身份验证:资源所有者不需要直接提供密码给第三方应用程序,提高了安全性。
-
提供可控制的授权:资源所有者可以选择授权给特定的客户端,并定义每个客户端的授权范围。
-
适用于多种应用场景:OAuth 2.0可以适用于Web、移动应用程序和后端服务等多种应用场景。
-
支持可扩展性:OAuth 2.0是一种开放标准,可以根据具体需求进行定制和扩展。
-
需要注意的是,正确实施OAuth 2.0需要谨慎处理访问令牌的生命周期和安全性,并且在开发过程中遵循最佳实践,以确保应用程序的安全性和用户的隐私保护。
3.6 数字签名
数字签名算法是一种用于验证和保护数据完整性的加密技术。它通过将数据进行哈希处理,然后使用私钥对哈希值进行加密生成数字签名。接收方可以使用发送方的公钥对数字签名进行解密验证,确保数据的完整性和来源的真实性。
3.6.1 场景分析一
如下是一个数据传输的场景,A与B通信,A向B发送消息,过程如下:
-
B向A公布自己的公钥;
-
A使用B的公钥对需要发送的数据进行加密;
-
B收到A发送的加密数据,使用自己的私钥进行解密;
在这个A–>B发送消息的过程中,即使A的消息以及B的公钥都被截获,仍旧无法获取A的真实内容,因为私钥是无法被获取到的。
在此场景下,保证了消息的安全性,但有一个前提就是,B想接收的就是A的消息。如果此时有一名黑客C,使用B公布的公钥伪造假消息发送给B,或者截获A的消息,然后篡改消息,B是无法鉴别的,因此,数字签名就有必要了。
数字签概念
使用私钥进行加签, 使用公钥进行验签,为了防止消息被篡改,通常采用具有不可逆与高抗碰撞性的算法(常用的如rsa-sha256)对信息hash运算生成一个hash字符串,因为该hash函数具有不可逆性,所以由结果不能反推出原始信息,同时,由于使用的hash函数具有高抗碰撞性,所以不同的信息经过hash之后得到的结果基本上不会相同,所以可以通过此特性来验证信息是否被篡改过。
3.6.2 场景分析二
如下图,A向B发送消息,黑客C也向B发送假消息,因为B只想要A的消息,所以过程如下:
-
A公布自己的公钥,B获取A的公钥;
-
A使用自己的私钥对消息进行加签(同时,该消息内容也可以使用数据加密),然后将签名以及消息同时发送给B;
-
B使用A的公钥对签名进行解签,解析处理来的内容与实际的消息内容一样,可以证明该消息确实是A发送的;
在此过程中,如果C向B发送伪造的消息,B使用A的公钥对伪造消息的签名进行验签,一定是不通过的。
综上,数据加密和数字签名各司其责,一个保证数据安全,一个保证数据正确,所以在消息传递过程中,为了保证安全性,可以考虑使用两者。
3.6.3 数字签名流程
数字签名的实现有多种方案,以rsa作为数字签名算法来说,其实现的流程步骤如下:
-
生成密钥对:发送方生成一对密钥,包括私钥和公钥。私钥用于签名数据,公钥用于验证签名。
-
数据哈希:发送方对要传输的数据进行哈希处理,生成固定长度的哈希值。
-
签名生成:发送方使用私钥对哈希值进行加密,生成数字签名。
-
签名传输:发送方将数字签名和原始数据一起传输给接收方。
-
签名验证:接收方使用发送方的公钥对数字签名进行解密,得到哈希值。
-
数据验证:接收方对接收到的数据再次进行哈希处理,与解密得到的哈希值进行比对,来验证数据的完整性和真实性。
常见的数字签名算法包括RSA、DSA、ECDSA等,它们各自有不同的特点和用途。在实际应用中,数字签名算法被广泛用于保护数据的安全和完整性,如数字证书、电子邮件加密、电子商务等领域。
四、常用加密算法使用
接下来结合实际案例对上面提到的几种常用的加密算法进行详细的说明。
4.1 MD5加密算法使用
在java中,使用md5进行加密有多种方式,下面介绍几种常用的方式
方式一:Java自身包实现
public static String md51(String str) {
byte[] secretBytes = null;
try {
secretBytes = MessageDigest.getInstance("md5").digest(
str.getBytes());
} catch (Exception e) {
throw new RuntimeException("没有这个md5算法!");
}
String md5code = new BigInteger(1, secretBytes).toString(16);
for (int i = 0; i < 32 - md5code.length(); i++) {
md5code = "0" + md5code;
}
return md5code;
}
方法式:使用apache加密包commons-codec
需要在pom中添加如下依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
完整代码
/**
* md5加密二
*
* @param plainText
* @return
*/
public static String md52(String plainText) {
try {
// md5加密方法使用规则
return DigestUtils.md5Hex(plainText.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
方式三:使用hutool包提供的MD5加密
依赖包
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
加密代码
/**
* md5加密三
*
* @param str
* @return
*/
public static String md53(String str) {
return SecureUtil.md5(str);
}
测试一下,可以看到对同样待加密的字符串得到加密后的结果是一样的
4.2 AES加密算法使用
AES算法采用不同的密钥长度,包括128位、192位和256位。这些密钥长度的选择可以根据需要和安全性要求进行调整。
以AES 256来说,AES 256表示使用256位的密钥长度,这是目前最安全的AES密钥长度。AES 256提供了更高的安全性和更强的加密能力,适用于对敏感数据进行保护。
加密过程中,原始数据通过AES算法和密钥进行加密,生成密文。解密过程中,密文通过相同的AES算法和密钥进行解密,恢复为原始数据。
AES 256密钥的加密/解密可以在Java中通过javax.crypto包中的Cipher类来实现。以下是一段使用AES加解密的完整代码。
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
/**
* AES 是一种可逆加密算法,对用户的敏感信息加密处理
* 对原始数据进行AES加密后,在进行Base64编码转化;
*/
public class AESOperator {
/*
* 加密用的Key 可以用26个字母和数字组成
* 此处使用AES-128-CBC加密模式,key需要为16位。
*/
private String sKey = "0123456789abcdef";
private String ivParameter = "0123456789abcdef";
private static AESOperator instance = null;
private AESOperator() {
}
public static AESOperator getInstance() {
if (instance == null)
instance = new AESOperator();
return instance;
}
// 加密
public String encrypt(String sSrc) throws Exception {
Cipher cipher = Cipher.getInstance("AES / CBC / PKCS5Padding");
byte[] raw = sKey.getBytes();
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
//使用CBC模式,需要一个向量iv,可增加加密算法的强度
IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(sSrc.getBytes("utf-8"));
//此处使用BASE64做转码。
return new BASE64Encoder().encode(encrypted);
}
// 解密
public String decrypt(String sSrc) throws Exception {
try {
byte[] raw = sKey.getBytes("ASCII");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
//先用base64解密
byte[] encrypted1 = new BASE64Decoder().decodeBuffer(sSrc);
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original, "utf-8");
return originalString;
} catch (Exception ex) {
return null;
}
}
public static void main(String[] args) throws Exception {
// 需要加密的字串
String originPwd = "Aes@321";
System.out.println("原始密码串:" + originPwd);
// 加密
long lStart = System.currentTimeMillis();
String enString = AESOperator.getInstance().encrypt(originPwd);
System.out.println("加密后的字串是:" + enString);
long lUseTime = System.currentTimeMillis() - lStart;
System.out.println("加密耗时:" + lUseTime + "毫秒");
// 解密
lStart = System.currentTimeMillis();
String DeString = AESOperator.getInstance().decrypt(enString);
System.out.println("解密后的字串是:" + DeString);
lUseTime = System.currentTimeMillis() - lStart;
System.out.println("解密耗时:" + lUseTime + "毫秒");
}
}
运行结果
4.3 RSA加密算法使用
在上文中,我们详细介绍了RSA的加解密流程,利用RSA的加解密算法,可以在项目的认证登录中使用,具体的实现思路主要包括下面几点:
-
提供一个生成公钥的接口给前端使用,前端首先调用该接口拿到公钥;
-
在上一步中服务端保存公钥与私钥(可以存储在redis中);
-
前端使用公钥何相同的RSA算法,对登录账户的密码进行加密,得到加密串;
-
服务器接收前端的账户何密码,使用私钥对密码进行解密;
下面的代码完整描述了上面的过程
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
/**
* RSA工具类 默认长度为2048位
*/
@Slf4j
public class RSAUtil {
private static final int DEFAULT_RSA_KEY_SIZE = 2048;
private static final String KEY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "MD5withRSA";
public static void main(String [] args) throws Exception{
Map<String,String> result = generateRsaKey(DEFAULT_RSA_KEY_SIZE);
String publicKey = result.get("publicKey");
System.out.println("本次的公钥为:"+publicKey);
String privateKey = result.get("privateKey");
System.out.println("本次的私钥为:"+publicKey);
String pwd = "Aes@111";
System.out.println("原始密码为:"+pwd);
//1、获取公钥
//2、公钥加密,字符串加密
String encryptPwd = encrypt(pwd, publicKey);
System.out.println("加密后的密码为:" + encryptPwd);
//3、私钥解密
String decryptPwd = decrypt(encryptPwd, privateKey);
System.out.println("解密后的密码为:" + decryptPwd);
}
/**
* 生成RSA 公私钥,可选长度为1025,2048位.
*/
public static Map<String,String> generateRsaKey(int keySize) {
Map<String,String> result = new HashMap<>(2);
try {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
// 初始化密钥对生成器,密钥大小为1024 2048位
keyPairGen.initialize(keySize, new SecureRandom());
// 生成一个密钥对,保存在keyPair中
KeyPair keyPair = keyPairGen.generateKeyPair();
// 得到公钥字符串
result.put("publicKey", new String(Base64.encodeBase64(keyPair.getPublic().getEncoded())));
// 得到私钥字符串
result.put("privateKey", new String(Base64.encodeBase64(keyPair.getPrivate().getEncoded())));
} catch (GeneralSecurityException e) {
e.printStackTrace();
}
return result;
}
/**
* RSA私钥解密
* @param str 解密字符串
* @param privateKey 私钥
* @return 明文
*/
public static String decrypt(String str, String privateKey) {
//64位解码加密后的字符串
byte[] inputByte;
String outStr = "";
try {
inputByte = Base64.decodeBase64(str.getBytes("UTF-8"));
//base64编码的私钥
byte[] decoded = Base64.decodeBase64(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
//RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
outStr = new String(cipher.doFinal(inputByte));
} catch (UnsupportedEncodingException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
return outStr;
}
/**
* RSA公钥加密
* @param str 需要加密的字符串
* @param publicKey 公钥
* @return 密文
* @throws Exception 加密过程中的异常信息
*/
public static String encrypt(String str, String publicKey) throws Exception {
//base64编码的公钥
byte[] decoded = Base64.decodeBase64(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
//RSA加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
String outStr = Base64.encodeBase64String(cipher.doFinal(str.getBytes("UTF-8")));
return outStr;
}
}
运行上面的代码,可以看到预期的效果
4.4 JWT加密算法使用
4.4.1 JWT构成
JWT(JSON Web Tokens)是一种基于JSON格式的令牌,用于在客户端和服务器之间传递安全信息。它由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
JWT的结构形式如下:xxxxx.yyyyy.zzzzz
头部(Header)
头部通常由两个部分组成:令牌类型(通常是JWT)和采用的加密算法(如HMAC-SHA256或RSA)。头部使用Base64 URL安全编码进行编码。
载荷(Payload)
载荷是JWT的主要内容,包含了被称为声明(Claims)的信息。声明分为三种类型:注册声明(Registered Claims)、公共声明(Public Claims)和私有声明(Private Claims)。常见的注册声明包括发行人(Issuer)、主题(Subject)、受众(Audience)、过期时间(Expiration Time)等。载荷同样使用Base64 URL安全编码进行编码。
签名(Signature)
签名是使用密钥对头部和载荷进行加密后的字符串,用于验证JWT的完整性和真实性。签名可以防止信息被篡改。签名使用头部中指定的加密算法进行计算。
JWT的工作流程通常如下:
-
客户端使用身份验证信息向服务器发送请求;
-
服务器验证身份验证信息,并生成一个JWT,并用密钥对JWT进行签名;
-
服务器将JWT作为响应的一部分发送给客户端;
-
客户端将JWT保存,以便在后续的请求中发送给服务器进行身份验证;
-
服务器接收到带有JWT的请求,验证JWT的签名,并读取其中的信息来确认用户身份和权限;
4.4.2 操作演示
引入jwt依赖,版本可以根据自己的情况选择
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
生成jwt令牌(token)
public static void main(String[] args) {
createToken();
}
public static void createToken() {
Map<String, Object> map = new HashMap<>();
Calendar instance = Calendar.getInstance();
//默认3000S过期
instance.add(Calendar.SECOND,3000);
String token = JWT.create()
.withHeader(map) //header,可以不写
.withClaim("userId", 21) //payload
.withClaim("username", "mike") //payload
.withClaim("mobile", "13325532123") //payload
.withExpiresAt(instance.getTime()) //设置过期时间
.sign(Algorithm.HMAC256("!ISN!@#¥%")); //签名
System.out.println(token);
}
运行这段代码,控制台可以看到生成了一长串加密后的字符串,细心的同学不难发现,这串字符串正是按照上面的xxxxx.yyyyy.zzzzz格式组装的,即对应着一个JWT token的三部分信息;
解析jwt令牌(token)
public static void parseToken(String token) {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!ISN!@#¥%")).build();
DecodedJWT verify = jwtVerifier.verify(token);
String userId = verify.getClaim("userId").asString();
String userName = verify.getClaim("username").asString();
String mobile = verify.getClaim("mobile").asString();
System.out.println("userId:"+userId);
System.out.println("userName:"+userName);
System.out.println("mobile:"+mobile);
}
运行上面的代码,可以看到经过解密,可以得到加密之前的相关字段信息
4.5 数字签名使用
在上文详细介绍了使用数字签名的过程和场景,下面通过示例来看下如何使用数字签名,以tsa算法为例,本次案例用于签名验证,使用私钥用于数字签名,公钥用于验证签名的有效性。
下面是一段完整的代码,代码的核心逻辑为:
-
生成公钥和私钥;
-
利用私钥对待传输数据进行加签;
-
利用公钥以及传入参数对数据进行验签;
import com.congge.entity.RsaEntity;
import org.apache.tomcat.util.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class PluRsaUtil {
private static final Logger logger = LoggerFactory.getLogger(RSAUtil.class);
public static void main(String[] args) {
RsaEntity entity = new RsaEntity();
// 使用RSA算法生成 公钥与私钥, 生成的公私钥 是一一对应的。
createRSAKey(entity);
String body = "123456";
String body2 = "12345";
// 将入参数据以及私钥进行数字加签
String sign = sign(body, entity.getPrivateKey());
// 根据入参数据以及公钥进行验证签名,若入参数据body被修改或者秘钥不正确都会导致验签失败;例如加签使用body,验签使用body2则导致验签失败
boolean verifyFlag = verify(body2,entity.getPublicKey(), sign);
if (verifyFlag) {
logger.info("验签成功");
} else {
logger.info("验签失败");
}
}
/**
* 生成对应的 与我通信的公钥和私钥
* @return
*/
public static void createRSAKey(RsaEntity entity) {
try {
// 创建KeyPairGenerator 指定算法为RSA,用于生成对应的公钥和私钥
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// 指定字节长度
keyPairGenerator.initialize(1024);
// 秘钥生成器
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 公钥
RSAPublicKey publicKey = (RSAPublicKey)keyPair.getPublic();
// 进行Base64编码存入
String clientPublicKey = Base64.encodeBase64String(publicKey.getEncoded());
logger.info("生成的clientPublicKey是: {}", clientPublicKey);
entity.setPublicKey(clientPublicKey);
// 私钥
RSAPrivateKey privateKey = (RSAPrivateKey)keyPair.getPrivate();
// 进行Base64编码存入
String clientPrivateKey = Base64.encodeBase64String(privateKey.getEncoded());
logger.info("生成的clientPrivateKey是: {}", clientPrivateKey);
entity.setPrivateKey(clientPrivateKey);
} catch (Exception e) {
logger.error("生成秘钥失败");
e.printStackTrace();
}
}
/**
* 利用私钥信息生成数字签名
* @param data 入参数据body
* @param privateKey 私钥
* @return
*/
public static String sign(String data, String privateKey) {
try {
// 入参数据body字节数组
byte[] dataBytes = data.getBytes();
// 获取私钥秘钥字节数组
byte[] keyBytes = Base64.decodeBase64(privateKey);
// 使用给定的编码密钥创建一个新的PKCS8EncodedKeySpec。
// PKCS8EncodedKeySpec 是 PKCS#8标准作为密钥规范管理的编码格式
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
// 实例化KeyFactory,指定为加密算法 为 RSA
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// 获得PrivateKey对象
PrivateKey privateKey1 = keyFactory.generatePrivate(keySpec);
// 用私钥对信息生成数字签名,指定签名算法为 MD5withRSA
Signature signature = Signature.getInstance("MD5withRSA");
// 初始化签名
signature.initSign(privateKey1);
// 数据body带入
signature.update(dataBytes);
// 对签名进行Base64编码
return Base64.encodeBase64String(signature.sign());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 利用公钥校验数字签名
* @param data 入参数据body
* @param publicKey 公钥
* @param sign 签名
* @return
*/
public static boolean verify(String data, String publicKey, String sign) {
try {
// 入参数据body字节数组
byte[] dataBytes = data.getBytes("UTF-8");
// 获取公钥秘钥字节数组
byte[] keyBytes = Base64.decodeBase64(publicKey);
// 使用给定的编码密钥创建一个新的X509EncodedKeySpec
// X509EncodedKeySpec是基于X.509证书提前的公钥,一种java秘钥规范
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyBytes);
// 实例化KeyFactory,指定为加密算法 为 RSA
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// 获取publicKey对象
PublicKey publicKey1 = keyFactory.generatePublic(x509EncodedKeySpec);
// 用私钥对信息生成数字签名,指定签名算法为 MD5withRSA
Signature signature = Signature.getInstance("MD5withRSA");
// 带入公钥进行验证
signature.initVerify(publicKey1);
// 数据body带入
signature.update(dataBytes);
// 验证签名
return signature.verify(Base64.decodeBase64(sign));
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
五、写在文末
数据安全是所有的互联网产品在安全建设方面越来越重要的内容,而选择合适的加密算法对保障数据传入安全有着非常重要的意义,在实际业务中,需要结合实际情况进行衡量,并非越复杂的加密算法越好,综合权衡之后选择合适的加密算法,配合合理的数据传输方案设计,从而构筑起系统安全的稳固防线,本篇到此结束感谢观看。