1. 密码加密
- 密码加密:在 Spring Security 中,密码加密是单向的,即加密后的密码无法解密回原始密码。这是为了确保即使数据库被泄露,攻击者也无法轻易获取用户的明文密码。
- 存储加密密码:Spring Security 使用
PasswordEncoder
对密码进行加密,并将加密后的密码存储在数据库中。(内置了很多种加密方法,可按需使用)
为什么密码加密一般是单向的?
SpringSecurity中的密码只能加密不能解密(不可逆),由于大部分加密算法是加密之后的串
和随机盐
生成最终的密钥,相同的密码加密之后也会变成不一样的密钥,除了用户自己没人知道密码,这样保证了密码的安全性。
为什么相同的密码,注册时生成的是不同的密钥,后台又是怎么知道用户输入的密码是正确的?
因为用户登录时,后台根据用户账号去数据库查找当前用户存在数据库里的密钥串,并解析这个串,获取其中的盐值
,并和用户当前输入的密码加密之后的串
重新加密。(密码加密后的串+盐值--->最终要和数据库里的密钥值比对的值)
2. 信息(用户、密码、验证码等)传输加解密
(前后端信息传输需要用到)
- 传输加解密:为了确保密码在传输过程中的安全性,前端可以使用公钥加密密码,后端使用私钥解密(此方式为RSA非对称加密,也可使用对称加密)。这是一种常见的做法,可以防止中间人攻击。
- 加密:想象你有一封重要的信件,你不想让别人看到信的内容。你可以把信放在一个保险箱里,只有你有钥匙(密钥)可以打开保险箱。这个过程就是加密。
- 解密:当你收到这封放在保险箱里的信件时,你可以用钥匙(密钥)打开保险箱,取出信件并读取内容。这个过程就是解密。
- 公钥和私钥:公钥可以公开,私钥必须保密。公钥通常存储在前端的公共 JavaScript 文件中(也可将公钥存储在服务器上,并通过 API 动态获取。这样可以避免公钥被篡改或泄露。),而私钥存储在后端的配置文件中(如
.properties
或.yml
文件),也可将私钥存储在环境变量或安全的密钥管理服务中,这样更安全。
3.签名和验签
(防止信息被篡改,一般用户登录之后用token值去调后台接口时需要验证是否被篡改,没有被篡改即可调用后台接口,如果被人为篡改过就不支持后续的操作)
- 签名:想象你写了一封信,并在信的末尾签上你的名字。这个签名可以证明这封信是你写的,并且信的内容没有被篡改。这个过程就是签名。
- 验签:当别人收到这封信时,可以通过比对签名和信的内容,确认这封信确实是你写的,并且信的内容没有被篡改,别人才会按照信的指示给你一定的响应。这个过程就是验签。
为什么这样设计?
- 密码加密:
实现PasswordEncoder类的内置类/自定义加密方法:这样设计是为了保护密码的机密性,只有用户自己才知道登录密码。
- 信息传输加解密:
公钥加密,私钥解密:这样设计是为了保护数据的机密性,只有拥有私钥的人才能解密数据。
- 签名和验签:
私钥签名,公钥验签:这样设计是为了确保数据的完整性和来源的可靠性。只有拥有私钥的人才能生成签名,其他人可以用公钥验证签名是否有效,确保数据确实来自签名者,并且没有被篡改。
RSA加密案例
前端加密
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
// 公钥,密钥对生成地址 http://web.chacuo.net/netrsakeypair
const publicKey = '你的RSA加密公钥'
// 账号密码传输加密
export function encrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
}
后端解密
/**
* RSA加解密工具类
*/
public class RSAUtils {
/**
* 私钥解密
* SecretConstants.DATA_RSA_PRIVATE_KEY 私钥
*/
public static String privateDecrypt(String data) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过PKCS#8编码的Key指令获得私钥对象
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(SecretConstants.DATA_RSA_PRIVATE_KEY)));
RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, key);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), key.getModulus().bitLength()), "UTF-8");
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
}
// 调用解密传输信息方法
RSAUtils.privateDecrypt(String.valueOf(credentials));
对于安全系数要求高的登录系统
通常会采用以下措施:
注意:jwt值可以看作一个token,下面会有两对公钥私钥,分别为public_key,private_key
,public_sign_key,private_sign_key
- 前端的账号密码验证码传输时,使用后端提供的公钥加密(public_key)也可以使用对称加密,即前后台使用一个密钥),并生成一个uuid(uuid需存在前端缓存
sessionStorage
里)方便后续使用 - 后端接收uuid后,将
uuid作为key
的一部分,jwt的值私钥签名(private_sign_key)之后作为value
值,后续后端存入redis中。 - 先进行私钥解密(private_key)就能获取前端的账号密码验证码信息,后端做登录密码验证时,需要将私钥解密的数据(即前端传过来的密码)再次
加密
(密码加密,一般可自行MD5+盐加密或者使用PasswordEncoder内置的实现方法),再和数据库里的密码(存的加密后的数据)比对,一致则登录成功,否则登录失败。 - 前端调用后台接口前都需要验签(白名单接口除外),此时需要将sessionStorage中的uuid和参数的其他的参数一并传到后台接口,后台接口获取请求参数中的uuid,并去查找存在redis中的jwt值签名值,并用公钥解签(public_sign_key)这个值(验证jwt是否是系统颁发的)
- 验签通过之后即可调用接口的信息
加密算法
- 加密算法:常见的非对称加密算法有 RSA、ECC 等。确保选择的算法足够安全,并且符合你的应用需求。