Springboot功能模块之常用认证加密方案总结

一、背景

登录认证功能可以说是所有系统基本功能。比如在springboot构建的微服务工程中,大家熟悉的jwt技术,以及基于oauth 2.0协议的安全认证等,就是用于构建系统安全的基础技术框架。\n\n认证的目的是为了保护系统资源不被恶意使用,通常来说,最基础的认证方式即大家熟悉的用户名和密码。随着移动端应用的广泛使用,用户对登录认证的操作便捷性也提出了越来越高的要求,像微信扫扫码登录,手机号登录等方式都是适应时代发展的产物

二、 登录认证安全问题

在所有需要登录认证的系统中,首要考虑的问题包括下面两点:

  • 采用什么认证方式?是用户名+密码?微信扫码登录?还是手机短信验证码登录?
  • 如果是用户名+密码登录,密码采用什么方式进行加密?

认证方式选择

选择哪种方式作为系统的认证方案呢?这个没有统一的答案。目前主流的互联网产品从大类上可以分为C端和B端两类产品。继续细分下去,C端产品又可以分成多种类型。比如面向社交的产品,电商交易产品,音视频等。而B端产品一般面向企业,政府,银行,券商等业务,主要是企业级内部业务使用。

相对来说,面向C端的产品通常为了考虑用户的使用体验,以及快速占领市场为目标,会尽可能让登录更简单,减少用户的操作难度,这才有今天的扫码登录,短信验证码登录的模式。

而B端产品,更多的是业务型驱动,系统的使用人员通常是为了处理某些具体场景下的业务而保持对系统的长期使用,所以在登录认证方面,为了系统的安全性,适当牺牲一些性能和体验也能被接受。这种情况下,就可以考虑安全性更好的登录认证方案。

三、常用的加密方案

互联网产品发展到今天,为了拥抱用户端的需求和变化,逐渐出现很多种成熟的密码加密方案,下面就常用的密码加密方式展开说明。

3.1MD5加密算法

MD5概述

MD5,全称:Message Digest Algorithm 5,是一种常用的哈希算法,可用于将任意长度的数据转换为固定长度的哈希值(通常为128位,16个字节)。在早些年,MD5被广泛使用过。但现在它已经不再被视为安全的加密算法,因为它存在一些安全漏洞,如碰撞攻击和彩虹表攻击。
 

MD5加密具有如下特点:

  • 单向加密: 明文经过加密之后形成密文,无法通过密文反推出原始的明文。

  • 固定长度散列值: 无论输入了多长的数据,经过MD5处理后都只会产生一个16字节的散列值。

  • 不可逆性: 经过MD5处理后得到的散列结果无法计算出原始数据,因此MD5不可用于解密。

  • 运算速度快: MD5采用的是位运算,速度很快,几乎不占用CPU资源。

  • 安全性差: 1996年后该算法被证实存在弱点,可被加以破解。2004年证实MD5算法无法防止碰撞,因此不适用于安全性认证,如SSL公开密钥认证、数字签名等用途。2011年,RFC 6151禁止MD5用作密钥散列消息认证码。

MD5原理

MD5 本质上也是一种哈希算法,会输出一个16字节(128位)固定长度的散列值,该算法的大致流程如上图,其 算法原理,可简单描述为:MD5 码以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。

md5使用场景

尽管MD5的安全性方面相比其他主流加密算法稍显不足,但是经过多年的发展和沉淀,也有很多适用场景,下面列举了一些经典的使用场景。

密码存储

在许多系统中,用户的密码需要存储。而密码直接存储在数据库中是非常危险的,因此MD5算法可以将密码转为不可逆的摘要值再存储,即使数据库被攻击,被泄露的密码也无法被还原。

数字签名

数字签名是利用密钥将数据进行签名,以确保数据完整性和可靠性。MD5算法可以生成一个用于数字签名的摘要值,如果数据在传输过程中被篡改,摘要值也会发生改变,从而保证数据完整性。

文件校验

MD5算法可以对文件进行校验,确保文件没有被篡改或损坏。对于需保密的文件,可以用MD5算法对文件进行加密处理,只有经过相应计算才能打开文件。

软件验证

MD5算法可用于软件验证,比如网站的下载链接可以加密,下载后进行MD5值比较,可以确保文件的完整性和安全性。

3.2AES加密算法

AES简介

AES加密算法,全称Advanced Encryption Standard,是一种对称加密算法,也被称为Rijndael算法,用于加密和解密数据。它在数据传输、文件加密和网络安全等领域有广泛的应用。

AES加解原理

AES算法采用对称加密算法进行加密和解密,使用相同的密钥进行加密和解密。对称加密算法是一种加密和解密使用相同密钥的加密算法。AES算法使用相同的密钥进行加密和解密,因此它是一种对称加密算法。

加密过程:

  • 首先,需要选择一个密钥,这个密钥用于加密和解密数据;
  • 将需要加密的数据分成若干个块,每个块的大小为128位;
  • 对每个块进行加密,加密过程采用AES算法进行;
  • 将加密后的块按顺序拼接起来,形成加密后的数据;

解密过程:

  • 首先,需要选择一个密钥,这个密钥用于加密和解密数据;
  • 将需要加密的数据分成若干个块,每个块的大小为128位;
  • 对每个块进行解密,解密过程采用AES算法进行;
  • 将解密后的块按顺序拼接起来,形成解密后的数据;

AES算法采用不同的密钥长度,包括128位、192位和256位。这些密钥长度的选择可以根据需要和安全性要求进行调整。

AES算法优缺点

优点:

  • AES算法是一种可靠的加密算法,在数据传输、文件加密和网络安全等领域有广泛的应用;
  • 效率高:AES算法采用对称加密算法进行加密和解密,使用相同的密钥进行加密和解密。对称加密算法比非对称加密算法更加高效,因此AES算法具有更高的效率;
  • 应用广泛:AES算法在数据传输、文件加密和网络安全等领域有着广泛的应用。在数据传输过程中,AES算法可以对数据进行加密,保护数据的安全性。在文件加密过程中AES算法可以对文件进行加密,保护文件的安全性。在网络安全领域,AES算法可以对网络数据进行加密,保护网络的安全性;

缺点:

  • 加密和解密的速度较慢,需要消耗大量的计算资源;
  • 对于大文件的加密和解密操作,可能会导致内存不足的问题;
  • 对于一些特定的攻击方式,如侧信道攻击等,可能会导致AES算法的安全性受到影响;

  • 密钥管理较为困难,密钥的生成和分发需要耗费大量的时间和资源;

  • 对于数据的完整性和认证等方面的保护能力较弱,需要结合其他的算法来实现更全面的保护;

AES算法使用场景
  1. 网络通信:AES可用于保护网络通信中的敏感数据,如HTTPS协议中使用TLS/SSL加密传输数据。
  2. 数据库存储:AES常用于对数据库中敏感数据进行加密,以保护数据在存储和备份过程中的安全性。
  3. 文件和文件夹加密:AES可以用于对文件和文件夹进行加密,确保敏感数据在存储介质上的安全性。
  4. 电子邮件加密:AES可以用于对电子邮件内容和附件进行加密,防止邮件被未授权的人读取。
  5. 移动设备和应用程序:AES被广泛用于保护移动设备上的数据,如手机、平板电脑等,以及移动应用程序中的敏感数据。
  6. 虚拟私人网络(VPN):AES常用于VPN连接的数据加密,确保远程访问和数据传输的安全性。
  7. 加密货币和区块链:AES算法在加密货币和区块链技术中起到了重要的作用,用于保护数字资产的安全性和隐私性。
  8. 云计算和数据中心:AES可用于对云计算和数据中心中的敏感数据进行加密,确保数据在传输和存储过程中的安全性。

3.3 RSA加密算法

RSA加密算法介绍

RSA (Rivest-Shamir-Adleman) 是一种非对称加密算法,由Ron Rivest、 Adi Shamir和Leonard Adleman在1977年共同提出。RSA算法的安全性基于大数分解的困难性,即将一个大的合数分解为其质数因子的乘积。
所谓非对称加密算法,即加密和解密使用不同的密钥。RSA算法的加密和解密密钥是一对,一个是公钥,另一个是私钥。公钥可被任何人使用加密消息,但只有私钥的持有者才能解密消息。

RSA加密算法原理

RSA算法中,加密和解密使用的密钥是不同的。公钥可以公开,任何人都可以使用来加密消息。私钥必须保密,只有私钥的持有者才能解密消息。这种非对称加密的方式可以保证通信的安全性。

RSA算法优缺点

优点

  • 安全性高:RSA算法基于大数分解的困难性,使得攻击者很难破解密文。因为大数分解是一种计算量极大的数学问题,即使使用最先进的计算机和算法,也需要花费很长的时间才能破解密文。
  • 非对称加密:RSA算法采用非对称加密方式,可以保证通信的安全性。非对称加密是一种使用不同的密钥进行加密和解密的加密方式,公钥可以公开,任何人都可以使用它来加密消息,但只有私钥的持有者才能解密消息。这种非对称加密的方式可以保证通信的安全性。

  • 数字签名:RSA算法可以用于数字签名,可以保证消息的完整性和真实性。数字签名是一种用于验证消息来源和完整性的技术,RSA算法可以用于生成数字签名,保证签名的真实性和不可伪造性。

  • 算法公开:RSA算法是一种公开的加密算法,任何人都可以使用它进行加密和解密。算法的公开性可以促进技术的发展和应用,也可以避免算法被滥用和误用。

缺点

  • 计算量大:RSA算法的加密和解密计算量很大,特别是在处理大数时,会消耗大量的时间和计算资源。因为RSA算法的安全性依赖于密钥长度,密钥长度越长,加密和解密的计算量也就越大。

  • 密钥管理:RSA算法需要管理公钥和私钥,保证私钥的安全性和公钥的正确性是很重要的。如果私钥泄露,就会导致通信的安全性受到威胁,如果公钥被篡改,就会导致消息的真实性和完整性受到威胁。

  • 明文长度限制:RSA算法对明文的长度有限制,一般不能超过密钥长度减去一定的安全边界。这是因为RSA算法采用的是模运算,明文长度超过一定的范围,就会导致模运算的结果不准确,从而影响加密和解密的正确性。

  • 选择合适的密钥长度:RSA算法的安全性依赖于密钥长度,密钥长度越长,安全性越高,但计算量也越大,需要在安全性和效率之间做出权衡。选择合适的密钥长度是一项关键的工作,需要根据具体的应用场景和安全需求来确定。

RSA算法应用场景

RSA加密算法在许多领域和场景中都有着广泛应用,其中包括下面这些场景:

  • 数据传输加密:RSA可用于保护敏感数据在传输过程中的安全性,特别是在网络通信、电子邮件和即时通讯等场景中。
  • 数字签名:RSA可用于生成和验证数字签名,确保数据的完整性、身份验证和防止篡改。
  • 身份认证:RSA可用于用户身份认证,如登录过程中的密码加密和解密。
  • 虚拟私人网络(VPN):RSA可以用于VPN连接中的密钥交换和身份验证,确保远程访问和数据传输的安全性。
  • 数字证书和SSL/TLS:RSA可以用于生成和管理数字证书,用于建立安全的SSL/TLS连接,例如用于安全的网站访问。
  • 数据库加密:RSA可用于保护数据库中存储的敏感数据,确保数据在存储和备份过程中的安全性。
  • 数字货币和区块链:RSA算法在加密货币和区块链技术中起到了重要的作用,用于生成和验证数字签名以及保护数字资产的安全性。
  • 移动设备应用程序:RSA可用于移动设备应用程序中的数据加密和数字签名,确保敏感数据在移动设备上的安全性。

总之,RSA加密算法适用于许多需要数据保护和身份认证的场景,特别是在网络通信、数据存储和移动设备等领域中得到广泛应用。

3.4JWT算法

 JWT是什么

JWT (JSON Web Tokens): JWT是一种基于JSON格式的令牌,用于在客户端和服务器之间传递安全信息。它使用基于密钥的签名算法(如HMAC-SHA256)或公钥/私钥对来验证和保护数据的完整性。

通俗地说,JWT的本质就是一个不规则字符串,它可以将用户相关信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改。

官网地址: JSON Web Token Introduction - jwt.io

 JWT算法特点

去中心化和无状态性已经成为许多系统在设计认证时的重要考虑因素,因此选择JWT作为认证方案就是考虑到这一特性。具体来说,如下:

  1. 可扩展性好: JWT的载荷(Payload)可以自定义,可以根据需要添加额外的信息。这使得JWT非常灵活,并且可以适应各种场景和需求。

  2. 无状态性: 由于JWT中包含了所有必要的信息,服务器不需要在后端存储任何会话数据。每次请求都可以独立验证JWT的完整性和有效性,因此实现了无状态的身份认证。

  3. 安全性: JWT使用密钥对令牌进行签名,以确保令牌的完整性和真实性。只有拥有正确密钥的服务器才能生成和验证JWT,防止令牌被篡改和伪造。

  4. 自包含性: JWT中包含了所有必要的信息,因此可以避免频繁地查询数据库或缓存来获取用户信息。载荷中的声明(Claims)可以存储用户身份、权限和其他相关信息,减少服务器的负载。

  5. 跨平台支持: JWT是基于JSON规范的,因此可以在不同的编程语言和平台之间轻松传输和解析。这使得JWT成为一种通用的身份认证和授权机制。

  6. 分布式系统支持: 由于JWT的无状态性和自包含性,它很适合于构建分布式系统和微服务架构。每个服务都可以独立验证JWT,并使用其中的信息进行身份认证和授权。

  7. 适用于单点登录: JWT可以用于实现单点登录(SSO),用户只需要在一次身份验证后,就可以使用生成的令牌访问多个应用程序和服务。

需要注意的是,JWT并不适合存储敏感数据,因为它的内容可以被解码。因此,在使用JWT时,需要避免将敏感信息存储在JWT的载荷中,或者对敏感信息进行额外的加密处理。另外,密钥管理也是使用JWT时需要特别关注的方面,保证密钥的安全性和合理的密钥轮换机制。

Token概述

JWT工作原理

认证流程

  • 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  •  后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx的字符串。 token head.payload.singurater
  •  后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
  •  前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题) HEADER
  •  后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  • 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果.
JWT的结构是什么

token string ====> header.payload.singnature token

1.令牌组成

  • 1.标头(Header)
  • 2.有效载荷(Payload)
  • 3.签名(Signature)
  • 因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz Header.Payload.Signature

2.Header

  • 标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分。

  • 注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

{
  "alg": "HS256",
  "typ": "JWT"
}

3.Payload

  • 令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用 Base64 编码组成 JWT 结构的第二部分
  • 载荷是JWT的主要内容,包含了被称为声明(Claims)的信息。声明分为三种类型:注册声明(Registered Claims)、公共声明(Public Claims)和私有声明(Private Claims)。常见的注册声明包括发行人(Issuer)、主题(Subject)、受众(Audience)、过期时间(Expiration Time)等。载荷同样使用Base64 URL安全编码进行编码。

  • {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }

4.Signature

  • 前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过
  • 签名是使用密钥对头部和载荷进行加密后的字符串,用于验证JWT的完整性和真实性。签名可以防止信息被篡改。签名使用头部中指定的加密算法进行计算。

  • 如: HMACSHA256(base64UrlEncode(header) +"."+base64UrlEncode(payload),secret);

签名目的

  • 最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

信息安全问题

  • 在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?
  • 是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏   感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第   三方通过Base64解码就能很快地知道你的密码了。因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系 统,甚至实现Web应用的单点登录。

5.放在一起

  • 输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。
  • 简洁(Compact) 可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快
  • 自包含(Self-contained) 负载中包含了所有用户所需要的信息,避免了多次查询数据库

 

JWT应用

1. 授权
这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
2. 信息交换
JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。

JWT优点

JWT具备如下优点:

  • 无状态:由于JWT包含了所有必要的信息,服务器不需要保存任何会话数据,可以实现无状态的身份验证。

  • 可扩展性:JWT的声明(Claims)可以自定义,可以根据需要添加额外的信息。

  • 安全性:JWT使用密钥对令牌进行签名,可以防止被篡改和伪造。

  • 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快

  • 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库

  • 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。

  • 不需要在服务端保存会话信息,特别适用于分布式微服务。

然而,需要注意的是,在使用JWT时,必须确保密钥的安全性。泄露密钥可能导致被恶意使用者篡改令牌或伪造令牌。因此,密钥的生成、存储和管理至关重要。

3.5基于传统的会话技术

会话跟踪方案对比
session认证方式
  • 我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。 t

暴露问题
  • 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

  • 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

  • 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

  • 在前后端分离系统中就更加痛苦:如下图所示,也就是说前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session每次携带sessionid到服务器,服务器还要查询用户信息。同时如果用户很多。这些信息存储在服务器内存中,给服务器增加负担。还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是sessionid就是一个特征值,表达的信息不够丰富。不容易扩展。而且如果你后端应用是多节点部署。那么就需要实现session共享机制。不方便集群应用。

3.6OAuth 2.0

 OAuth2.0 介绍

严格来说,OAuth 2.0并不是一种具体的算法,而是一种用于授权的开放标准。它允许用户向第三方应用程序提供对其资源的有限访问权限,而无需共享其登录凭据。像我们平时使用12306购票付款时,弹出一个微信或支付宝付款的界面,这个操作以及后面的动作就涉及到OAuth2.0的相关内容。

OAuth 2.0是一个授权框架(Authorization Framework),用于允许用户通过第三方应用程序(客户端)访问受保护的资源,而无需直接公开他们的用户名和密码。OAuth 2.0的设计目标是简化和统一授权流程,同时提供更好的安全性和可扩展性。

OAuth2.0 流程

OAuth 2.0的工作流程通常如下:

  • 客户端向授权服务器发起请求,请求访问某个资源。
  • 授权服务器验证资源所有者的身份,如果验证成功,则颁发访问令牌给客户端。
  • 客户端使用获得的访问令牌来请求访问资源服务器。
  • 资源服务器验证访问令牌的有效性,如果有效,则返回受保护的资源给客户端。

以SpringSecurity为例,该框架遵循OAuth2.0协议,提供了多种安全认证方式,可以用来在企业级项目中集成使用,在下面是一张在实际项目中利用SpringSecurity+JWT身份验证及动态权限解决方案的完整流程;

OAuth2.0 授权类型

OAuth 2.0中有四种不同的授权流程(Grant Types),如下:

  • 授权码模式(Authorization Code Grant):适用于Web应用程序,并且可以在安全的服务器端进行授权流程。
  • 密码模式(Password Grant):适用于受信任的客户端,并且资源所有者能够直接提供其凭据。
  • 简化模式(Implicit Grant):适用于无法在客户端保持机密信息的情况下,比如JavaScript应用程序或移动应用程序。
  • 客户端凭证模式(Client Credentials Grant):适用于客户端自身需要访问受保护资源,而不代表特定的用户。
OAuth2.0 核心角色

OAuth 2.0的核心概念主要包括以下角色:

  • 资源所有者(Resource Owner):资源所有者是指授权访问自己资源的用户,通常是终端用户。

  • 客户端(Client):客户端是第三方应用程序,代表资源所有者请求访问受保护的资源。客户端可以是Web、移动应用程序或后端服务。

  • 授权服务器(Authorization Server):授权服务器是负责验证资源所有者并颁发访问令牌(Access Token)的服务器。它可以是独立的服务或与身份提供者(如Google、Facebook等)结合在一起。

  • 资源服务器(Resource Server):资源服务器是存储和提供受保护资源的服务器,它通过验证访问令牌来控制资源的访问权限。

OAuth2.0 优点

OAuth 2.0具备如下优点:

  • 简化用户身份验证:资源所有者不需要直接提供密码给第三方应用程序,提高了安全性。

  • 提供可控制的授权:资源所有者可以选择授权给特定的客户端,并定义每个客户端的授权范围。

  • 适用于多种应用场景:OAuth 2.0可以适用于Web、移动应用程序和后端服务等多种应用场景。

  • 支持可扩展性:OAuth 2.0是一种开放标准,可以根据具体需求进行定制和扩展。

需要注意的是,正确实施OAuth 2.0需要谨慎处理访问令牌的生命周期和安全性,并且在开发过程中遵循最佳实践,以确保应用程序的安全性和用户的隐私保护。

3.7数字签名

数字签名算法是一种用于验证和保护数据完整性的加密技术。它通过将数据进行哈希处理,然后使用私钥对哈希值进行加密生成数字签名。接收方可以使用发送方的公钥对数字签名进行解密验证,确保数据的完整性和来源的真实性。

数字签概念

使用私钥进行加签, 使用公钥进行验签,为了防止消息被篡改,通常采用具有不可逆与高抗碰撞性的算法(常用的如rsa-sha256)对信息hash运算生成一个hash字符串,因为该hash函数具有不可逆性,所以由结果不能反推出原始信息,同时,由于使用的hash函数具有高抗碰撞性,所以不同的信息经过hash之后得到的结果基本上不会相同,所以可以通过此特性来验证信息是否被篡改过。

数字签名流程

数字签名的实现有多种方案,以RSA作为数字签名算法来说,其实现的流程步骤如下:

- 生成密钥对:发送方生成一对密钥,包括私钥和公钥。私钥用于签名数据,公钥用于验证签名。

- 数据哈希:发送方对要传输的数据进行哈希处理,生成固定长度的哈希值。

- 签名生成:发送方使用私钥对哈希值进行加密,生成数字签名。

- 签名传输:发送方将数字签名和原始数据一起传输给接收方。

- 签名验证:接收方使用发送方的公钥对数字签名进行解密,得到哈希值。

- 数据验证:接收方对接收到的数据再次进行哈希处理,与解密得到的哈希值进行比对,来验证数据的完整性和真实性。

常见的数字签名算法包括RSA、DSA、ECDSA等,它们各自有不同的特点和用途。在实际应用中,数字签名算法被广泛用于保护数据的安全和完整性,如数字证书、电子邮件加密、电子商务等领域。

四、常用加密算法使用

接下来结合实际案例对上面提到的几种常用的加密算法进行详细的说明。

4.1JWT加密算法使用

在java中,使用md5进行加密有多种方式,下面介绍几种常用的方式

常见异常信息

  • SignatureVerificationException: 签名不一致异常
  • TokenExpiredException: 令牌过期异常
  • AlgorithmMismatchException: 算法不匹配异常
  • InvalidClaimException: 失效的payload异常

方式一:Java自身包实现

pom依赖

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
		<dependency>
			<groupId>com.auth0</groupId>
			<artifactId>java-jwt</artifactId>
			<version>4.4.0</version>
		</dependency>

自定义拦截器 JwtInterceptor

public class JwtInterceptor implements HandlerInterceptor {

    @Resource
    private UserMapper userMapper; // 用户映射器,用于查询用户信息

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("token"); // 从请求头中获取token
        if (StrUtil.isBlank(token)) {
            token = request.getParameter("token"); // 如果请求头中没有token,则从请求参数中获取
        }
        // 如果不是映射到方法直接通过
        if (handler instanceof HandlerMethod) {
            AuthAccess annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthAccess.class);
            if (annotation != null) {
                return true; // 如果方法上有AuthAccess注解,则直接通过
            }
        }
        // 执行认证
        if (StrUtil.isBlank(token)) {
            throw new ServiceException("401", "请登录"); // 如果token为空,则抛出异常,提示用户登录
        }
        // 获取 token 中的 user id
        String userId;
        try {
            userId = JWT.decode(token).getAudience().get(0); // 解码token,获取userid
        } catch (JWTDecodeException j) {
            throw new ServiceException("401", "请登录"); // 如果解码失败,则抛出异常,提示用户登录
        }
        // 根据token中的userid查询数据库
        User user = userMapper.selectById(userId); // 根据userid查询用户信息
        if (user == null) {
            throw new ServiceException("401", "请登录"); // 如果用户不存在,则抛出异常,提示用户登录
        }
        // 用户密码加签验证 token
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); // 创建JWT验证器,使用用户的密码作为密钥
        try {
            jwtVerifier.verify(token); // 验证token
        } catch (JWTVerificationException e) {
            throw new ServiceException("401", "请登录"); // 如果验证失败,则抛出异常,提示用户登录
        }
        return true; // 验证通过,放行请求
    }
}

配置拦截器

@Configuration // 表示这是一个配置类
public class InterceptorConfig extends WebMvcConfigurationSupport { // 继承WebMvcConfigurationSupport,可以自定义拦截器

    @Override // 重写父类的addInterceptors方法
    protected void addInterceptors(InterceptorRegistry registry) { // 添加拦截器的方法
        registry.addInterceptor(jwtInterceptor()) // 添加自定义的JwtInterceptor拦截器
                .addPathPatterns("/**") // 拦截所有请求路径
                .excludePathPatterns("/login","/register","/file"); // 排除登录、注册和文件相关的请求路径
        super.addInterceptors(registry); // 调用父类的addInterceptors方法
    }

    @Bean // 表示这是一个Bean
    public JwtInterceptor jwtInterceptor() { // 定义一个JwtInterceptor的Bean
        return new JwtInterceptor(); // 返回一个新的JwtInterceptor实例
    }

}

工具类 TokenUtils

@Component // 表示这是一个组件类
public class TokenUtils {

    private static UserMapper staticUserMapper; // 静态的UserMapper对象,用于获取当前用户

    @Autowired // 自动注入UserMapper对象
    UserMapper userMapper;

    @PostConstruct // 在构造方法执行后调用该方法
    public void setUserService() {
        staticUserMapper = userMapper; // 将非静态的UserMapper对象赋值给静态的UserMapper对象
    }

    @Operation(summary = "生成token") // 生成token的方法
    public static String genToken(String userId, String sign) {
        return JWT.create().withAudience(userId) // 将 user id 保存到 token 里面,作为载荷
                .withExpiresAt(DateUtil.offsetHour(new Date(), 24)) // 2小时后token过期
                .sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的密钥
    }

    @Operation(summary = "获取当前用户") // 获取当前用户的方法
    public static User getCurrentUser() {
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // 获取当前请求对象
            String token = request.getHeader("token"); // 从请求头中获取token
            if (StrUtil.isNotBlank(token)) { // 如果token不为空
                String userId = JWT.decode(token).getAudience().get(0); // 解码token,获取userid
                return staticUserMapper.selectById(Integer.valueOf(userId)); // 根据userid查询用户信息并返回
            }
        } catch (Exception e) {
            return null; // 如果发生异常,返回null
        }
        return null; // 如果token为空或无效,返回null
    }
}

另外一种解密

@Component // 表示这是一个组件类
public class JWTComponent {
    // 设置token过期时间为1天
    private final LocalDateTime time = LocalDateTime.now().plusDays(1);
    // 从配置文件中获取私钥
    @Value("${my.secretkey}")
    private String secretkey;
    private Algorithm algorithm;
    // 组件初始化后,初始化加密算法对象。无需反复创建
    @PostConstruct
    private void init() {
        algorithm = Algorithm.HMAC256(secretkey);
    }
    // 生成token的方法
    public String encode(Map<String, Object> map) {
        return JWT.create()
                .withPayload(map) // 将传入的map作为payload
                .withIssuedAt(new Date()) // 设置token的签发时间为当前时间
                .withExpiresAt(Date.from(time.atZone(ZoneId.systemDefault()).toInstant())) // 设置token的过期时间为1天后
                .sign(algorithm); // 使用指定的加密算法进行签名
    }

    // 验证token的方法
    public DecodedJWT decode(String token) {
        try {
            return JWT.require(algorithm).build().verify(token); // 使用指定的加密算法进行验证
        } catch (TokenExpiredException | SignatureVerificationException e) {
            if (e instanceof SignatureVerificationException) {
                throw XException.builder().code(Code.FORBIDDEN).build(); // 如果签名验证失败,抛出异常
            }
            throw XException.builder().code(Code.TOKEN_EXPIRED).build(); // 如果token已过期,抛出异常
        }
    }
}

生成token

token

登录后存储数据

4.2MD5加密算法使用

在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

        <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.3AES加密算法使用

AES算法采用不同的密钥长度,包括128位、192位和256位。这些密钥长度的选择可以根据需要和安全性要求进行调整。

以AES 256来说,AES 256表示使用256位的密钥长度,这是目前最安全的AES密钥长度。AES 256提供了更高的安全性和更强的加密能力,适用于对敏感数据进行保护。

加密过程中,原始数据通过AES算法和密钥进行加密,生成密文。解密过程中,密文通过相同的AES算法和密钥进行解密,恢复为原始数据。

AES 256密钥的加密/解密可以在Java中通过javax.crypto包中的Cipher类来实现。


/**
 * AESOperator类用于对用户敏感信息进行加密处理
 */
public class AESOperator {

    /*
     * 加密所使用的Key,需为16位字符
     */
    private String sKey = "0123456789abcdef";
    
    /*
     * 初始向量IV,需为16位字符
     */
    private String ivParameter = "0123456789abcdef";
    private static AESOperator instance = null;

    private AESOperator() {

    }

    /**
     * 获取AESOperator实例
     * @return AESOperator实例
     */
    public static AESOperator getInstance() {
        if (instance == null)
            instance = new AESOperator();
        return instance;
    }

    /**
     * 对字符串进行AES加密
     * @param sSrc 待加密的字符串
     * @return 加密后的字符串
     * @throws Exception 加密过程中的异常
     */
    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 Base64.getEncoder().encodeToString(encrypted);
    }

    /**
     * 对字符串进行AES解密
     * @param sSrc 待解密的字符串
     * @return 解密后的字符串
     * @throws Exception 解密过程中的异常
     */
    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 = Base64.getDecoder().decode(sSrc);
            byte[] original = cipher.doFinal(encrypted1);
            return new String(original, "utf-8");
        } 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 + "毫秒");
    }
}

  • 24
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烟雨平生9527

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值